leapp-repository/SOURCES/0027-Workaround-for-ARM-Upgrades-from-RHEL8-to-RHEL9.5.patch

1757 lines
67 KiB
Diff
Raw Permalink Normal View History

2024-11-25 09:10:29 +00:00
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