Skip to content

Commit a2e017d

Browse files
authored
Merge pull request #2117 from deepssin/uefi_fix
Fix kernel boot on UEFI systems
2 parents b009fcc + a5d45de commit a2e017d

File tree

1 file changed

+158
-20
lines changed

1 file changed

+158
-20
lines changed

teuthology/task/kernel.py

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -922,43 +922,181 @@ def update_grub_rpm(remote, newversion):
922922
grub2_kernel_select_generic(remote, newversion, 'rpm')
923923

924924

925+
def _kernel_is_uefi(remote):
926+
"""Return True if the remote is booted in UEFI mode."""
927+
try:
928+
remote.run(args=['test', '-d', '/sys/firmware/efi'])
929+
return True
930+
except Exception:
931+
return False
932+
933+
934+
def _kernel_has_bls(remote):
935+
"""Return True if BLS entries exist on the remote.
936+
937+
We use a conservative detection approach:
938+
1) If /boot/loader/entries contains any *.conf files, treat as BLS.
939+
2) Otherwise, best-effort parse GRUB_ENABLE_BLSCFG from /etc/default/grub.
940+
"""
941+
try:
942+
out = remote.sh(
943+
'sudo find /boot/loader/entries -maxdepth 1 -name "*.conf" -type f -print -quit'
944+
).strip()
945+
if out:
946+
return True
947+
except Exception:
948+
pass
949+
950+
try:
951+
val = remote.sh(
952+
r"sudo grep -E '^GRUB_ENABLE_BLSCFG=' /etc/default/grub | "
953+
r"cut -d'=' -f2 | sed \"s/^['\\\"]//; s/['\\\"]$//\" || echo ''"
954+
).strip().lower()
955+
return val in ('1', 'y', 'yes', 'true')
956+
except Exception:
957+
return False
958+
959+
960+
def _kernel_select_bls_entry_id(remote, newversion):
961+
"""Return the BLS entry id (filename without .conf) for the requested kernel version."""
962+
# Fast path: grep for the version string directly
963+
try:
964+
entry = remote.sh(
965+
f'sudo find /boot/loader/entries -maxdepth 1 -name "*.conf" -type f | '
966+
f'grep -F "{newversion}" | head -n 1'
967+
).strip()
968+
if entry:
969+
basename = os.path.basename(entry)
970+
return basename[:-5] if basename.endswith('.conf') else None
971+
except Exception:
972+
pass
973+
974+
# fallback - grep for the version string in the entry file
975+
try:
976+
escaped_version = re.escape(newversion)
977+
entry = remote.sh(
978+
f'sudo find /boot/loader/entries -maxdepth 1 -name "*.conf" -type f -exec grep -l '
979+
f'-E "^(linux|version|options).*{escaped_version}" {{}} \\; | head -n 1'
980+
).strip()
981+
if entry:
982+
basename = os.path.basename(entry)
983+
return basename[:-5] if basename.endswith('.conf') else None
984+
except Exception:
985+
pass
986+
987+
return None
988+
989+
990+
def _kernel_set_default_bls(remote, newversion, ostype):
991+
"""Set default kernel on a BLS system.
992+
993+
Prefer `grubby --set-default` when available.
994+
Otherwise fall back to `grub2-set-default <BLS_ENTRY_ID>` using /boot/loader/entries.
995+
"""
996+
has_grubby = remote.sh("sudo command -v grubby && echo yes || echo no").strip() == 'yes'
997+
998+
if has_grubby and ostype == 'rpm':
999+
vmlinuz = remote.sh(
1000+
f"sudo find /boot -maxdepth 1 -name 'vmlinuz-*' -type f | "
1001+
f"grep -F '{newversion}' | head -n 1"
1002+
).strip()
1003+
if vmlinuz:
1004+
remote.run(args=['sudo', 'grubby', '--set-default', vmlinuz])
1005+
return True
1006+
1007+
entry_id = _kernel_select_bls_entry_id(remote, newversion)
1008+
if not entry_id:
1009+
return False
1010+
1011+
grubset = 'grub2-set-default' if ostype == 'rpm' else 'grub-set-default'
1012+
remote.run(args=['sudo', grubset, entry_id])
1013+
return True
1014+
1015+
1016+
def _kernel_sync_uefi_grubcfg(remote, grubconfig, ostype):
1017+
"""Sync firmware-facing UEFI grub.cfg with the system grub.cfg (RPM only).
1018+
1019+
On many RPM UEFI installs, firmware boots from /boot/efi/EFI/<vendor>/grub.cfg.
1020+
Regenerating /boot/grub2/grub.cfg alone may not affect the file firmware reads.
1021+
We copy the generated grub.cfg into each vendor dir's grub.cfg if it exists.
1022+
1023+
For DEB (Ubuntu/Debian), EFI grub.cfg is frequently a small stub; overwriting it
1024+
can break boot. So we intentionally do nothing for ostype == 'deb'.
1025+
"""
1026+
if ostype != 'rpm':
1027+
return
1028+
if not _kernel_is_uefi(remote):
1029+
return
1030+
efi_dirs = remote.sh(
1031+
"sudo find /boot/efi/EFI -mindepth 1 -maxdepth 1 -type d || true"
1032+
).splitlines()
1033+
if not efi_dirs:
1034+
return
1035+
1036+
for dir in efi_dirs:
1037+
dir = dir.strip()
1038+
if not dir:
1039+
continue
1040+
target = f"{dir.rstrip('/')}/grub.cfg"
1041+
exists = remote.sh(
1042+
f"sudo test -f {target} && echo yes || echo no"
1043+
).strip() == 'yes'
1044+
if exists:
1045+
remote.run(args=['sudo', 'cp', '-f', grubconfig, target])
1046+
1047+
9251048
def grub2_kernel_select_generic(remote, newversion, ostype):
9261049
"""
927-
Can be used on DEB and RPM. Sets which entry should be boted by entrynum.
1050+
Can be used on DEB and RPM.
1051+
1052+
Supports:
1053+
* Classic GRUB menuentry selection (non-BLS) by menuentry index
1054+
* BLS (Boot Loader Spec) systems (common on EL8+/Fedora) using grubby/BLS entry id
1055+
* UEFI RPM systems by syncing /boot/grub2/grub.cfg into /boot/efi/EFI/*/grub.cfg
9281056
"""
9291057
log.info("Updating grub on {node} to boot {version}".format(
9301058
node=remote.shortname, version=newversion))
9311059
if ostype == 'rpm':
9321060
grubset = 'grub2-set-default'
9331061
mkconfig = 'grub2-mkconfig'
9341062
grubconfig = '/boot/grub2/grub.cfg'
935-
if ostype == 'deb':
1063+
elif ostype == 'deb':
9361064
grubset = 'grub-set-default'
9371065
grubconfig = '/boot/grub/grub.cfg'
9381066
mkconfig = 'grub-mkconfig'
939-
remote.run(args=['sudo', mkconfig, '-o', grubconfig, ])
1067+
else:
1068+
raise UnsupportedPackageTypeError(f"Unknown ostype: {ostype}")
1069+
1070+
if _kernel_has_bls(remote):
1071+
status_ok = _kernel_set_default_bls(remote, newversion, ostype)
1072+
if not status_ok:
1073+
log.warning('Unable to set default kernel on BLS system')
1074+
return
1075+
# Regenerate grub.cfg to reflect the new default, then sync to UEFI
1076+
remote.run(args=['sudo', mkconfig, '-o', grubconfig])
1077+
_kernel_sync_uefi_grubcfg(remote, grubconfig, ostype)
1078+
return
1079+
1080+
# Non-BLS path- regenerate grub.cfg then pick the matching menuentry index.
1081+
remote.run(args=['sudo', mkconfig, '-o', grubconfig])
9401082
grub2conf = teuthology.get_file(remote, grubconfig, sudo=True).decode()
1083+
9411084
entry_num = 0
942-
if '\nmenuentry ' not in grub2conf:
943-
# okay, do the newer (el8) grub2 thing
944-
grub2conf = remote.sh('sudo /bin/ls /boot/loader/entries || true')
945-
entry = None
946-
for line in grub2conf.split('\n'):
947-
if line.endswith('.conf') and newversion in line:
948-
entry = line[:-5] # drop .conf suffix
1085+
entry = None
1086+
for line in grub2conf.split('\n'):
1087+
if line.startswith('menuentry '):
1088+
if newversion in line:
1089+
entry = str(entry_num)
9491090
break
950-
else:
951-
# do old menuitem counting thing
952-
for line in grub2conf.split('\n'):
953-
if line.startswith('menuentry '):
954-
if newversion in line:
955-
break
956-
entry_num += 1
957-
entry = str(entry_num)
1091+
entry_num += 1
1092+
9581093
if entry is None:
9591094
log.warning('Unable to update grub2 order')
960-
else:
961-
remote.run(args=['sudo', grubset, entry])
1095+
return
1096+
1097+
remote.run(args=['sudo', grubset, entry])
1098+
_kernel_sync_uefi_grubcfg(remote, grubconfig, ostype)
1099+
9621100

9631101

9641102
def generate_legacy_grub_entry(remote, newversion):

0 commit comments

Comments
 (0)