]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
kernel: Support UEFI and BLS
authorDavid Galloway <david.galloway@ibm.com>
Tue, 16 Dec 2025 02:57:00 +0000 (21:57 -0500)
committerDavid Galloway <david.galloway@ibm.com>
Tue, 23 Dec 2025 20:45:38 +0000 (15:45 -0500)
Signed-off-by: David Galloway <david.galloway@ibm.com>
teuthology/task/kernel.py

index 22cd1a69c20d81428c67f7ae6270ad37e780d53e..86979629430ecf55206dda47865f2f0c3c451c6a 100644 (file)
@@ -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/<vendor>/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/<vendor>/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):