proxmox-kernel-helper/bin/proxmox-boot-tool
Stoiko Ivanov 83a41a1a03 fix #3761: proxmox-boot: add pin/unpin for kernel-version
The 2 commands follow the mechanics of p-b-t kernel add/remove in
writing the desired abi-version to a config-file in /etc/kernel and
actually modifying the boot-loader configuration upon p-b-t refresh.

A dedicated new file is used instead of writing the version (with some
kind of annotation) to the manual kernel list to keep parsing the file
simple (and hopefully also cause fewer problems with manually edited
files)

For systemd-boot we write the entry into the loader.conf on the ESP(s)
instead of relying on the `bootctl set-default` mechanics (bootctl(1))
which write the entry in an EFI-var. This was preferred, because of a
few reports of unwriteable EFI-vars on some systems (e.g. DELL servers
have a setting preventing writing EFI-vars from the OS). The rationale
in `Why not simply rely on the EFI boot menu logic?` from [0] also
makes a few points in that direction.

For grub the following choices were made:
* write the pinned version (or actually the menu-path leading to it)
  to a snippet in /etc/default/grub.d instead of editing the grub.cfg
  files on the partition. Mostly to divert as little as possible from
  the grub-workflow I assume people are used to.
* the 'root-device-id' part of the menu-entries is parsed from
  /boot/grub/grug.cfg since it was stable (the same on all ESPs and in
  /boot/grub), saves us from copying the part of "find device behind
  /, mangle it if zfs/btrfs, call grub_probe a few times" part of
  grub-mkconfig - and seems a bit more robust

Tested with a BIOS and an UEFI VM with / on ZFS.

[0] https://systemd.io/BOOT_LOADER_SPECIFICATION/

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
Reviewed-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Tested-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2022-02-22 13:31:31 +01:00

547 lines
12 KiB
Bash
Executable File

#!/bin/sh
set -e
. /usr/share/pve-kernel-helper/scripts/functions
_add_entry_to_list_file() {
file="$1"
entry="$2"
if [ -e "$file" ]; then
cp "$file" "$file.new"
fi
echo "$entry" >> "$file.new"
sort -uo "$file.new" "$file.new"
mv "$file.new" "$file"
}
_remove_entry_from_list_file() {
file="$1"
entry="$2"
# guard against removing whole file by accident!
if [ -z "$entry" ]; then
echo "cannot remove empty entry from '$file'."
return
fi
if [ -e "$file" ]; then
grep -vFx "$entry" "$file" > "$file.new" || true
mv "$file.new" "$file"
else
echo "'$file' does not exist.."
fi
}
_get_partition_info() {
if [ ! -e "$1" ]; then
warn "E: '$1' does not exist!"
exit 1
fi
bdev=$(realpath "$1")
if [ ! -b "$bdev" ]; then
warn "E: '$bdev' is not a block device!"
exit 1
fi
bdev_info=$( \
lsblk \
--bytes \
--pairs \
-o 'UUID,SIZE,FSTYPE,PARTTYPE,PKNAME,MOUNTPOINT' \
"$bdev" \
)
if [ -z "$bdev_info" ]; then
warn "E: unable to get information about block device '$1'!"
exit 1
fi
count=$(echo "$bdev_info" | grep -c '^')
if [ "$count" -ne '1' ]; then
echo "$bdev_info"
warn "E: block device '$1' has children!"
exit 1
fi
echo "$bdev_info"
eval "$bdev_info"
if [ -z "$PKNAME" ]; then
warn "E: cannot determine parent device of '$1' - please provide a partition, not a full disk."
exit 1
fi
if [ -n "$SIZE" ] && [ "$SIZE" -lt 268435456 ]; then
warn "E: '$1' is too small (<256M)."
exit 1
fi
if [ -n "$MOUNTPOINT" ]; then
warn "E: '$1' is mounted on '$MOUNTPOINT' - exiting."
exit 1
fi
}
format() {
part="$1"
force="$2"
_get_partition_info "$part"
if [ -n "$FSTYPE" ]; then
if [ -z "$force" ] || [ "$force" != '--force' ]; then
warn "E: '$part' contains a filesystem ('$FSTYPE') - exiting (use --force to override)"
exit 1
fi
fi
part_basename=$(basename "$bdev")
if [ -z "$part_basename" ]; then
if [ $part != $bdev ]; then
symlinkmsg=" -> '$bdev'"
fi
warn "E: unable to determine basename of '$part'$symlinkmsg"
exit 1
fi
part_num=$(cat /sys/block/"$PKNAME"/"$part_basename"/partition)
if [ -z "$part_num" ]; then
warn "E: unable to determine partition number of '$part'"
exit 1
fi
if [ -z "$PARTTYPE" ] || [ "$PARTTYPE" != "$ESPTYPE" ]; then
echo "Setting partition type of '$part' to '$ESPTYPE'.."
sgdisk "-t$part_num:$ESPTYPE" "/dev/$PKNAME"
echo "Calling 'udevadm settle'.."
udevadm settle --timeout=5
fi
echo "Formatting '$part' as vfat.."
mkfs.vfat -F 32 "$part"
echo "Done."
exit 0
}
init() {
part="$1"
_get_partition_info "$part"
if [ -z "$PARTTYPE" ] || [ "$PARTTYPE" != "$ESPTYPE" ]; then
warn "E: '$part' has wrong partition type (!= $ESPTYPE)."
exit 1
fi
if [ -z "$FSTYPE" ] || [ "$FSTYPE" != 'vfat' ]; then
warn "E: '$part' has wrong filesystem (!= vfat)."
exit 1
fi
if [ -z "$UUID" ]; then
warn "E: '$part' has no UUID set, required for mounting."
exit 1
fi
esp_mp="/var/tmp/espmounts/$UUID"
mkdir -p "$esp_mp"
echo "Mounting '$part' on '$esp_mp'."
mount -t vfat "$part" "$esp_mp"
if [ -d /sys/firmware/efi ]; then
echo "Installing systemd-boot.."
mkdir -p "$esp_mp/$PMX_ESP_DIR"
bootctl --path "$esp_mp" install
echo "Configuring systemd-boot.."
echo "timeout 3" > "$esp_mp/$PMX_LOADER_CONF.tmp"
echo "default proxmox-*" >> "$esp_mp/$PMX_LOADER_CONF.tmp"
mv "$esp_mp/$PMX_LOADER_CONF.tmp" "$esp_mp/$PMX_LOADER_CONF"
else
echo "Installing grub i386-pc target.."
grub-install.real \
--boot-directory $esp_mp \
--target i386-pc \
--no-floppy \
--bootloader-id='proxmox' \
"/dev/$PKNAME"
fi
echo "Unmounting '$part'."
umount "$part"
echo "Adding '$part' to list of synced ESPs.."
_add_entry_to_list_file "$ESP_LIST" "$UUID"
echo "Refreshing kernels and initrds.."
refresh
}
_clean_impl() {
if [ ! -e "/dev/disk/by-uuid/" ]; then
warn 'E: /dev/disk/by-uuid does not exist, aborting!'
exit 1
fi
echo -n "Checking whether ESP '$curr_uuid' exists.. "
if [ -e "/dev/disk/by-uuid/$curr_uuid" ]; then
echo "Found!"
else
echo "Not found!"
if [ -z "$dry_run" ] || [ "$dry_run" != '--dry-run' ]; then
_remove_entry_from_list_file "$ESP_LIST" "$curr_uuid"
fi
fi
}
clean() {
dry_run="$1"
rm -f "$ESP_LIST".tmp
loop_esp_list _clean_impl
if [ "$?" -eq 2 ]; then
warn "E: $ESP_LIST does not exist."
exit 1
fi
if [ -e "$ESP_LIST".tmp ]; then
mv "$ESP_LIST".tmp "$ESP_LIST"
fi
echo "Sorting and removing duplicate ESPs.."
sort -uo "$ESP_LIST".tmp "$ESP_LIST"
mv "$ESP_LIST".tmp "$ESP_LIST"
}
refresh() {
hook=$1
hookscripts='proxmox-auto-removal zz-proxmox-boot'
if [ -n "$hook" ]; then
if echo "$hookscripts" | grep -sqE "(^|[[:space:]]+)$hook([[:space:]]+|$)"; then
hookscripts="$hook"
else
warn "E: '$hook' is not a valid hook script name.";
exit 1;
fi
fi
for script in $hookscripts; do
scriptpath="/etc/kernel/postinst.d/$script"
if [ -f "$scriptpath" ] && [ -x "$scriptpath" ]; then
echo "Running hook script '$script'.."
$scriptpath
else
warn "Hook script '$script' not found or not executable, skipping."
fi
done
}
add_kernel() {
ver="$1"
if [ -z "$ver" ]; then
warn "E: <kernel-version> is mandatory"
warn ""
exit 1
fi
if [ ! -e "/boot/vmlinuz-$ver" ]; then
warn "E: no kernel image found in /boot for '$ver', not adding."
exit 1
fi
_add_entry_to_list_file "$MANUAL_KERNEL_LIST" "$ver"
echo "Added kernel '$ver' to manual kernel list. Use the 'refresh' command to update the ESPs."
}
remove_kernel() {
ver="$1"
if [ -z "$ver" ]; then
warn "E: <kernel-version> is mandatory"
warn ""
exit 1
fi
if grep -sqFx "$ver" "$MANUAL_KERNEL_LIST"; then
_remove_entry_from_list_file "$MANUAL_KERNEL_LIST" "$ver"
echo "Removed kernel '$ver' from manual kernel list. Use the 'refresh' command to update the ESPs."
else
echo "Kernel '$ver' not found in manual kernel list."
fi
}
list_kernels() {
boot_kernels="$(boot_kernel_list)"
if [ -e "$MANUAL_KERNEL_LIST" ]; then
manual_kernels="$(cat "$MANUAL_KERNEL_LIST" || true)"
boot_kernels="$(echo "$boot_kernels" | grep -Fxv -f "$MANUAL_KERNEL_LIST" || true)"
fi
if [ -z "$manual_kernels" ]; then
manual_kernels="None."
fi
echo "Manually selected kernels:"
echo "$manual_kernels"
echo ""
echo "Automatically selected kernels:"
echo "$boot_kernels"
pinned_kernel="$(get_first_line "$PINNED_KERNEL_CONF")"
if [ -n "$pinned_kernel" ]; then
echo ""
echo "Pinned kernel:"
echo "${pinned_kernel}"
fi
}
usage() {
warn "USAGE: $0 <commands> [ARGS]"
warn ""
warn " $0 format <partition> [--force]"
warn " $0 init <partition>"
warn " $0 clean [--dry-run]"
warn " $0 refresh [--hook <name>]"
warn " $0 kernel <add|remove> <kernel-version>"
warn " $0 kernel pin <kernel-version>"
warn " $0 kernel unpin"
warn " $0 kernel list"
warn " $0 status [--quiet]"
warn " $0 help"
}
help() {
echo "USAGE: $0 format <partition> [--force]"
echo ""
echo " format <partition> as EFI system partition. Use --force to format even if <partition> is currently in use."
echo ""
echo "USAGE: $0 init <partition>"
echo ""
echo " initialize EFI system partition at <partition> for automatic synchronization of pve-kernels and their associated initrds."
echo ""
echo "USAGE: $0 clean [--dry-run]"
echo ""
echo " remove no longer existing EFI system partition UUIDs from $ESP_LIST. Use --dry-run to only print outdated entries instead of removing them."
echo ""
echo "USAGE: $0 refresh [--hook <name>]"
echo ""
echo " refresh all configured EFI system partitions. Use --hook to only run the specified hook, omit to run all."
echo ""
echo "USAGE: $0 kernel <add|remove> <kernel-version>"
echo ""
echo " add/remove pve-kernel with ABI <kernel-version> to list of synced kernels, in addition to automatically selected ones."
echo " NOTE: you need to manually run 'refresh' once you're finished with adding/removing kernels from the list"
echo ""
echo "USAGE: $0 kernel pin <kernel-version>"
echo ""
echo " pin pve-kernel with ABI <kernel-version> as the default entry to be booted."
echo " NOTE: you need to manually run 'refresh' once you're finished with pinning kernels"
echo ""
echo "USAGE: $0 kernel unpin"
echo ""
echo " unpin sets the latest kernel as the default entry (undoes a previous pin)"
echo ""
echo "USAGE: $0 kernel list"
echo ""
echo " list kernel versions currently selected for inclusion on ESPs."
echo ""
echo "USAGE: $0 status [--quiet]"
echo ""
echo " Print details about the ESPs configuration. Exits with 0 if any ESP is configured, else with 2."
echo ""
}
_status_detail() {
if ! (echo "${curr_uuid}" | grep -qE '[0-9a-fA-F]{4}-[0-9a-fA-F]{4}'); then
warn "WARN: ${curr_uuid} read from ${ESP_LIST} does not look like a VFAT-UUID - skipping"
return
fi
path="/dev/disk/by-uuid/$curr_uuid"
if [ ! -e "${path}" ]; then
warn "WARN: ${path} does not exist - clean '${ESP_LIST}'! - skipping"
return
fi
mountpoint="${MOUNTROOT}/${curr_uuid}"
mkdir -p "${mountpoint}" || \
{ warn "creation of mountpoint ${mountpoint} failed - skipping"; return; }
mount "${path}" "${mountpoint}" || \
{ warn "mount of ${path} failed - skipping"; return; }
result=""
if [ -f "${mountpoint}/$PMX_LOADER_CONF" ]; then
if [ ! -d "${mountpoint}/$PMX_ESP_DIR" ]; then
warn "${path}/$PMX_ESP_DIR does not exist"
fi
versions_uefi=$(ls -1 ${mountpoint}/$PMX_ESP_DIR | awk '{printf (NR>1?", ":"") $0}')
result="uefi (versions: ${versions_uefi})"
fi
if [ -d "${mountpoint}/grub" ]; then
versions_grub=$(ls -1 ${mountpoint}/vmlinuz-* | awk '{ gsub(/.*\/vmlinuz-/, ""); printf (NR>1?", ":"") $0 }')
if [ -n "$result" ]; then
result="${result}, grub (versions: ${versions_grub})"
else
result="grub (versions: ${versions_grub})"
fi
fi
echo "$curr_uuid is configured with: $result"
umount "${mountpoint}" || \
{ warn "umount of ${path} failed - failure"; exit 0; }
rmdir "${mountpoint}" || true
}
status() {
quiet="$1"
if [ ! -e "${ESP_LIST}" ]; then
if [ -z "$quiet" ]; then
warn "E: $ESP_LIST does not exist."
fi
exit 2
fi
if [ -z "$quiet" ]; then
if [ -d /sys/firmware/efi ]; then
echo "System currently booted with uefi"
else
echo "System currently booted with legacy bios"
fi
loop_esp_list _status_detail
fi
}
pin_kernel() {
ver="$1"
if [ -z "$ver" ]; then
warn "E: <kernel-version> is mandatory"
warn ""
exit 1
fi
if [ ! -e "/boot/vmlinuz-$ver" ]; then
warn "E: no kernel image found in /boot for '$ver', not setting default."
exit 1
fi
echo "$ver" > "$PINNED_KERNEL_CONF"
echo "Set kernel '$ver' $PINNED_KERNEL_CONF. Use the 'refresh' command to update the ESPs."
}
unpin_kernel() {
rm -f "$PINNED_KERNEL_CONF"
echo "Removed $PINNED_KERNEL_CONF. Use the 'refresh' command to update the ESPs."
}
if [ -z "$1" ]; then
usage
exit 0
fi
case "$1" in
'format')
shift
if [ -z "$1" ]; then
warn "E: <partition> is mandatory."
warn ""
usage
exit 1
fi
format "$@"
exit 0
;;
'init')
reexec_in_mountns "$@"
shift
if [ -z "$1" ]; then
warn "E: <partition> is mandatory."
warn ""
usage
exit 1
fi
init "$@"
exit 0
;;
'clean')
shift
clean "$@"
exit 0
;;
'refresh')
shift
if [ "$#" -eq 0 ]; then
refresh
elif [ "$#" -eq 2 ] && [ "$1" = "--hook" ]; then
refresh "$2"
else
usage
exit 1
fi
exit 0
;;
'kernel'|'kernels')
shift
if [ -z "$1" ]; then
warn "E: subcommand is mandatory for 'kernel'."
warn ""
usage
exit 1
fi
cmd="$1"
case "$cmd" in
'add')
add_kernel "$2"
exit 0
;;
'remove')
remove_kernel "$2"
exit 0
;;
'list')
list_kernels
exit 0
;;
'pin')
pin_kernel "$2"
exit 0
;;
'unpin')
unpin_kernel "$2"
exit 0
;;
*)
warn "E: invalid 'kernel' subcommand '$cmd'."
warn ""
usage
exit 1
;;
esac
;;
'status')
if [ "$#" -eq 2 ] && [ "$2" = '--quiet' ]; then
shift
status "$1"
elif [ "$#" -eq 1 ]; then
reexec_in_mountns "$@"
shift
status
else
usage
exit 1
fi
exit 0
;;
'help')
shift
help
exit 0
;;
*)
warn "Invalid/unknown command '$1'."
warn ""
usage
exit 1
;;
esac
exit 1