From ccbe1cd2def3959c613386b15bd294f4b6440cdf Mon Sep 17 00:00:00 2001 From: David Galloway Date: Mon, 15 Dec 2025 21:57:00 -0500 Subject: [PATCH] kernel: Support UEFI and BLS Signed-off-by: David Galloway (cherry picked from commit dc3469495ce0fb697e7a7a7126ef040033f95bde) --- teuthology/task/kernel.py | 154 +++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 36 deletions(-) diff --git a/teuthology/task/kernel.py b/teuthology/task/kernel.py index 22cd1a69c..869796294 100644 --- a/teuthology/task/kernel.py +++ b/teuthology/task/kernel.py @@ -922,43 +922,132 @@ def update_grub_rpm(remote, newversion): grub2_kernel_select_generic(remote, newversion, 'rpm') +def _rpm_grub_cfg(remote) -> str: + """ + On RPM distros, always regenerate /boot/grub2/grub.cfg. + /etc/grub2.cfg and /etc/grub2-efi.cfg are typically symlinks to it. + Never mkconfig directly into /boot/efi/EFI//grub.cfg (wrapper/stub). + """ + cmd = r''' +set -e +for p in /etc/grub2-efi.cfg /etc/grub2.cfg /boot/grub2/grub.cfg; do + if [ -e "$p" ]; then + readlink -f "$p" || echo "$p" + exit 0 + fi +done +echo /boot/grub2/grub.cfg +''' + return remote.sh(['bash', '-lc', cmd]).strip() + +def _rpm_ensure_uefi_esp_stub(remote) -> None: + """ + If UEFI, ensure /boot/efi/EFI//grub.cfg is a stub that chains to /boot/grub2/grub.cfg. + This prevents 'grub2-mkconfig' from producing a static ESP grub.cfg that ignores BLS updates. + """ + cmd = r''' +set -euo pipefail +[ -d /sys/firmware/efi ] || exit 0 + +# Ensure ESP is mounted +if ! mountpoint -q /boot/efi; then + mount /boot/efi 2>/dev/null || true +fi +mountpoint -q /boot/efi || exit 0 + +# UUID of /boot if separate, else / +boot_uuid="$(findmnt -no UUID /boot 2>/dev/null || true)" +if [ -z "$boot_uuid" ]; then + boot_uuid="$(findmnt -no UUID /)" +fi +[ -n "$boot_uuid" ] || exit 0 + +for d in /boot/efi/EFI/*; do + [ -d "$d" ] || continue + # only touch vendor dirs that look bootable + [ -e "$d/shimx64.efi" ] || [ -e "$d/grubx64.efi" ] || continue + + cfg="$d/grub.cfg" + [ -e "$cfg" ] || continue + + # If it's already a stub chaining to grub2, do nothing + if grep -qE '^[[:space:]]*configfile[[:space:]]+\$prefix/grub\.cfg' "$cfg" \ + || grep -qE '^[[:space:]]*configfile[[:space:]]+.*grub2/grub\.cfg' "$cfg"; then + continue + fi + + # If it's a full mkconfig output, replace with stub. + # (We key off the generated header or menuentry.) + if grep -qE 'generated by grub2-mkconfig|^menuentry ' "$cfg"; then + cp -a "$cfg" "$cfg.bak.$(date +%s)" || true + printf '%s\n' \ + "search --no-floppy --fs-uuid --set=root ${boot_uuid}" \ + 'set prefix=($root)/boot/grub2' \ + 'configfile $prefix/grub.cfg' \ + > "$cfg" + fi +done +''' + remote.run(args=['sudo', 'bash', '-ceu', cmd]) + + def grub2_kernel_select_generic(remote, newversion, ostype): """ - Can be used on DEB and RPM. Sets which entry should be boted by entrynum. + DEB: grub-set-default + grub-mkconfig. + RPM: grub2-set-default + grub2-mkconfig to /boot/grub2/grub.cfg, plus UEFI ESP stub enforcement. """ - log.info("Updating grub on {node} to boot {version}".format( - node=remote.shortname, version=newversion)) + log.info("Updating grub on %s to boot %s", remote.shortname, newversion) + if ostype == 'rpm': grubset = 'grub2-set-default' mkconfig = 'grub2-mkconfig' - grubconfig = '/boot/grub2/grub.cfg' - if ostype == 'deb': + grubconfig = _rpm_grub_cfg(remote) + + # Critical: on UEFI, make sure ESP grub.cfg is a stub (not a full mkconfig output) + _rpm_ensure_uefi_esp_stub(remote) + + elif ostype == 'deb': grubset = 'grub-set-default' - grubconfig = '/boot/grub/grub.cfg' mkconfig = 'grub-mkconfig' - remote.run(args=['sudo', mkconfig, '-o', grubconfig, ]) + grubconfig = '/boot/grub/grub.cfg' + else: + raise ValueError(f"unknown ostype: {ostype}") + + # Safe everywhere (and required on some setups) + remote.run(args=['sudo', mkconfig, '-o', grubconfig]) + grub2conf = teuthology.get_file(remote, grubconfig, sudo=True).decode() - entry_num = 0 - if '\nmenuentry ' not in grub2conf: - # okay, do the newer (el8) grub2 thing - grub2conf = remote.sh('sudo /bin/ls /boot/loader/entries || true') - entry = None - for line in grub2conf.split('\n'): - if line.endswith('.conf') and newversion in line: - entry = line[:-5] # drop .conf suffix + + entry = None + + is_bls = ('blscfg' in grub2conf) + + if is_bls: + bls = remote.sh( + 'sudo ls -1 /boot/loader/entries 2>/dev/null || true' + ).splitlines() + + for fname in bls: + if not fname.endswith('.conf'): + continue + if newversion in fname: + entry = fname[:-5] # strip ".conf" break else: - # do old menuitem counting thing - for line in grub2conf.split('\n'): + # Legacy menuentry counting + entry_num = 0 + for line in grub2conf.splitlines(): if line.startswith('menuentry '): if newversion in line: + entry = str(entry_num) break entry_num += 1 - entry = str(entry_num) + if entry is None: - log.warning('Unable to update grub2 order') - else: - remote.run(args=['sudo', grubset, entry]) + log.warning('Unable to update grub order (no matching entry for %s)', newversion) + return + + remote.run(args=['sudo', grubset, entry]) def generate_legacy_grub_entry(remote, newversion): @@ -1048,23 +1137,16 @@ def get_latest_image_version_rpm(remote): Used for distro case. """ dist_release = remote.os.name - kernel_pkg_name = None - version = None - if dist_release in ['opensuse', 'sle']: + if dist_release in ['opensuse', 'sle']: kernel_pkg_name = "kernel-default" + newest_package = remote.sh('rpm -q %s | sort -rV | head -n 1' % kernel_pkg_name).strip() + return newest_package.split(kernel_pkg_name + '-')[1] else: - kernel_pkg_name = "kernel" - # get tip of package list ordered by descending version - newest_package = remote.sh( - 'rpm -q %s | sort -rV | head -n 1' % kernel_pkg_name).strip() - for kernel in newest_package.split(): - if kernel.startswith('kernel'): - if 'ceph' not in kernel: - if dist_release in ['opensuse', 'sle']: - kernel = kernel.split()[0] - version = kernel.split(str(kernel_pkg_name) + '-')[1] - log.debug("get_latest_image_version_rpm: %s", version) - return version + # kernel-core is the reliable “real kernel” version carrier on EL8/9 + newest = remote.sh( + r"rpm -q kernel-core --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort -V | tail -1" + ).strip() + return newest def get_latest_image_version_deb(remote, ostype, role_config): -- 2.47.3