1757 lines
67 KiB
Diff
1757 lines
67 KiB
Diff
From abcf7a5d209d4f9fc054d39cf6866b2809fe382b Mon Sep 17 00:00:00 2001
|
|
From: David Kubek <dkubek@redhat.com>
|
|
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<path>\\.*)\)$', 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: {"<boot_number>": EFIBootLoader}
|
|
+ """
|
|
+
|
|
+ self.entries = {}
|
|
+ regexp_entry = re.compile(
|
|
+ r"^Boot(?P<bootnum>[a-zA-Z0-9]+)(?P<active>\*?)\s*(?P<label>.*?)\t(?P<bin_source>.*)$"
|
|
+ )
|
|
+
|
|
+ for line in bootmgr_output.splitlines():
|
|
+ match = regexp_entry.match(line)
|
|
+ if not match:
|
|
+ continue
|
|
+
|
|
+ self.entries[match.group('bootnum')] = EFIBootLoaderEntry(
|
|
+ boot_number=match.group('bootnum'),
|
|
+ label=match.group('label'),
|
|
+ active='*' in match.group('active'),
|
|
+ efi_bin_source=match.group('bin_source'),
|
|
+ )
|
|
+
|
|
+ if not self.entries:
|
|
+ # it's not expected that no entry exists
|
|
+ raise StopActorExecution('UEFI: Unable to detect any UEFI bootloader entry.')
|
|
+
|
|
+ def _parse_key_value(self, bootmgr_output, key):
|
|
+ # e.g.: <key>: <value>
|
|
+ for line in bootmgr_output.splitlines():
|
|
+ if line.startswith(key + ':'):
|
|
+ return line.split(':')[1].strip()
|
|
+
|
|
+ return None
|
|
+
|
|
+ def _parse_current_bootnum(self, bootmgr_output):
|
|
+ # e.g.: BootCurrent: 0002
|
|
+ self.current_bootnum = self._parse_key_value(bootmgr_output, 'BootCurrent')
|
|
+
|
|
+ if self.current_bootnum is None:
|
|
+ raise StopActorExecution('UEFI: Unable to detect current boot number.')
|
|
+
|
|
+ def _parse_next_bootnum(self, bootmgr_output):
|
|
+ # e.g.: BootNext: 0002
|
|
+ self.next_bootnum = self._parse_key_value(bootmgr_output, 'BootNext')
|
|
+
|
|
+ def _parse_boot_order(self, bootmgr_output):
|
|
+ # e.g.: BootOrder: 0001,0002,0000,0003
|
|
+ read_boot_order = self._parse_key_value(bootmgr_output, 'BootOrder')
|
|
+ self.boot_order = tuple(read_boot_order.split(','))
|
|
+
|
|
+ if self.boot_order is None:
|
|
+ raise StopActorExecution('UEFI: Unable to detect current boot order.')
|
|
+
|
|
+ def _print_loaded_info(self):
|
|
+ msg = 'Bootloader setup:'
|
|
+ msg += '\nCurrent boot: %s' % self.current_bootnum
|
|
+ msg += '\nBoot order: %s\nBoot entries:' % ', '.join(self.boot_order)
|
|
+ for bootnum, entry in self.entries.items():
|
|
+ msg += '\n- %s: %s' % (bootnum, entry.label.rstrip())
|
|
+
|
|
+ api.current_logger().debug(msg)
|
|
+
|
|
|
|
def has_grub(blk_dev):
|
|
"""
|
|
@@ -26,17 +220,88 @@ def has_grub(blk_dev):
|
|
return test in mbr
|
|
|
|
|
|
+def _get_partition(directory):
|
|
+ """
|
|
+ Get partition name of `directory`.
|
|
+ """
|
|
+
|
|
+ try:
|
|
+ result = run(['grub2-probe', '--target=device', directory])
|
|
+ except CalledProcessError:
|
|
+ msg = 'Could not get name of underlying {} partition'.format(directory)
|
|
+ api.current_logger().warning(msg)
|
|
+ raise StopActorExecution(msg)
|
|
+ except OSError:
|
|
+ msg = ('Could not get name of underlying {} partition:'
|
|
+ ' grub2-probe is missing.'
|
|
+ ' Possibly called on system that does not use GRUB2?').format(directory)
|
|
+ api.current_logger().warning(msg)
|
|
+ raise StopActorExecution(msg)
|
|
+
|
|
+ partition = result['stdout'].strip()
|
|
+ api.current_logger().info('{} is on {}'.format(directory, partition))
|
|
+
|
|
+ return partition
|
|
+
|
|
+
|
|
+def get_boot_partition():
|
|
+ """
|
|
+ Get /boot partition name.
|
|
+ """
|
|
+
|
|
+ return _get_partition('/boot')
|
|
+
|
|
+
|
|
+def is_efi():
|
|
+ """
|
|
+ Return True if UEFI is used.
|
|
+
|
|
+ NOTE(pstodulk): the check doesn't have to be valid for hybrid boot (e.g. AWS, Azure, ..)
|
|
+ """
|
|
+
|
|
+ return os.path.exists("/sys/firmware/efi")
|
|
+
|
|
+
|
|
+def get_efi_partition():
|
|
+ """
|
|
+ Return the EFI System Partition (ESP).
|
|
+
|
|
+ Raise StopActorExecution when:
|
|
+ - UEFI is not detected,
|
|
+ - ESP is not mounted where expected,
|
|
+ - the partition can't be obtained from GRUB.
|
|
+ """
|
|
+
|
|
+ if not is_efi():
|
|
+ raise StopActorExecution('Unable to get ESP when BIOS is used.')
|
|
+
|
|
+ if not os.path.exists(EFI_MOUNTPOINT) or not os.path.ismount(EFI_MOUNTPOINT):
|
|
+ raise StopActorExecution(
|
|
+ 'The UEFI has been detected but the ESP is not mounted in /boot/efi as required.'
|
|
+ )
|
|
+
|
|
+ return _get_partition('/boot/efi/')
|
|
+
|
|
+
|
|
def blk_dev_from_partition(partition):
|
|
"""
|
|
- Find parent device of /boot partition
|
|
+ Get the block device.
|
|
+
|
|
+ In case of the block device itself (e.g. /dev/sda), return just the block
|
|
+ device. In case of a partition, return its block device:
|
|
+ /dev/sda -> /dev/sda
|
|
+ /dev/sda1 -> /dev/sda
|
|
+
|
|
+ Raise CalledProcessError when unable to get the block device.
|
|
"""
|
|
+
|
|
try:
|
|
result = run(['lsblk', '-spnlo', 'name', partition])
|
|
except CalledProcessError:
|
|
- api.current_logger().warning(
|
|
- 'Could not get parent device of {} partition'.format(partition)
|
|
- )
|
|
- raise StopActorExecution() # TODO: return some meaningful value/error
|
|
+ msg = 'Could not get parent device of {} partition'.format(partition)
|
|
+ api.current_logger().warning(msg)
|
|
+ raise StopActorExecution(msg)
|
|
+
|
|
# lsblk "-s" option prints dependencies in inverse order, so the parent device will always
|
|
# be the last or the only device.
|
|
# Command result example:
|
|
@@ -44,28 +309,32 @@ def blk_dev_from_partition(partition):
|
|
return result['stdout'].strip().split()[-1]
|
|
|
|
|
|
-def get_boot_partition():
|
|
- """
|
|
- Get /boot partition name.
|
|
+def get_device_number(device):
|
|
+ """Get the partition number of a particular device.
|
|
+
|
|
+ This method will use `blkid` to determinate what is the partition number
|
|
+ related to a particular device.
|
|
+
|
|
+ :param device: The device to be analyzed.
|
|
+ :type device: str
|
|
+ :return: The device partition number.
|
|
+ :rtype: int
|
|
"""
|
|
+
|
|
try:
|
|
- # call grub2-probe to identify /boot partition
|
|
- result = run(['grub2-probe', '--target=device', '/boot'])
|
|
- except CalledProcessError:
|
|
- api.current_logger().warning(
|
|
- 'Could not get name of underlying /boot partition'
|
|
+ result = run(
|
|
+ ['/usr/sbin/blkid', '-p', '-s', 'PART_ENTRY_NUMBER', device],
|
|
)
|
|
- raise StopActorExecution() # TODO: return some meaningful value/error
|
|
- except OSError:
|
|
- api.current_logger().warning(
|
|
- 'Could not get name of underlying /boot partition:'
|
|
- ' grub2-probe is missing.'
|
|
- ' Possibly called on system that does not use GRUB2?'
|
|
- )
|
|
- raise StopActorExecution() # TODO: return some meaningful value/error
|
|
- boot_partition = result['stdout'].strip()
|
|
- api.current_logger().info('/boot is on {}'.format(boot_partition))
|
|
- return boot_partition
|
|
+ output = result['stdout'].strip()
|
|
+ except CalledProcessError:
|
|
+ raise StopActorExecution('Unable to get information about the {} device'.format(device))
|
|
+
|
|
+ if not output:
|
|
+ raise StopActorExecution('The {} device has no PART_ENTRY_NUMBER'.format(device))
|
|
+
|
|
+ partition_number = output.split('PART_ENTRY_NUMBER=')[-1].replace('"', '')
|
|
+
|
|
+ return int(partition_number)
|
|
|
|
|
|
def get_grub_devices():
|
|
@@ -94,6 +363,12 @@ def get_grub_devices():
|
|
return have_grub
|
|
|
|
|
|
+def get_efi_device():
|
|
+ """Get the block device on which GRUB is installed."""
|
|
+
|
|
+ return blk_dev_from_partition(get_efi_partition())
|
|
+
|
|
+
|
|
@deprecated(since='2023-06-23', message='This function has been replaced by get_grub_devices')
|
|
def get_grub_device():
|
|
"""
|
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_grub.py b/repos/system_upgrade/common/libraries/tests/test_grub.py
|
|
index 6f13538c..9bc9f682 100644
|
|
--- a/repos/system_upgrade/common/libraries/tests/test_grub.py
|
|
+++ b/repos/system_upgrade/common/libraries/tests/test_grub.py
|
|
@@ -9,7 +9,10 @@ from leapp.libraries.stdlib import api, CalledProcessError
|
|
from leapp.models import DefaultGrub, DefaultGrubInfo
|
|
from leapp.utils.deprecation import suppress_deprecation
|
|
|
|
-BOOT_PARTITION = '/dev/vda1'
|
|
+EFI_PARTITION = '/dev/vda1'
|
|
+EFI_DEVICE = '/dev/vda'
|
|
+
|
|
+BOOT_PARTITION = '/dev/vda2'
|
|
BOOT_DEVICE = '/dev/vda'
|
|
|
|
MD_BOOT_DEVICE = '/dev/md0'
|
|
@@ -20,6 +23,72 @@ INVALID_DD = b'Nothing to see here!'
|
|
|
|
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
+# pylint: disable=E501
|
|
+# flake8: noqa: E501
|
|
+EFIBOOTMGR_OUTPUT = r"""
|
|
+BootCurrent: 0006
|
|
+Timeout: 5 seconds
|
|
+BootOrder: 0003,0004,0001,0006,0000,0002,0007,0005
|
|
+Boot0000 redhat VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)
|
|
+Boot0001* UEFI: Built-in EFI Shell VenMedia(5023b95c-db26-429b-a648-bd47664c8012)..BO
|
|
+Boot0002 Fedora VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)
|
|
+Boot0003* UEFI: PXE IPv4 Intel(R) Network D8:5E:D3:8F:A4:E8 PcieRoot(0x40000)/Pci(0x1,0x0)/Pci(0x0,0x0)/MAC(d85ed38fa4e8,1)/IPv4(0.0.0.00.0.0.0,0,0)..BO
|
|
+Boot0004* UEFI: PXE IPv4 Intel(R) Network D8:5E:D3:8F:A4:E9 PcieRoot(0x40000)/Pci(0x1,0x0)/Pci(0x0,0x1)/MAC(d85ed38fa4e9,1)/IPv4(0.0.0.00.0.0.0,0,0)..BO
|
|
+Boot0005 centos VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)
|
|
+Boot0006* Red Hat Enterprise Linux HD(1,GPT,050609f2-0ad0-43cf-8cdf-e53132b898c9,0x800,0x12c000)/File(\EFI\REDHAT\SHIMAA64.EFI)
|
|
+Boot0007 CentOS Stream VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)
|
|
+"""
|
|
+EFIBOOTMGR_OUTPUT_ENTRIES = {
|
|
+ '0000': grub.EFIBootLoaderEntry(
|
|
+ '0000',
|
|
+ 'redhat',
|
|
+ False,
|
|
+ 'VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)'
|
|
+ ),
|
|
+ '0001': grub.EFIBootLoaderEntry(
|
|
+ '0001',
|
|
+ 'UEFI: Built-in EFI Shell',
|
|
+ True,
|
|
+ 'VenMedia(5023b95c-db26-429b-a648-bd47664c8012)..BO'
|
|
+ ),
|
|
+ '0002': grub.EFIBootLoaderEntry(
|
|
+ '0002',
|
|
+ 'Fedora',
|
|
+ False,
|
|
+ 'VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)'
|
|
+ ),
|
|
+ '0003': grub.EFIBootLoaderEntry(
|
|
+ '0003',
|
|
+ 'UEFI: PXE IPv4 Intel(R) Network D8:5E:D3:8F:A4:E8',
|
|
+ True,
|
|
+ 'PcieRoot(0x40000)/Pci(0x1,0x0)/Pci(0x0,0x0)/MAC(d85ed38fa4e8,1)/IPv4(0.0.0.00.0.0.0,0,0)..BO'
|
|
+ ),
|
|
+ '0004': grub.EFIBootLoaderEntry(
|
|
+ '0004',
|
|
+ 'UEFI: PXE IPv4 Intel(R) Network D8:5E:D3:8F:A4:E9',
|
|
+ True,
|
|
+ 'PcieRoot(0x40000)/Pci(0x1,0x0)/Pci(0x0,0x1)/MAC(d85ed38fa4e9,1)/IPv4(0.0.0.00.0.0.0,0,0)..BO'
|
|
+ ),
|
|
+ '0005': grub.EFIBootLoaderEntry(
|
|
+ '0005',
|
|
+ 'centos',
|
|
+ False,
|
|
+ 'VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)'
|
|
+ ),
|
|
+ '0006': grub.EFIBootLoaderEntry(
|
|
+ '0006',
|
|
+ 'Red Hat Enterprise Linux',
|
|
+ True,
|
|
+ 'HD(1,GPT,050609f2-0ad0-43cf-8cdf-e53132b898c9,0x800,0x12c000)/File(\\EFI\\REDHAT\\SHIMAA64.EFI)'
|
|
+ ),
|
|
+ '0007': grub.EFIBootLoaderEntry(
|
|
+ '0007',
|
|
+ 'CentOS Stream',
|
|
+ False,
|
|
+ 'VenHw(99e275e7-75a0-4b37-a2e6-c5385e6c00cb)'
|
|
+ ),
|
|
+}
|
|
+
|
|
|
|
def raise_call_error(args=None):
|
|
raise CalledProcessError(
|
|
@@ -37,24 +106,37 @@ class RunMocked(object):
|
|
self.raise_err = raise_err
|
|
self.boot_on_raid = boot_on_raid
|
|
|
|
- def __call__(self, args, encoding=None):
|
|
+ def __call__(self, args, encoding=None, checked=True):
|
|
self.called += 1
|
|
self.args = args
|
|
stdout = ''
|
|
if self.raise_err:
|
|
- raise_call_error(args)
|
|
+ if checked is True:
|
|
+ raise_call_error(args)
|
|
|
|
- if self.args == ['grub2-probe', '--target=device', '/boot']:
|
|
- stdout = MD_BOOT_DEVICE if self.boot_on_raid else BOOT_PARTITION
|
|
+ return {'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
|
|
+
|
|
+ if self.args[:-1] == ['grub2-probe', '--target=device']:
|
|
+ directory = self.args[-1]
|
|
+ if directory == '/boot':
|
|
+ stdout = MD_BOOT_DEVICE if self.boot_on_raid else BOOT_PARTITION
|
|
+ elif directory == '/boot/efi/':
|
|
+ stdout = EFI_PARTITION
|
|
+ else:
|
|
+ raise ValueError('Invalid argument {}'.format(directory))
|
|
|
|
elif self.args == ['lsblk', '-spnlo', 'name', BOOT_PARTITION]:
|
|
stdout = BOOT_DEVICE
|
|
+ elif self.args == ['lsblk', '-spnlo', 'name', EFI_PARTITION]:
|
|
+ stdout = EFI_DEVICE
|
|
elif self.args[:-1] == ['lsblk', '-spnlo', 'name']:
|
|
stdout = self.args[-1][:-1]
|
|
+ elif self.args == ['/usr/sbin/efibootmgr', '-v']:
|
|
+ stdout = EFIBOOTMGR_OUTPUT
|
|
else:
|
|
assert False, 'RunMockedError: Called unexpected cmd not covered by test: {}'.format(self.args)
|
|
|
|
- return {'stdout': stdout}
|
|
+ return {'stdout': stdout, 'exit_code': 0}
|
|
|
|
|
|
def open_mocked(fn, flags):
|
|
@@ -190,3 +272,153 @@ def test_get_grub_devices_raid_device(monkeypatch, component_devs, expected):
|
|
assert sorted(expected) == result
|
|
assert not api.current_logger.warnmsg
|
|
assert 'GRUB is installed on {}'.format(",".join(result)) in api.current_logger.infomsg
|
|
+
|
|
+
|
|
+def test_canonical_path_to_efi_format():
|
|
+ assert grub.canonical_path_to_efi_format('/boot/efi/EFI/redhat/shimx64.efi') == r'\EFI\redhat\shimx64.efi'
|
|
+
|
|
+
|
|
+def test_EFIBootLoaderEntry__efi_path_to_canonical():
|
|
+ real = grub.EFIBootLoaderEntry._efi_path_to_canonical(r'\EFI\redhat\shimx64.efi')
|
|
+ expected = '/boot/efi/EFI/redhat/shimx64.efi'
|
|
+ assert real == expected
|
|
+
|
|
+
|
|
+def test_canonical_to_efi_to_canonical():
|
|
+ canonical = '/boot/efi/EFI/redhat/shimx64.efi'
|
|
+ efi = grub.canonical_path_to_efi_format(canonical)
|
|
+
|
|
+ assert grub.EFIBootLoaderEntry._efi_path_to_canonical(efi) == canonical
|
|
+
|
|
+
|
|
+def test_efi_path_to_canonical_to_efi():
|
|
+ efi = r'\EFI\redhat\shimx64.efi'
|
|
+ canonical = grub.EFIBootLoaderEntry._efi_path_to_canonical(efi)
|
|
+
|
|
+ assert grub.canonical_path_to_efi_format(canonical) == efi
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize(
|
|
+ 'efi_bin_source, expected',
|
|
+ [
|
|
+ ('FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(462caa21-7614-4503-836e-8ab6f4662331) ', False),
|
|
+ ('PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)N.....YM....R,Y.', False),
|
|
+ ('HD(1,GPT,28c77f6b-3cd0-4b22-985f-c99903835d79,0x800,0x12c000)/File(\\EFI\\redhat\\shimx64.efi)', True),
|
|
+ ]
|
|
+)
|
|
+def test_EFIBootLoaderEntry_is_referring_to_file(efi_bin_source, expected):
|
|
+ bootloader_entry = grub.EFIBootLoaderEntry('0001', 'Redhat', False, efi_bin_source)
|
|
+ assert bootloader_entry.is_referring_to_file() is expected
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize(
|
|
+ 'efi_bin_source, expected',
|
|
+ [
|
|
+ ('FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(462caa21-7614-4503-836e-8ab6f4662331) ', None),
|
|
+ ('PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)N.....YM....R,Y.', None),
|
|
+ ('HD(1,GPT,28c77f6b-3cd0-4b22-985f-c99903835d79,0x800,0x12c000)/File(\\EFI\\redhat\\shimx64.efi)',
|
|
+ '/boot/efi/EFI/redhat/shimx64.efi'),
|
|
+ ]
|
|
+)
|
|
+def test_EFIBootLoaderEntry_get_canonical_path(efi_bin_source, expected):
|
|
+ bootloader_entry = grub.EFIBootLoaderEntry('0001', 'Redhat', False, efi_bin_source)
|
|
+ assert bootloader_entry.get_canonical_path() == expected
|
|
+
|
|
+
|
|
+def test_is_efi_success(monkeypatch):
|
|
+ def exists_mocked(path):
|
|
+ if path == '/sys/firmware/efi':
|
|
+ return True
|
|
+ raise ValueError('Unexpected path checked: {}'.format(path))
|
|
+
|
|
+ monkeypatch.setattr(os.path, 'exists', exists_mocked)
|
|
+
|
|
+ assert grub.is_efi() is True
|
|
+
|
|
+
|
|
+def test_is_efi_fail(monkeypatch):
|
|
+ def exists_mocked(path):
|
|
+ if path == '/sys/firmware/efi':
|
|
+ return False
|
|
+ raise ValueError('Unexpected path checked: {}'.format(path))
|
|
+
|
|
+ monkeypatch.setattr(os.path, 'exists', exists_mocked)
|
|
+
|
|
+ assert grub.is_efi() is False
|
|
+
|
|
+
|
|
+def test_get_efi_partition_success(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: True)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: path == '/boot/efi/')
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda path: path == '/boot/efi/')
|
|
+
|
|
+ assert grub.get_efi_partition() == EFI_PARTITION
|
|
+
|
|
+
|
|
+def test_get_efi_partition_success_fail_not_efi(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: False)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: path == '/boot/efi/')
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda path: path == '/boot/efi/')
|
|
+
|
|
+ with pytest.raises(StopActorExecution) as err:
|
|
+ grub.get_efi_partition()
|
|
+ assert 'Unable to get ESP when BIOS is used.' in err
|
|
+
|
|
+
|
|
+def test_get_efi_partition_success_fail_not_exists(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: True)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: False)
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda path: path == '/boot/efi/')
|
|
+
|
|
+ with pytest.raises(StopActorExecution) as err:
|
|
+ grub.get_efi_partition()
|
|
+ assert 'The UEFI has been detected but' in err
|
|
+
|
|
+
|
|
+def test_get_efi_partition_success_fail_not_mounted(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: True)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: path == '/boot/efi/')
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda path: False)
|
|
+
|
|
+ with pytest.raises(StopActorExecution) as err:
|
|
+ grub.get_efi_partition()
|
|
+ assert 'The UEFI has been detected but' in err
|
|
+
|
|
+
|
|
+def test_get_efi_device(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+ monkeypatch.setattr(grub, 'get_efi_partition', lambda: EFI_PARTITION)
|
|
+
|
|
+ assert grub.get_efi_device() == EFI_DEVICE
|
|
+
|
|
+
|
|
+def test_EFIBootInfo_fail_not_efi(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: False)
|
|
+
|
|
+ with pytest.raises(StopActorExecution) as err:
|
|
+ grub.EFIBootInfo()
|
|
+ assert 'Unable to collect data about UEFI on a BIOS system.' in err
|
|
+
|
|
+
|
|
+def test_EFIBootInfo_fail_efibootmgr_error(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: True)
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked(raise_err=True))
|
|
+
|
|
+ with pytest.raises(StopActorExecution) as err:
|
|
+ grub.EFIBootInfo()
|
|
+ assert 'Unable to get information about UEFI boot entries.' in err
|
|
+
|
|
+
|
|
+def test_EFIBootInfo_success(monkeypatch):
|
|
+ monkeypatch.setattr(grub, 'is_efi', lambda: True)
|
|
+ monkeypatch.setattr(grub, 'run', RunMocked())
|
|
+
|
|
+ efibootinfo = grub.EFIBootInfo()
|
|
+ assert efibootinfo.current_bootnum == '0006'
|
|
+ assert efibootinfo.next_bootnum is None
|
|
+ assert efibootinfo.boot_order == ('0003', '0004', '0001', '0006', '0000', '0002', '0007', '0005')
|
|
+ assert efibootinfo.entries == EFIBOOTMGR_OUTPUT_ENTRIES
|
|
diff --git a/repos/system_upgrade/common/models/efiinfo.py b/repos/system_upgrade/common/models/efiinfo.py
|
|
new file mode 100644
|
|
index 00000000..b989c389
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/common/models/efiinfo.py
|
|
@@ -0,0 +1,27 @@
|
|
+from leapp.models import fields, Model
|
|
+from leapp.topics import SystemInfoTopic
|
|
+
|
|
+
|
|
+class EFIBootEntry(Model):
|
|
+ """
|
|
+ Information about an UEFI boot loader entry.
|
|
+ """
|
|
+ topic = SystemInfoTopic
|
|
+
|
|
+ boot_number = fields.String()
|
|
+ """Expected string, e.g. '0001'. """
|
|
+
|
|
+ label = fields.String()
|
|
+ """Label of the UEFI entry. E.g. 'Redhat'"""
|
|
+
|
|
+ active = fields.Boolean()
|
|
+ """True when the UEFI entry is active (asterisk is present next to the boot number)"""
|
|
+
|
|
+ efi_bin_source = fields.String()
|
|
+ """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.
|
|
+ """
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/actor.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/actor.py
|
|
new file mode 100644
|
|
index 00000000..54e8aca8
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/actor.py
|
|
@@ -0,0 +1,59 @@
|
|
+from leapp.actors import Actor
|
|
+from leapp.libraries.actor import addupgradebootloader
|
|
+from leapp.libraries.common.config import architecture
|
|
+from leapp.libraries.common.config.version import matches_target_version
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo, TargetUserSpaceInfo
|
|
+from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag
|
|
+
|
|
+
|
|
+class AddArmBootloaderWorkaround(Actor):
|
|
+ """
|
|
+ Workaround for ARM Upgrades from RHEL8 to RHEL9.5 onwards
|
|
+
|
|
+ This actor addresses an issue encountered during the upgrade process on ARM
|
|
+ systems. Specifically, the problem arises due to an incompatibility between
|
|
+ the GRUB bootloader used in RHEL 8 and the newer kernels from RHEL 9.5
|
|
+ onwards. When the kernel of the target system is loaded using the
|
|
+ bootloader from the source system, this incompatibility causes the
|
|
+ bootloader to crash, halting the upgrade.
|
|
+
|
|
+ To mitigate this issue, the following steps are implemented:
|
|
+
|
|
+ Before the Upgrade (handled by CheckArmBootloader):
|
|
+
|
|
+ * Install required RPM packages:
|
|
+ - Specific packages, including an updated GRUB bootloader and shim for
|
|
+ ARM (grub2-efi-aa64 and shim-aa64), are installed to ensure
|
|
+ compatibility with the newer kernel.
|
|
+
|
|
+ Before the Upgrade (this actor):
|
|
+
|
|
+ * Create a new Upgrade EFI entry:
|
|
+ - A new EFI boot entry is created and populated with the updated RHEL 9
|
|
+ bootloader that is compatible with the new kernel.
|
|
+
|
|
+ * Preserve the original EFI boot entry and GRUB configuration:
|
|
+ - The original EFI boot entry and GRUB configuration remain unchanged to
|
|
+ ensure system stability.
|
|
+
|
|
+ After the Upgrade (handled by RemoveUpgradeEFIEntry):
|
|
+
|
|
+ * Remove the upgrade EFI boot entry:
|
|
+ - The temporary EFI boot entry created for the upgrade is removed to
|
|
+ restore the system to its pre-upgrade state.
|
|
+
|
|
+ """
|
|
+
|
|
+ name = 'add_arm_bootloader_workaround'
|
|
+ consumes = (TargetUserSpaceInfo,)
|
|
+ produces = (ArmWorkaroundEFIBootloaderInfo,)
|
|
+ tags = (IPUWorkflowTag, InterimPreparationPhaseTag)
|
|
+
|
|
+ def process(self):
|
|
+ if not architecture.matches_architecture(architecture.ARCH_ARM64):
|
|
+ return
|
|
+
|
|
+ if matches_target_version('< 9.5'):
|
|
+ return
|
|
+
|
|
+ addupgradebootloader.process()
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py
|
|
new file mode 100644
|
|
index 00000000..5e9bf5c6
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py
|
|
@@ -0,0 +1,185 @@
|
|
+import os
|
|
+import shutil
|
|
+
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.common import mounting
|
|
+from leapp.libraries.common.grub import (
|
|
+ canonical_path_to_efi_format,
|
|
+ EFIBootInfo,
|
|
+ get_device_number,
|
|
+ get_efi_device,
|
|
+ get_efi_partition,
|
|
+ GRUB2_BIOS_ENTRYPOINT,
|
|
+ GRUB2_BIOS_ENV_FILE
|
|
+)
|
|
+from leapp.libraries.stdlib import api, CalledProcessError, run
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo, EFIBootEntry, TargetUserSpaceInfo
|
|
+
|
|
+UPGRADE_EFI_ENTRY_LABEL = 'Leapp Upgrade'
|
|
+
|
|
+ARM_SHIM_PACKAGE_NAME = 'shim-aa64'
|
|
+ARM_GRUB_PACKAGE_NAME = 'grub2-efi-aa64'
|
|
+
|
|
+EFI_MOUNTPOINT = '/boot/efi/'
|
|
+LEAPP_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/leapp/')
|
|
+RHEL_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/redhat/')
|
|
+
|
|
+CONTAINER_DOWNLOAD_DIR = '/tmp_pkg_download_dir'
|
|
+
|
|
+
|
|
+def _copy_file(src_path, dst_path):
|
|
+ if os.path.exists(dst_path):
|
|
+ api.current_logger().debug("The {} file already exists and its content will be overwritten.".format(dst_path))
|
|
+
|
|
+ api.current_logger().info("Copying {} to {}".format(src_path, dst_path))
|
|
+ try:
|
|
+ shutil.copy2(src_path, dst_path)
|
|
+ except (OSError, IOError) as err:
|
|
+ raise StopActorExecutionError('I/O error({}): {}'.format(err.errno, err.strerror))
|
|
+
|
|
+
|
|
+def process():
|
|
+ userspace = _get_userspace_info()
|
|
+
|
|
+ with mounting.NspawnActions(base_dir=userspace.path) as context:
|
|
+ _ensure_clean_environment()
|
|
+
|
|
+ # NOTE(dkubek): Assumes required shim-aa64 and grub2-efi-aa64 packages
|
|
+ # have been installed
|
|
+ context.copytree_from(RHEL_EFIDIR_CANONICAL_PATH, LEAPP_EFIDIR_CANONICAL_PATH)
|
|
+
|
|
+ _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg'])
|
|
+ _link_grubenv_to_upgrade_entry()
|
|
+
|
|
+ efibootinfo = EFIBootInfo()
|
|
+ current_boot_entry = efibootinfo.entries[efibootinfo.current_bootnum]
|
|
+ upgrade_boot_entry = _add_upgrade_boot_entry(efibootinfo)
|
|
+ _set_bootnext(upgrade_boot_entry.boot_number)
|
|
+
|
|
+ efibootentry_fields = ['boot_number', 'label', 'active', 'efi_bin_source']
|
|
+ api.produce(
|
|
+ ArmWorkaroundEFIBootloaderInfo(
|
|
+ original_entry=EFIBootEntry(**{f: getattr(current_boot_entry, f) for f in efibootentry_fields}),
|
|
+ upgrade_entry=EFIBootEntry(**{f: getattr(upgrade_boot_entry, f) for f in efibootentry_fields}),
|
|
+ )
|
|
+ )
|
|
+
|
|
+
|
|
+def _get_userspace_info():
|
|
+ msgs = api.consume(TargetUserSpaceInfo)
|
|
+
|
|
+ userspace = next(msgs, None)
|
|
+ if userspace is None:
|
|
+ raise StopActorExecutionError('Could not retrieve TargetUserSpaceInfo!')
|
|
+
|
|
+ if next(msgs, None):
|
|
+ api.current_logger().warning('Unexpectedly received more than one TargetUserSpaceInfo message.')
|
|
+
|
|
+ return userspace
|
|
+
|
|
+
|
|
+def _ensure_clean_environment():
|
|
+ if os.path.exists(LEAPP_EFIDIR_CANONICAL_PATH):
|
|
+ shutil.rmtree(LEAPP_EFIDIR_CANONICAL_PATH)
|
|
+
|
|
+
|
|
+def _copy_grub_files(required, optional):
|
|
+ """
|
|
+ Copy grub files from redhat/ dir to the /boot/efi/EFI/leapp/ dir.
|
|
+ """
|
|
+
|
|
+ all_files = required + optional
|
|
+ for filename in all_files:
|
|
+ src_path = os.path.join(RHEL_EFIDIR_CANONICAL_PATH, filename)
|
|
+ dst_path = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, filename)
|
|
+
|
|
+ if not os.path.exists(src_path):
|
|
+ if filename in required:
|
|
+ msg = 'Required file {} does not exists. Aborting.'.format(filename)
|
|
+ raise StopActorExecutionError(msg)
|
|
+
|
|
+ continue
|
|
+
|
|
+ _copy_file(src_path, dst_path)
|
|
+
|
|
+
|
|
+def _link_grubenv_to_upgrade_entry():
|
|
+ upgrade_env_file = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv')
|
|
+ upgrade_env_file_relpath = os.path.relpath(upgrade_env_file, GRUB2_BIOS_ENTRYPOINT)
|
|
+ run(['ln', '--symbolic', '--force', upgrade_env_file_relpath, GRUB2_BIOS_ENV_FILE])
|
|
+
|
|
+
|
|
+def _add_upgrade_boot_entry(efibootinfo):
|
|
+ """
|
|
+ Create a new UEFI bootloader entry with a upgrade label and bin file.
|
|
+
|
|
+ If an entry for the label and bin file already exists no new entry
|
|
+ will be created.
|
|
+
|
|
+ Return the upgrade efi entry (EFIEntry).
|
|
+ """
|
|
+
|
|
+ dev_number = get_device_number(get_efi_partition())
|
|
+ blk_dev = get_efi_device()
|
|
+
|
|
+ tmp_efi_path = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, 'shimaa64.efi')
|
|
+ if os.path.exists(tmp_efi_path):
|
|
+ efi_path = canonical_path_to_efi_format(tmp_efi_path)
|
|
+ else:
|
|
+ raise StopActorExecutionError('Unable to detect upgrade UEFI binary file.')
|
|
+
|
|
+ upgrade_boot_entry = _get_upgrade_boot_entry(efibootinfo, efi_path, UPGRADE_EFI_ENTRY_LABEL)
|
|
+ if upgrade_boot_entry is not None:
|
|
+ return upgrade_boot_entry
|
|
+
|
|
+ cmd = [
|
|
+ "/usr/sbin/efibootmgr",
|
|
+ "--create",
|
|
+ "--disk",
|
|
+ blk_dev,
|
|
+ "--part",
|
|
+ str(dev_number),
|
|
+ "--loader",
|
|
+ efi_path,
|
|
+ "--label",
|
|
+ UPGRADE_EFI_ENTRY_LABEL,
|
|
+ ]
|
|
+
|
|
+ try:
|
|
+ run(cmd)
|
|
+ except CalledProcessError:
|
|
+ raise StopActorExecutionError('Unable to add a new UEFI bootloader entry for upgrade.')
|
|
+
|
|
+ # Sanity check new boot entry has been added
|
|
+ efibootinfo_new = EFIBootInfo()
|
|
+ upgrade_boot_entry = _get_upgrade_boot_entry(efibootinfo_new, efi_path, UPGRADE_EFI_ENTRY_LABEL)
|
|
+ if upgrade_boot_entry is None:
|
|
+ raise StopActorExecutionError('Unable to find the new UEFI bootloader entry after adding it.')
|
|
+
|
|
+ return upgrade_boot_entry
|
|
+
|
|
+
|
|
+def _get_upgrade_boot_entry(efibootinfo, efi_path, label):
|
|
+ """
|
|
+ Get the UEFI boot entry with label `label` and EFI binary path `efi_path`
|
|
+
|
|
+ Return EFIBootEntry or None if not found.
|
|
+ """
|
|
+
|
|
+ for entry in efibootinfo.entries.values():
|
|
+ if entry.label == label and efi_path in entry.efi_bin_source:
|
|
+ return entry
|
|
+
|
|
+ return None
|
|
+
|
|
+
|
|
+def _set_bootnext(boot_number):
|
|
+ """
|
|
+ Set the BootNext UEFI entry to `boot_number`.
|
|
+ """
|
|
+
|
|
+ api.current_logger().debug('Setting {} as BootNext'.format(boot_number))
|
|
+ try:
|
|
+ run(['/usr/sbin/efibootmgr', '--bootnext', boot_number])
|
|
+ except CalledProcessError:
|
|
+ raise StopActorExecutionError('Could not set boot entry {} as BootNext.'.format(boot_number))
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py
|
|
new file mode 100644
|
|
index 00000000..d2015272
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py
|
|
@@ -0,0 +1,312 @@
|
|
+import os
|
|
+
|
|
+import pytest
|
|
+
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.actor import addupgradebootloader
|
|
+from leapp.libraries.common.grub import EFIBootLoaderEntry
|
|
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, make_OSError, produce_mocked
|
|
+from leapp.libraries.stdlib import api, CalledProcessError
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo, EFIBootEntry, TargetUserSpaceInfo
|
|
+
|
|
+TEST_RHEL_EFI_ENTRY = EFIBootLoaderEntry(
|
|
+ '0000',
|
|
+ 'Red Hat Enterprise Linux',
|
|
+ True,
|
|
+ 'File(\\EFI\\redhat\\shimaa64.efi)'
|
|
+ )
|
|
+TEST_UPGRADE_EFI_ENTRY = EFIBootLoaderEntry(
|
|
+ '0001',
|
|
+ addupgradebootloader.UPGRADE_EFI_ENTRY_LABEL,
|
|
+ True,
|
|
+ 'File(\\EFI\\leapp\\shimaa64.efi)'
|
|
+ )
|
|
+
|
|
+
|
|
+class MockEFIBootInfo:
|
|
+ def __init__(self, entries):
|
|
+ assert len(entries) > 0
|
|
+
|
|
+ self.boot_order = tuple(entry.boot_number for entry in entries)
|
|
+ self.current_bootnum = self.boot_order[0]
|
|
+ self.next_bootnum = None
|
|
+ self.entries = {
|
|
+ entry.boot_number: entry for entry in entries
|
|
+ }
|
|
+
|
|
+
|
|
+class IsolatedActionsMocked(object):
|
|
+ def __init__(self):
|
|
+ self.copytree_from_calls = []
|
|
+
|
|
+ def __enter__(self):
|
|
+ return self
|
|
+
|
|
+ def __exit__(self, *args):
|
|
+ pass
|
|
+
|
|
+ def copytree_from(self, src, dst):
|
|
+ self.copytree_from_calls.append((src, dst))
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize('dst_exists', [True, False])
|
|
+def test_copy_file(monkeypatch, dst_exists):
|
|
+ src_path = '/src/file.txt'
|
|
+ dst_path = '/dst/file.txt'
|
|
+ logger = logger_mocked()
|
|
+
|
|
+ copy2_calls = []
|
|
+
|
|
+ def mock_copy2(src, dst):
|
|
+ copy2_calls.append((src, dst))
|
|
+
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: dst_exists)
|
|
+ monkeypatch.setattr('shutil.copy2', mock_copy2)
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ addupgradebootloader._copy_file(src_path, dst_path)
|
|
+
|
|
+ assert copy2_calls == [(src_path, dst_path)]
|
|
+ if dst_exists:
|
|
+ assert 'The {} file already exists and its content will be overwritten.'.format(dst_path) in logger.dbgmsg
|
|
+
|
|
+ assert 'Copying {} to {}'.format(src_path, dst_path) in logger.infomsg
|
|
+
|
|
+
|
|
+def test_copy_file_error(monkeypatch):
|
|
+ src_path = '/src/file.txt'
|
|
+ dst_path = '/dst/file.txt'
|
|
+ logger = logger_mocked()
|
|
+
|
|
+ def mock_copy2_fail(src, dst):
|
|
+ raise make_OSError(5)
|
|
+
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: False)
|
|
+ monkeypatch.setattr('shutil.copy2', mock_copy2_fail)
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ with pytest.raises(StopActorExecutionError, match=r'I/O error\(5\)'):
|
|
+ addupgradebootloader._copy_file(src_path, dst_path)
|
|
+
|
|
+
|
|
+def test_get_userspace_info(monkeypatch):
|
|
+ target_info_mock = TargetUserSpaceInfo(path='/USERSPACE', scratch='/SCRATCH', mounts='/MOUNTS')
|
|
+
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[target_info_mock]))
|
|
+
|
|
+ result = addupgradebootloader._get_userspace_info()
|
|
+ assert result == target_info_mock
|
|
+
|
|
+
|
|
+def test_get_userspace_info_none(monkeypatch):
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
|
|
+
|
|
+ with pytest.raises(StopActorExecutionError, match='Could not retrieve TargetUserSpaceInfo'):
|
|
+ addupgradebootloader._get_userspace_info()
|
|
+
|
|
+
|
|
+def test_get_userspace_info_multiple(monkeypatch):
|
|
+ logger = logger_mocked()
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[
|
|
+ TargetUserSpaceInfo(path='/USERSPACE1', scratch='/SCRATCH1', mounts='/MOUNTS1'),
|
|
+ TargetUserSpaceInfo(path='/USERSPACE2', scratch='/SCRATCH2', mounts='/MOUNTS2'),
|
|
+ ]))
|
|
+
|
|
+ addupgradebootloader._get_userspace_info()
|
|
+
|
|
+ assert 'Unexpectedly received more than one TargetUserSpaceInfo message.' in logger.warnmsg
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize('exists', [True, False])
|
|
+def test_ensure_clean_environment(monkeypatch, exists):
|
|
+ rmtree_calls = []
|
|
+
|
|
+ monkeypatch.setattr('os.path.exists', lambda path: exists)
|
|
+ monkeypatch.setattr('shutil.rmtree', rmtree_calls.append)
|
|
+
|
|
+ addupgradebootloader._ensure_clean_environment()
|
|
+
|
|
+ assert rmtree_calls == ([addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH] if exists else [])
|
|
+
|
|
+
|
|
+def test_copy_grub_files(monkeypatch):
|
|
+ copy_file_calls = []
|
|
+
|
|
+ def mock_copy_file(src, dst):
|
|
+ copy_file_calls.append((src, dst))
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, '_copy_file', mock_copy_file)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ addupgradebootloader._copy_grub_files(['required'], ['optional'])
|
|
+
|
|
+ assert (
|
|
+ os.path.join(addupgradebootloader.RHEL_EFIDIR_CANONICAL_PATH, 'required'),
|
|
+ os.path.join(addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH, 'required')
|
|
+ ) in copy_file_calls
|
|
+ assert (
|
|
+ os.path.join(addupgradebootloader.RHEL_EFIDIR_CANONICAL_PATH, 'optional'),
|
|
+ os.path.join(addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH, 'optional')
|
|
+ ) in copy_file_calls
|
|
+
|
|
+
|
|
+def test_set_bootnext(monkeypatch):
|
|
+ run_calls = []
|
|
+ logger = logger_mocked()
|
|
+
|
|
+ def mock_run(command):
|
|
+ run_calls.append(command)
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ addupgradebootloader._set_bootnext('0000')
|
|
+
|
|
+ assert run_calls == [['/usr/sbin/efibootmgr', '--bootnext', '0000']]
|
|
+ assert logger.dbgmsg == ['Setting {} as BootNext'.format('0000')]
|
|
+
|
|
+
|
|
+def test_add_upgrade_boot_entry_no_efi_binary(monkeypatch):
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_partition', lambda: '/dev/sda1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_device_number', lambda device: '1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_device', lambda: '/dev/sda')
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: False)
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY])
|
|
+ with pytest.raises(StopActorExecutionError, match="Unable to detect upgrade UEFI binary file"):
|
|
+ addupgradebootloader._add_upgrade_boot_entry(efibootinfo_mock)
|
|
+
|
|
+
|
|
+def test_add_upgrade_already_exists(monkeypatch):
|
|
+ run_calls = []
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_partition', lambda: '/dev/sda1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_device_number', lambda device: '1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_device', lambda: '/dev/sda')
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ def mock_run(cmd):
|
|
+ run_calls.append(cmd)
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY, TEST_UPGRADE_EFI_ENTRY])
|
|
+ result = addupgradebootloader._add_upgrade_boot_entry(efibootinfo_mock)
|
|
+
|
|
+ assert result == TEST_UPGRADE_EFI_ENTRY
|
|
+ assert len(run_calls) == 0
|
|
+
|
|
+
|
|
+def test_add_upgrade_boot_entry_command_failure(monkeypatch):
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_partition', lambda: '/dev/sda1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_device_number', lambda device: '1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_device', lambda: '/dev/sda')
|
|
+ monkeypatch.setattr(addupgradebootloader, '_get_upgrade_boot_entry', lambda efi, path, label: None)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ def mock_run(cmd):
|
|
+ raise CalledProcessError(
|
|
+ message='A Leapp Command Error occurred.',
|
|
+ command=cmd,
|
|
+ result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
|
|
+ )
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY])
|
|
+ with pytest.raises(StopActorExecutionError, match="Unable to add a new UEFI bootloader entry"):
|
|
+ addupgradebootloader._add_upgrade_boot_entry(efibootinfo_mock)
|
|
+
|
|
+
|
|
+def test_add_upgrade_boot_entry_verification_failure(monkeypatch):
|
|
+ run_calls = []
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_partition', lambda: '/dev/sda1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_device_number', lambda device: '1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_device', lambda: '/dev/sda')
|
|
+ monkeypatch.setattr(addupgradebootloader, '_get_upgrade_boot_entry', lambda efi, path, label: None)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ def mock_run(cmd):
|
|
+ run_calls.append(cmd)
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+ monkeypatch.setattr(addupgradebootloader, 'EFIBootInfo', lambda: MockEFIBootInfo([TEST_RHEL_EFI_ENTRY]))
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY])
|
|
+ with pytest.raises(StopActorExecutionError, match="Unable to find the new UEFI bootloader entry after adding it"):
|
|
+ addupgradebootloader._add_upgrade_boot_entry(efibootinfo_mock)
|
|
+
|
|
+
|
|
+def test_add_upgrade_boot_entry_success(monkeypatch):
|
|
+ run_calls = []
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_partition', lambda: '/dev/sda1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_device_number', lambda device: '1')
|
|
+ monkeypatch.setattr(addupgradebootloader, 'get_efi_device', lambda: '/dev/sda')
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ def mock_run(cmd):
|
|
+ run_calls.append(cmd)
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+ monkeypatch.setattr(
|
|
+ addupgradebootloader,
|
|
+ 'EFIBootInfo',
|
|
+ lambda: MockEFIBootInfo([TEST_RHEL_EFI_ENTRY, TEST_UPGRADE_EFI_ENTRY])
|
|
+ )
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY])
|
|
+ result = addupgradebootloader._add_upgrade_boot_entry(efibootinfo_mock)
|
|
+
|
|
+ assert [
|
|
+ '/usr/sbin/efibootmgr',
|
|
+ '--create',
|
|
+ '--disk', '/dev/sda',
|
|
+ '--part', '1',
|
|
+ '--loader', '\\EFI\\leapp\\shimaa64.efi',
|
|
+ '--label', 'Leapp Upgrade',
|
|
+ ] in run_calls
|
|
+ assert result.label == addupgradebootloader.UPGRADE_EFI_ENTRY_LABEL
|
|
+
|
|
+
|
|
+def test_process(monkeypatch):
|
|
+ run_calls = []
|
|
+
|
|
+ def mock_run(cmd):
|
|
+ run_calls.append(cmd)
|
|
+
|
|
+ target_info_mock = TargetUserSpaceInfo(path='/USERSPACE', scratch='/SCRATCH', mounts='/MOUNTS')
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[target_info_mock]))
|
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
|
+ monkeypatch.setattr(addupgradebootloader, 'run', mock_run)
|
|
+
|
|
+ context_mock = IsolatedActionsMocked()
|
|
+ monkeypatch.setattr(addupgradebootloader.mounting, 'NspawnActions', lambda *args, **kwargs: context_mock)
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, '_copy_grub_files', lambda optional, required: None)
|
|
+ monkeypatch.setattr(addupgradebootloader, '_link_grubenv_to_upgrade_entry', lambda: None)
|
|
+
|
|
+ efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY])
|
|
+ monkeypatch.setattr(addupgradebootloader, 'EFIBootInfo', lambda: efibootinfo_mock)
|
|
+
|
|
+ def mock_add_upgrade_boot_entry(efibootinfo):
|
|
+ return TEST_UPGRADE_EFI_ENTRY
|
|
+
|
|
+ monkeypatch.setattr(addupgradebootloader, '_add_upgrade_boot_entry', mock_add_upgrade_boot_entry)
|
|
+ monkeypatch.setattr(addupgradebootloader, '_set_bootnext', lambda _: None)
|
|
+
|
|
+ addupgradebootloader.process()
|
|
+
|
|
+ assert api.produce.called == 1
|
|
+ assert len(api.produce.model_instances) == 1
|
|
+
|
|
+ efibootentry_fields = ['boot_number', 'label', 'active', 'efi_bin_source']
|
|
+ expected = ArmWorkaroundEFIBootloaderInfo(
|
|
+ original_entry=EFIBootEntry(**{f: getattr(TEST_RHEL_EFI_ENTRY, f) for f in efibootentry_fields}),
|
|
+ upgrade_entry=EFIBootEntry(**{f: getattr(TEST_UPGRADE_EFI_ENTRY, f) for f in efibootentry_fields}),
|
|
+ )
|
|
+ actual = api.produce.model_instances[0]
|
|
+ assert actual == expected
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/actor.py b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/actor.py
|
|
index b4938ced..d47ca8ca 100644
|
|
--- a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/actor.py
|
|
+++ b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/actor.py
|
|
@@ -1,24 +1,24 @@
|
|
import leapp.libraries.actor.checkarmbootloader as checkarmbootloader
|
|
from leapp.actors import Actor
|
|
-from leapp.reporting import Report
|
|
+from leapp.models import TargetUserSpacePreupgradeTasks
|
|
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
|
|
|
|
|
class CheckArmBootloader(Actor):
|
|
"""
|
|
- Inhibit ARM system upgrades on path with incompatible kernel/bootloader
|
|
+ Install required RPM packages for ARM system upgrades on paths with
|
|
+ incompatible kernel/bootloader.
|
|
|
|
- Due to an incompatibility of RHEL8 bootloader with newer versions of kernel
|
|
- on RHEL9 since version 9.5, the upgrade cannot be performed as the old
|
|
- bootloader cannot load the new kernel when entering the interim phase.
|
|
-
|
|
- This is temporary workaround until the issue is resolved.
|
|
+ Due to an incompatibility of the RHEL8 bootloader with newer versions of
|
|
+ the kernel on RHEL9 (from version 9.5 onward), the upgrade requires the
|
|
+ installation of specific packages to support the new kernel during the
|
|
+ interim phase.
|
|
|
|
"""
|
|
|
|
name = 'check_arm_bootloader'
|
|
consumes = ()
|
|
- produces = (Report,)
|
|
+ produces = (TargetUserSpacePreupgradeTasks,)
|
|
tags = (ChecksPhaseTag, IPUWorkflowTag,)
|
|
|
|
def process(self):
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/libraries/checkarmbootloader.py b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/libraries/checkarmbootloader.py
|
|
index a5fdffe9..91950be2 100644
|
|
--- a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/libraries/checkarmbootloader.py
|
|
+++ b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/libraries/checkarmbootloader.py
|
|
@@ -1,44 +1,16 @@
|
|
-from leapp import reporting
|
|
from leapp.libraries.common.config.architecture import ARCH_ARM64, matches_architecture
|
|
from leapp.libraries.common.config.version import get_source_version, get_target_version, matches_target_version
|
|
from leapp.libraries.stdlib import api
|
|
+from leapp.models import TargetUserSpacePreupgradeTasks
|
|
|
|
-
|
|
-def _inhibit_upgrade():
|
|
- title = 'Upgrade RHEL {} to RHEL {} not possible for ARM machines.'.format(
|
|
- get_source_version(), get_target_version())
|
|
- summary = (
|
|
- 'Due to the incompatibility of the RHEL 8 bootloader with a newer version of kernel on RHEL {}'
|
|
- ' for ARM machines, the direct upgrade cannot be performed to this RHEL'
|
|
- ' system version now. The fix is not expected to be delivered during the RHEL 9.5 lifetime.'
|
|
- .format(get_target_version())
|
|
- )
|
|
-
|
|
- reporting.create_report([
|
|
- reporting.Title(title),
|
|
- reporting.Summary(summary),
|
|
- reporting.ExternalLink(
|
|
- title='Known issues for the RHEL 8.10 to RHEL 9.5 upgrade',
|
|
- url='https://red.ht/upgrading-rhel8-to-rhel9-known-issues'),
|
|
- reporting.Severity(reporting.Severity.HIGH),
|
|
- reporting.Groups([reporting.Groups.INHIBITOR]),
|
|
- reporting.Groups([reporting.Groups.SANITY]),
|
|
- reporting.Remediation(hint=(
|
|
- 'To upgrade to the RHEL {} version, first in-place upgrade to RHEL 9.4 instead'
|
|
- ' using the leapp `--target=9.4` option. After you finish the upgrade - including'
|
|
- ' all required manual post-upgrade steps as well -'
|
|
- ' update to the newer minor version using the dnf tool. In case of using Red Hat'
|
|
- ' subscription-manager, do not forget to change the lock version to the newer one'
|
|
- ' or unset the version lock before using DNF to be able to perform the minor version update.'
|
|
- ' You can use e.g. `subscription-manager release --unset` command for that.'
|
|
- .format(get_target_version())
|
|
- )),
|
|
- ])
|
|
+ARM_SHIM_PACKAGE_NAME = 'shim-aa64'
|
|
+ARM_GRUB_PACKAGE_NAME = 'grub2-efi-aa64'
|
|
|
|
|
|
def process():
|
|
"""
|
|
- Check whether the upgrade path will use a target kernel compatible with the source bootloader on ARM systems
|
|
+ Check whether the upgrade path will use a target kernel compatible with the
|
|
+ source bootloader on ARM systems. Prepare for a workaround otherwise.
|
|
"""
|
|
|
|
if not matches_architecture(ARCH_ARM64):
|
|
@@ -51,4 +23,8 @@ def process():
|
|
'Skipping bootloader check.').format(get_source_version(), get_target_version()))
|
|
return
|
|
|
|
- _inhibit_upgrade()
|
|
+ api.produce(
|
|
+ TargetUserSpacePreupgradeTasks(
|
|
+ install_rpms=[ARM_GRUB_PACKAGE_NAME, ARM_SHIM_PACKAGE_NAME]
|
|
+ )
|
|
+ )
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/tests/test_checkarmbootloader.py b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/tests/test_checkarmbootloader.py
|
|
index 97c01e77..f38e4afb 100644
|
|
--- a/repos/system_upgrade/el8toel9/actors/checkarmbootloader/tests/test_checkarmbootloader.py
|
|
+++ b/repos/system_upgrade/el8toel9/actors/checkarmbootloader/tests/test_checkarmbootloader.py
|
|
@@ -1,37 +1,35 @@
|
|
import pytest
|
|
|
|
-from leapp import reporting
|
|
from leapp.libraries.actor import checkarmbootloader
|
|
from leapp.libraries.common.config.architecture import ARCH_ARM64, ARCH_SUPPORTED
|
|
-from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked
|
|
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
|
|
from leapp.libraries.stdlib import api
|
|
-from leapp.utils.report import is_inhibitor
|
|
|
|
|
|
-@pytest.mark.parametrize("arch", [arch for arch in ARCH_SUPPORTED if not arch == ARCH_ARM64])
|
|
+@pytest.mark.parametrize('arch', [arch for arch in ARCH_SUPPORTED if not arch == ARCH_ARM64])
|
|
def test_not_x86_64_passes(monkeypatch, arch):
|
|
"""
|
|
- Test no report is generated on an architecture different from ARM
|
|
+ Test no message is generated on an architecture different from ARM
|
|
"""
|
|
|
|
- monkeypatch.setattr(reporting, "create_report", create_report_mocked())
|
|
monkeypatch.setattr(api, 'current_logger', logger_mocked())
|
|
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch))
|
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
|
|
|
checkarmbootloader.process()
|
|
|
|
assert 'Architecture not ARM.' in api.current_logger.infomsg[0]
|
|
- assert not reporting.create_report.called
|
|
+ assert not api.produce.called
|
|
|
|
|
|
-@pytest.mark.parametrize("target_version", ["9.2", "9.4"])
|
|
+@pytest.mark.parametrize('target_version', ['9.2', '9.4'])
|
|
def test_valid_path(monkeypatch, target_version):
|
|
"""
|
|
- Test no report is generated on a supported path
|
|
+ Test no message is generated on a supported path
|
|
"""
|
|
|
|
- monkeypatch.setattr(reporting, "create_report", create_report_mocked())
|
|
monkeypatch.setattr(api, 'current_logger', logger_mocked())
|
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
|
monkeypatch.setattr(
|
|
api, 'current_actor',
|
|
CurrentActorMocked(arch=ARCH_ARM64, src_ver='8.10', dst_ver=target_version)
|
|
@@ -40,16 +38,16 @@ def test_valid_path(monkeypatch, target_version):
|
|
checkarmbootloader.process()
|
|
|
|
assert 'Upgrade on ARM architecture on a compatible path' in api.current_logger.infomsg[0]
|
|
- assert not reporting.create_report.called
|
|
+ assert not api.produce.called
|
|
|
|
|
|
def test_invalid_path(monkeypatch):
|
|
"""
|
|
- Test report is generated on a invalid upgrade path
|
|
+ Test message is generated on an invalid upgrade path
|
|
"""
|
|
|
|
- monkeypatch.setattr(reporting, "create_report", create_report_mocked())
|
|
monkeypatch.setattr(api, 'current_logger', logger_mocked())
|
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
|
monkeypatch.setattr(
|
|
api, 'current_actor',
|
|
CurrentActorMocked(arch=ARCH_ARM64, src_ver='8.10', dst_ver='9.5')
|
|
@@ -57,11 +55,9 @@ def test_invalid_path(monkeypatch):
|
|
|
|
checkarmbootloader.process()
|
|
|
|
- produced_title = reporting.create_report.report_fields.get('title')
|
|
- produced_summary = reporting.create_report.report_fields.get('summary')
|
|
+ assert api.produce.called == 1
|
|
+ assert len(api.produce.model_instances) == 1
|
|
|
|
- assert reporting.create_report.called == 1
|
|
- assert 'not possible for ARM machines' in produced_title
|
|
- assert 'Due to the incompatibility' in produced_summary
|
|
- assert reporting.create_report.report_fields['severity'] == reporting.Severity.HIGH
|
|
- assert is_inhibitor(reporting.create_report.report_fields)
|
|
+ msg = api.produce.model_instances[0]
|
|
+ assert checkarmbootloader.ARM_GRUB_PACKAGE_NAME in msg.install_rpms
|
|
+ assert checkarmbootloader.ARM_SHIM_PACKAGE_NAME in msg.install_rpms
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/actor.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/actor.py
|
|
new file mode 100644
|
|
index 00000000..cccfd515
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/actor.py
|
|
@@ -0,0 +1,26 @@
|
|
+from leapp.actors import Actor
|
|
+from leapp.libraries.actor.removeupgradeefientry import remove_upgrade_efi_entry
|
|
+from leapp.libraries.common.config import architecture
|
|
+from leapp.libraries.common.config.version import matches_target_version
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo
|
|
+from leapp.tags import InitRamStartPhaseTag, IPUWorkflowTag
|
|
+
|
|
+
|
|
+class RemoveUpgradeEFIEntry(Actor):
|
|
+ """
|
|
+ Remove UEFI entry for LEAPP upgrade (see AddArmBootloaderWorkaround).
|
|
+ """
|
|
+
|
|
+ name = 'remove_upgrade_efi_entry'
|
|
+ consumes = (ArmWorkaroundEFIBootloaderInfo,)
|
|
+ produces = ()
|
|
+ tags = (IPUWorkflowTag, InitRamStartPhaseTag)
|
|
+
|
|
+ def process(self):
|
|
+ if not architecture.matches_architecture(architecture.ARCH_ARM64):
|
|
+ return
|
|
+
|
|
+ if matches_target_version('< 9.5'):
|
|
+ return
|
|
+
|
|
+ remove_upgrade_efi_entry()
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py
|
|
new file mode 100644
|
|
index 00000000..3ff3ead9
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py
|
|
@@ -0,0 +1,100 @@
|
|
+import os
|
|
+import shutil
|
|
+
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.common.grub import GRUB2_BIOS_ENTRYPOINT, GRUB2_BIOS_ENV_FILE
|
|
+from leapp.libraries.stdlib import api, CalledProcessError, run
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo
|
|
+
|
|
+EFI_MOUNTPOINT = '/boot/efi/'
|
|
+LEAPP_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/leapp/')
|
|
+RHEL_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/redhat/')
|
|
+
|
|
+
|
|
+def get_workaround_efi_info():
|
|
+ bootloader_info_msgs = api.consume(ArmWorkaroundEFIBootloaderInfo)
|
|
+ bootloader_info = next(bootloader_info_msgs, None)
|
|
+ if list(bootloader_info_msgs):
|
|
+ api.current_logger().warning('Unexpectedly received more than one UpgradeEFIBootEntry message.')
|
|
+ if not bootloader_info:
|
|
+ raise StopActorExecutionError('Could not remove UEFI boot entry for the upgrade initramfs.',
|
|
+ details={'details': 'Did not receive a message about the leapp-provided'
|
|
+ ' kernel and initramfs.'})
|
|
+ return bootloader_info
|
|
+
|
|
+
|
|
+def remove_upgrade_efi_entry():
|
|
+ # we need to make sure /boot/efi/ is mounted before trying to remove the boot entry
|
|
+ mount_points = ['/boot', '/boot/efi']
|
|
+ for mp in mount_points:
|
|
+ try:
|
|
+ run(['/bin/mount', mp])
|
|
+ except CalledProcessError:
|
|
+ # partitions have been most likely already mounted
|
|
+ pass
|
|
+
|
|
+ bootloader_info = get_workaround_efi_info()
|
|
+
|
|
+ _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg'])
|
|
+ _link_grubenv_to_rhel_entry()
|
|
+
|
|
+ upgrade_boot_number = bootloader_info.upgrade_entry.boot_number
|
|
+ try:
|
|
+ run([
|
|
+ '/usr/sbin/efibootmgr',
|
|
+ '--delete-bootnum',
|
|
+ '--bootnum',
|
|
+ upgrade_boot_number
|
|
+ ])
|
|
+ except CalledProcessError:
|
|
+ api.current_logger().warning('Unable to remove Leapp upgrade efi entry.')
|
|
+
|
|
+ try:
|
|
+ run(['rm', '-rf', LEAPP_EFIDIR_CANONICAL_PATH])
|
|
+ except CalledProcessError:
|
|
+ api.current_logger().warning('Unable to remove Leapp upgrade efi files.')
|
|
+
|
|
+ original_boot_number = bootloader_info.original_entry.boot_number
|
|
+ run(['/usr/sbin/efibootmgr', '--bootnext', original_boot_number])
|
|
+
|
|
+ # TODO: Move calling `mount -a` to a separate actor as it is not really
|
|
+ # related to removing the upgrade boot entry. It's worth to call it after
|
|
+ # removing the boot entry to avoid boot loop in case mounting fails.
|
|
+ run(['/bin/mount', '-a'])
|
|
+
|
|
+
|
|
+def _link_grubenv_to_rhel_entry():
|
|
+ rhel_env_file = os.path.join(RHEL_EFIDIR_CANONICAL_PATH, 'grubenv')
|
|
+ rhel_env_file_relpath = os.path.relpath(rhel_env_file, GRUB2_BIOS_ENTRYPOINT)
|
|
+ run(['ln', '--symbolic', '--force', rhel_env_file_relpath, GRUB2_BIOS_ENV_FILE])
|
|
+
|
|
+
|
|
+def _copy_file(src_path, dst_path):
|
|
+ if os.path.exists(dst_path):
|
|
+ api.current_logger().debug("The {} file already exists and its content will be overwritten.".format(dst_path))
|
|
+
|
|
+ api.current_logger().info("Copying {} to {}".format(src_path, dst_path))
|
|
+ try:
|
|
+ shutil.copy2(src_path, dst_path)
|
|
+ except (OSError, IOError) as err:
|
|
+ raise StopActorExecutionError('I/O error({}): {}'.format(err.errno, err.strerror))
|
|
+
|
|
+
|
|
+def _copy_grub_files(required, optional):
|
|
+ """
|
|
+ Copy grub files from redhat/ dir to the /boot/efi/EFI/leapp/ dir.
|
|
+ """
|
|
+
|
|
+ all_files = required + optional
|
|
+ for filename in all_files:
|
|
+ src_path = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, filename)
|
|
+ dst_path = os.path.join(RHEL_EFIDIR_CANONICAL_PATH, filename)
|
|
+
|
|
+ if not os.path.exists(src_path):
|
|
+ if filename in required:
|
|
+ msg = 'Required file {} does not exists. Aborting.'.format(filename)
|
|
+ raise StopActorExecutionError(msg)
|
|
+
|
|
+ continue
|
|
+
|
|
+ _copy_file(src_path, dst_path)
|
|
diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py
|
|
new file mode 100644
|
|
index 00000000..1af3cd1e
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py
|
|
@@ -0,0 +1,105 @@
|
|
+import os
|
|
+
|
|
+import pytest
|
|
+
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.actor import removeupgradeefientry
|
|
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
|
|
+from leapp.libraries.stdlib import api
|
|
+from leapp.models import ArmWorkaroundEFIBootloaderInfo, EFIBootEntry
|
|
+
|
|
+TEST_EFI_INFO = ArmWorkaroundEFIBootloaderInfo(
|
|
+ original_entry=EFIBootEntry(
|
|
+ boot_number='0001',
|
|
+ label='Redhat',
|
|
+ active=True,
|
|
+ efi_bin_source="HD(.*)/File(\\EFI\\redhat\\shimx64.efi)",
|
|
+ ),
|
|
+ upgrade_entry=EFIBootEntry(
|
|
+ boot_number='0002',
|
|
+ label='Leapp',
|
|
+ active=True,
|
|
+ efi_bin_source="HD(.*)/File(\\EFI\\leapp\\shimx64.efi)",
|
|
+ )
|
|
+)
|
|
+
|
|
+
|
|
+def test_get_workaround_efi_info_single_entry(monkeypatch):
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[TEST_EFI_INFO]))
|
|
+
|
|
+ result = removeupgradeefientry.get_workaround_efi_info()
|
|
+ assert result == TEST_EFI_INFO
|
|
+
|
|
+
|
|
+def test_get_workaround_efi_info_multiple_entries(monkeypatch):
|
|
+ logger = logger_mocked()
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
|
|
+ msgs=[TEST_EFI_INFO, TEST_EFI_INFO]))
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ result = removeupgradeefientry.get_workaround_efi_info()
|
|
+ assert result == TEST_EFI_INFO
|
|
+ assert 'Unexpectedly received more than one UpgradeEFIBootEntry message.' in logger.warnmsg
|
|
+
|
|
+
|
|
+def test_get_workaround_efi_info_no_entry(monkeypatch):
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
|
|
+
|
|
+ with pytest.raises(StopActorExecutionError, match='Could not remove UEFI boot entry for the upgrade initramfs'):
|
|
+ removeupgradeefientry.get_workaround_efi_info()
|
|
+
|
|
+
|
|
+def test_copy_grub_files(monkeypatch):
|
|
+ copy_file_calls = []
|
|
+
|
|
+ def mock_copy_file(src, dst):
|
|
+ copy_file_calls.append((src, dst))
|
|
+
|
|
+ monkeypatch.setattr(removeupgradeefientry, '_copy_file', mock_copy_file)
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: True)
|
|
+
|
|
+ removeupgradeefientry._copy_grub_files(['required'], ['optional'])
|
|
+
|
|
+ assert (
|
|
+ os.path.join(removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH, 'required'),
|
|
+ os.path.join(removeupgradeefientry.RHEL_EFIDIR_CANONICAL_PATH, 'required'),
|
|
+ ) in copy_file_calls
|
|
+ assert (
|
|
+ os.path.join(removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH, 'optional'),
|
|
+ os.path.join(removeupgradeefientry.RHEL_EFIDIR_CANONICAL_PATH, 'optional'),
|
|
+ ) in copy_file_calls
|
|
+
|
|
+
|
|
+def test_copy_grub_files_missing_required(monkeypatch):
|
|
+ monkeypatch.setattr(os.path, 'exists', lambda path: False)
|
|
+
|
|
+ with pytest.raises(StopActorExecutionError, match='Required file required does not exists'):
|
|
+ removeupgradeefientry._copy_grub_files(['required'], [])
|
|
+
|
|
+
|
|
+def test_remove_upgrade_efi_entry(monkeypatch):
|
|
+ run_calls = []
|
|
+ copy_grub_files_calls = []
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[TEST_EFI_INFO]))
|
|
+
|
|
+ def mock_run(command, checked=False):
|
|
+ run_calls.append(command)
|
|
+ return {'exit_code': 0}
|
|
+
|
|
+ def mock_copy_grub_files(required, optional):
|
|
+ copy_grub_files_calls.append((required, optional))
|
|
+
|
|
+ monkeypatch.setattr(removeupgradeefientry, '_copy_grub_files', mock_copy_grub_files)
|
|
+ monkeypatch.setattr(removeupgradeefientry, '_link_grubenv_to_rhel_entry', lambda: None)
|
|
+ monkeypatch.setattr(removeupgradeefientry, 'run', mock_run)
|
|
+
|
|
+ removeupgradeefientry.remove_upgrade_efi_entry()
|
|
+
|
|
+ assert run_calls == [
|
|
+ ['/bin/mount', '/boot'],
|
|
+ ['/bin/mount', '/boot/efi'],
|
|
+ ['/usr/sbin/efibootmgr', '--delete-bootnum', '--bootnum', '0002'],
|
|
+ ['rm', '-rf', removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH],
|
|
+ ['/usr/sbin/efibootmgr', '--bootnext', '0001'],
|
|
+ ['/bin/mount', '-a'],
|
|
+ ]
|
|
diff --git a/repos/system_upgrade/el8toel9/models/upgradeefientry.py b/repos/system_upgrade/el8toel9/models/upgradeefientry.py
|
|
new file mode 100644
|
|
index 00000000..877cdc8f
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el8toel9/models/upgradeefientry.py
|
|
@@ -0,0 +1,14 @@
|
|
+from leapp.models import EFIBootEntry, fields, Model
|
|
+from leapp.topics import SystemInfoTopic
|
|
+
|
|
+
|
|
+class ArmWorkaroundEFIBootloaderInfo(Model):
|
|
+ """
|
|
+ Information about an Upgrade UEFI boot loader entry.
|
|
+ """
|
|
+
|
|
+ topic = SystemInfoTopic
|
|
+
|
|
+ original_entry = fields.Model(EFIBootEntry)
|
|
+
|
|
+ upgrade_entry = fields.Model(EFIBootEntry)
|
|
--
|
|
2.47.0
|
|
|