@@ -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+
9251048def 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 '\n menuentry ' 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
9641102def generate_legacy_grub_entry (remote , newversion ):
0 commit comments