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):
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):