From abcf7a5d209d4f9fc054d39cf6866b2809fe382b Mon Sep 17 00:00:00 2001 From: David Kubek Date: Wed, 24 Jul 2024 21:59:53 +0200 Subject: [PATCH 27/40] Workaround for ARM Upgrades from RHEL8 to RHEL9.5+ Address issue with ARM system upgrades from RHEL 8 to RHEL 9.5+ caused by GRUB bootloader incompatibility with newer kernels. When attempting to load the RHEL 9.5+ kernel using the RHEL 8 bootloader, the upgrade process halts due to a boot crash. JIRA: 41193 --- repos/system_upgrade/common/libraries/grub.py | 323 ++++++++++++++++-- .../common/libraries/tests/test_grub.py | 244 ++++++++++++- repos/system_upgrade/common/models/efiinfo.py | 27 ++ .../addarmbootloaderworkaround/actor.py | 59 ++++ .../libraries/addupgradebootloader.py | 185 ++++++++++ .../tests/test_addarmbootloaderworkaround.py | 312 +++++++++++++++++ .../actors/checkarmbootloader/actor.py | 16 +- .../libraries/checkarmbootloader.py | 44 +-- .../tests/test_checkarmbootloader.py | 36 +- .../actors/removeupgradeefientry/actor.py | 26 ++ .../libraries/removeupgradeefientry.py | 100 ++++++ .../tests/test_removeupgradeefientry.py | 105 ++++++ .../el8toel9/models/upgradeefientry.py | 14 + 13 files changed, 1399 insertions(+), 92 deletions(-) create mode 100644 repos/system_upgrade/common/models/efiinfo.py create mode 100644 repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/actor.py create mode 100644 repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py create mode 100644 repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py create mode 100644 repos/system_upgrade/el8toel9/actors/removeupgradeefientry/actor.py create mode 100644 repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py create mode 100644 repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py create mode 100644 repos/system_upgrade/el8toel9/models/upgradeefientry.py diff --git a/repos/system_upgrade/common/libraries/grub.py b/repos/system_upgrade/common/libraries/grub.py index 3c80556e..cd960ea4 100644 --- a/repos/system_upgrade/common/libraries/grub.py +++ b/repos/system_upgrade/common/libraries/grub.py @@ -1,10 +1,204 @@ import os +import re from leapp.exceptions import StopActorExecution from leapp.libraries.common import mdraid from leapp.libraries.stdlib import api, CalledProcessError, run from leapp.utils.deprecation import deprecated +EFI_MOUNTPOINT = '/boot/efi/' +"""The path to the required mountpoint for ESP.""" + +GRUB2_BIOS_ENTRYPOINT = '/boot/grub2' +"""The entrypoint path of the BIOS GRUB2""" + +GRUB2_BIOS_ENV_FILE = os.path.join(GRUB2_BIOS_ENTRYPOINT, 'grubenv') +"""The path to the env file for GRUB2 in BIOS""" + + +def canonical_path_to_efi_format(canonical_path): + r"""Transform the canonical path to the UEFI format. + + e.g. /boot/efi/EFI/redhat/shimx64.efi -> \EFI\redhat\shimx64.efi + (just single backslash; so the string needs to be put into apostrophes + when used for /usr/sbin/efibootmgr cmd) + + The path has to start with /boot/efi otherwise the path is invalid for UEFI. + """ + + # We want to keep the last "/" of the EFI_MOUNTPOINT + return canonical_path.replace(EFI_MOUNTPOINT[:-1], "").replace("/", "\\") + + +class EFIBootLoaderEntry(object): + """ + Representation of an UEFI boot loader entry. + """ + # pylint: disable=eq-without-hash + + def __init__(self, boot_number, label, active, efi_bin_source): + self.boot_number = boot_number + """Expected string, e.g. '0001'. """ + + self.label = label + """Label of the UEFI entry. E.g. 'Redhat'""" + + self.active = active + """True when the UEFI entry is active (asterisk is present next to the boot number)""" + + self.efi_bin_source = efi_bin_source + """Source of the UEFI binary. + + It could contain various values, e.g.: + FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(462caa21-7614-4503-836e-8ab6f4662331) + HD(1,GPT,28c77f6b-3cd0-4b22-985f-c99903835d79,0x800,0x12c000)/File(\\EFI\\redhat\\shimx64.efi) + PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)N.....YM....R,Y. + """ + + def __eq__(self, other): + return all( + [ + self.boot_number == other.boot_number, + self.label == other.label, + self.active == other.active, + self.efi_bin_source == other.efi_bin_source, + ] + ) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'EFIBootLoaderEntry({boot_number}, {label}, {active}, {efi_bin_source})'.format( + boot_number=repr(self.boot_number), + label=repr(self.label), + active=repr(self.active), + efi_bin_source=repr(self.efi_bin_source) + ) + + def is_referring_to_file(self): + """Return True when the boot source is a file. + + Some sources could refer e.g. to PXE boot. Return true if the source + refers to a file ("ends with /File(...path...)") + + Does not matter whether the file exists or not. + """ + return '/File(\\' in self.efi_bin_source + + @staticmethod + def _efi_path_to_canonical(efi_path): + return os.path.join(EFI_MOUNTPOINT, efi_path.replace("\\", "/").lstrip("/")) + + def get_canonical_path(self): + """Return expected canonical path for the referred UEFI bin or None. + + Return None in case the entry is not referring to any UEFI bin + (e.g. when it refers to a PXE boot). + """ + if not self.is_referring_to_file(): + return None + match = re.search(r'/File\((?P\\.*)\)$', self.efi_bin_source) + return EFIBootLoaderEntry._efi_path_to_canonical(match.groups('path')[0]) + + +class EFIBootInfo(object): + """ + Data about the current UEFI boot configuration. + + Raise StopActorExecution when: + - unable to obtain info about the UEFI configuration. + - BIOS is detected. + - ESP is not mounted where expected. + """ + + def __init__(self): + if not is_efi(): + raise StopActorExecution('Unable to collect data about UEFI on a BIOS system.') + try: + result = run(['/usr/sbin/efibootmgr', '-v']) + except CalledProcessError: + raise StopActorExecution('Unable to get information about UEFI boot entries.') + + bootmgr_output = result['stdout'] + + self.current_bootnum = None + """The boot number (str) of the current boot.""" + self.next_bootnum = None + """The boot number (str) of the next boot.""" + self.boot_order = tuple() + """The tuple of the UEFI boot loader entries in the boot order.""" + self.entries = {} + """The UEFI boot loader entries {'boot_number': EFIBootLoader}""" + + self._parse_efi_boot_entries(bootmgr_output) + self._parse_current_bootnum(bootmgr_output) + self._parse_next_bootnum(bootmgr_output) + self._parse_boot_order(bootmgr_output) + self._print_loaded_info() + + def _parse_efi_boot_entries(self, bootmgr_output): + """ + Return dict of UEFI boot loader entries: {"": EFIBootLoader} + """ + + self.entries = {} + regexp_entry = re.compile( + r"^Boot(?P[a-zA-Z0-9]+)(?P\*?)\s*(?P