leapp-repository/0034-Update-Grub-on-component-drives-if-boot-is-on-md-dev.patch
Petr Stodulka ee57901913 CTC2 build candidate
- Fix the calculation of the required free space on each partitions/volume for the upgrade transactions
- Create source overlay images with dynamic sizes to optimize disk space consumption
- Update GRUB2 when /boot resides on multiple devices aggregated in RAID
- Use new leapp CLI API which provides better report summary output
- Introduce possibility to add (custom) kernel drivers to initramfs
- Detect and report use of deprecated Xorg drivers
- Fix the generation of the report about hybrid images
- Inhibit the upgrade when unsupported x86-64 microarchitecture is detected
- Minor improvements and fixes of various reports
- Requires leapp-framework 4.0
- Update leapp data files
- Resolves: rhbz#2140011, rhbz#2144304, rhbz#2174095, rhbz#2219544, rhbz#2215997
2023-07-18 09:39:37 +02:00

664 lines
27 KiB
Diff

From 2ba44076625e35aabfd2a1f9e45b2934f99f1e8d Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 20 Mar 2023 13:27:46 +0100
Subject: [PATCH 34/42] Update Grub on component drives if /boot is on md
device
On BIOS systems, previously, if /boot was on md device such as RAID
consisting of multiple partitions on different MBR/GPT partitioned
drives, the part of Grub residing in the 512 Mb after MBR was only
updated for one of the drives. Similar situation occurred on GPT
partitioned drives and the BIOS boot partition. This resulted in
outdated GRUB on the remaining drives which could cause the system to be
unbootable.
Now, Grub is updated on all the component devices of an md array if Grub
was already installed on them before the upgrade.
Jira: OAMG-7835
BZ#2219544
BZ#2140011
---
.../common/actors/checkgrubcore/actor.py | 7 +-
.../checkgrubcore/tests/test_checkgrubcore.py | 9 +-
.../common/actors/scangrubdevice/actor.py | 11 +--
.../tests/test_scangrubdevice.py | 35 +++++++
.../common/actors/updategrubcore/actor.py | 8 +-
.../libraries/updategrubcore.py | 48 ++++++----
.../tests/test_updategrubcore.py | 39 ++++++--
repos/system_upgrade/common/libraries/grub.py | 28 ++++++
.../system_upgrade/common/libraries/mdraid.py | 48 ++++++++++
.../common/libraries/tests/test_grub.py | 71 ++++++++++++--
.../common/libraries/tests/test_mdraid.py | 94 +++++++++++++++++++
.../system_upgrade/common/models/grubinfo.py | 12 +++
12 files changed, 358 insertions(+), 52 deletions(-)
create mode 100644 repos/system_upgrade/common/actors/scangrubdevice/tests/test_scangrubdevice.py
create mode 100644 repos/system_upgrade/common/libraries/mdraid.py
create mode 100644 repos/system_upgrade/common/libraries/tests/test_mdraid.py
diff --git a/repos/system_upgrade/common/actors/checkgrubcore/actor.py b/repos/system_upgrade/common/actors/checkgrubcore/actor.py
index 6aa99797..ae9e53ef 100644
--- a/repos/system_upgrade/common/actors/checkgrubcore/actor.py
+++ b/repos/system_upgrade/common/actors/checkgrubcore/actor.py
@@ -32,7 +32,7 @@ class CheckGrubCore(Actor):
grub_info = next(self.consume(GrubInfo), None)
if not grub_info:
raise StopActorExecutionError('Actor did not receive any GrubInfo message.')
- if grub_info.orig_device_name:
+ if grub_info.orig_devices:
create_report([
reporting.Title(
'GRUB2 core will be automatically updated during the upgrade'
@@ -45,8 +45,9 @@ class CheckGrubCore(Actor):
create_report([
reporting.Title('Leapp could not identify where GRUB2 core is located'),
reporting.Summary(
- 'We assumed GRUB2 core is located on the same device as /boot, however Leapp could not '
- 'detect GRUB2 on the device. GRUB2 core needs to be updated maually on legacy (BIOS) systems. '
+ 'We assumed GRUB2 core is located on the same device(s) as /boot, '
+ 'however Leapp could not detect GRUB2 on the device(s). '
+ 'GRUB2 core needs to be updated maually on legacy (BIOS) systems. '
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.BOOT]),
diff --git a/repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py b/repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py
index fe15b65b..b834f9fe 100644
--- a/repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py
+++ b/repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py
@@ -1,18 +1,17 @@
-import pytest
-
-from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import mock_configs
from leapp.models import FirmwareFacts, GrubInfo
from leapp.reporting import Report
NO_GRUB = 'Leapp could not identify where GRUB2 core is located'
+GRUB = 'GRUB2 core will be automatically updated during the upgrade'
def test_actor_update_grub(current_actor_context):
current_actor_context.feed(FirmwareFacts(firmware='bios'))
- current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
+ current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
current_actor_context.run(config_model=mock_configs.CONFIG)
assert current_actor_context.consume(Report)
+ assert current_actor_context.consume(Report)[0].report['title'].startswith(GRUB)
def test_actor_no_grub_device(current_actor_context):
@@ -31,6 +30,6 @@ def test_actor_with_efi(current_actor_context):
def test_s390x(current_actor_context):
current_actor_context.feed(FirmwareFacts(firmware='bios'))
- current_actor_context.feed(GrubInfo(orig_device_name='/dev/vda'))
+ current_actor_context.feed(GrubInfo(orig_devices=['/dev/vda', '/dev/vdb']))
current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
assert not current_actor_context.consume(Report)
diff --git a/repos/system_upgrade/common/actors/scangrubdevice/actor.py b/repos/system_upgrade/common/actors/scangrubdevice/actor.py
index a12739e1..cb6be7ea 100644
--- a/repos/system_upgrade/common/actors/scangrubdevice/actor.py
+++ b/repos/system_upgrade/common/actors/scangrubdevice/actor.py
@@ -7,7 +7,7 @@ from leapp.tags import FactsPhaseTag, IPUWorkflowTag
class ScanGrubDeviceName(Actor):
"""
- Find the name of the block device where GRUB is located
+ Find the name of the block devices where GRUB is located
"""
name = 'scan_grub_device_name'
@@ -19,8 +19,7 @@ class ScanGrubDeviceName(Actor):
if architecture.matches_architecture(architecture.ARCH_S390X):
return
- device_name = grub.get_grub_device()
- if device_name:
- self.produce(GrubInfo(orig_device_name=device_name))
- else:
- self.produce(GrubInfo())
+ devices = grub.get_grub_devices()
+ grub_info = GrubInfo(orig_devices=devices)
+ grub_info.orig_device_name = devices[0] if len(devices) == 1 else None
+ self.produce(grub_info)
diff --git a/repos/system_upgrade/common/actors/scangrubdevice/tests/test_scangrubdevice.py b/repos/system_upgrade/common/actors/scangrubdevice/tests/test_scangrubdevice.py
new file mode 100644
index 00000000..0114d717
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scangrubdevice/tests/test_scangrubdevice.py
@@ -0,0 +1,35 @@
+from leapp.libraries.common import grub
+from leapp.libraries.common.config import mock_configs
+from leapp.models import GrubInfo
+
+
+def _get_grub_devices_mocked():
+ return ['/dev/vda', '/dev/vdb']
+
+
+def test_actor_scan_grub_device(current_actor_context, monkeypatch):
+ monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
+ current_actor_context.run(config_model=mock_configs.CONFIG)
+ info = current_actor_context.consume(GrubInfo)
+ assert info and info[0].orig_devices == ['/dev/vda', '/dev/vdb']
+ assert len(info) == 1, 'Expected just one GrubInfo message'
+ assert not info[0].orig_device_name
+
+
+def test_actor_scan_grub_device_one(current_actor_context, monkeypatch):
+
+ def _get_grub_devices_mocked():
+ return ['/dev/vda']
+
+ monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
+ current_actor_context.run(config_model=mock_configs.CONFIG)
+ info = current_actor_context.consume(GrubInfo)
+ assert info and info[0].orig_devices == ['/dev/vda']
+ assert len(info) == 1, 'Expected just one GrubInfo message'
+ assert info[0].orig_device_name == '/dev/vda'
+
+
+def test_actor_scan_grub_device_s390x(current_actor_context, monkeypatch):
+ monkeypatch.setattr(grub, 'get_grub_devices', _get_grub_devices_mocked)
+ current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
+ assert not current_actor_context.consume(GrubInfo)
diff --git a/repos/system_upgrade/common/actors/updategrubcore/actor.py b/repos/system_upgrade/common/actors/updategrubcore/actor.py
index 4545bad6..ac9aa829 100644
--- a/repos/system_upgrade/common/actors/updategrubcore/actor.py
+++ b/repos/system_upgrade/common/actors/updategrubcore/actor.py
@@ -21,8 +21,8 @@ class UpdateGrubCore(Actor):
def process(self):
ff = next(self.consume(FirmwareFacts), None)
if ff and ff.firmware == 'bios':
- grub_dev = grub.get_grub_device()
- if grub_dev:
- update_grub_core(grub_dev)
+ grub_devs = grub.get_grub_devices()
+ if grub_devs:
+ update_grub_core(grub_devs)
else:
- api.current_logger().warning('Leapp could not detect GRUB on {}'.format(grub_dev))
+ api.current_logger().warning('Leapp could not detect GRUB devices')
diff --git a/repos/system_upgrade/common/actors/updategrubcore/libraries/updategrubcore.py b/repos/system_upgrade/common/actors/updategrubcore/libraries/updategrubcore.py
index 22ee3372..2bdad929 100644
--- a/repos/system_upgrade/common/actors/updategrubcore/libraries/updategrubcore.py
+++ b/repos/system_upgrade/common/actors/updategrubcore/libraries/updategrubcore.py
@@ -1,35 +1,43 @@
from leapp import reporting
-from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api, CalledProcessError, config, run
-def update_grub_core(grub_dev):
+def update_grub_core(grub_devs):
"""
Update GRUB core after upgrade from RHEL7 to RHEL8
On legacy systems, GRUB core does not get automatically updated when GRUB packages
are updated.
"""
- cmd = ['grub2-install', grub_dev]
- if config.is_debug():
- cmd += ['-v']
- try:
- run(cmd)
- except CalledProcessError as err:
- reporting.create_report([
- reporting.Title('GRUB core update failed'),
- reporting.Summary(str(err)),
- reporting.Groups([reporting.Groups.BOOT]),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Remediation(
- hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
- )
- ])
- api.current_logger().warning('GRUB core update on {} failed'.format(grub_dev))
- raise StopActorExecution()
+
+ successful = []
+ failed = []
+ for dev in grub_devs:
+ cmd = ['grub2-install', dev]
+ if config.is_debug():
+ cmd += ['-v']
+ try:
+ run(cmd)
+ except CalledProcessError as err:
+ api.current_logger().warning('GRUB core update on {} failed: {}'.format(dev, err))
+ failed.append(dev)
+ continue
+
+ successful.append(dev)
+
+ reporting.create_report([
+ reporting.Title('GRUB core update failed'),
+ reporting.Summary('Leapp failed to update GRUB on {}'.format(', '.join(failed))),
+ reporting.Groups([reporting.Groups.BOOT]),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Remediation(
+ hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
+ )
+ ])
+
reporting.create_report([
reporting.Title('GRUB core successfully updated'),
- reporting.Summary('GRUB core on {} was successfully updated'.format(grub_dev)),
+ reporting.Summary('GRUB core on {} was successfully updated'.format(', '.join(successful))),
reporting.Groups([reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.INFO)
])
diff --git a/repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py b/repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py
index e65807a2..fe0cca50 100644
--- a/repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py
+++ b/repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py
@@ -1,7 +1,6 @@
import pytest
from leapp import reporting
-from leapp.exceptions import StopActorExecution
from leapp.libraries.actor import updategrubcore
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import api, CalledProcessError
@@ -32,21 +31,45 @@ class run_mocked(object):
raise_call_error(args)
-def test_update_grub(monkeypatch):
+@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
+def test_update_grub(monkeypatch, devices):
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
monkeypatch.setattr(updategrubcore, 'run', run_mocked())
- updategrubcore.update_grub_core('/dev/vda')
+ updategrubcore.update_grub_core(devices)
assert reporting.create_report.called
- assert UPDATE_OK_TITLE == reporting.create_report.report_fields['title']
+ assert UPDATE_OK_TITLE == reporting.create_report.reports[1]['title']
+ assert all(dev in reporting.create_report.reports[1]['summary'] for dev in devices)
-def test_update_grub_failed(monkeypatch):
+@pytest.mark.parametrize('devices', [['/dev/vda'], ['/dev/vda', '/dev/vdb']])
+def test_update_grub_failed(monkeypatch, devices):
monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
monkeypatch.setattr(updategrubcore, 'run', run_mocked(raise_err=True))
- with pytest.raises(StopActorExecution):
- updategrubcore.update_grub_core('/dev/vda')
+ updategrubcore.update_grub_core(devices)
assert reporting.create_report.called
- assert UPDATE_FAILED_TITLE == reporting.create_report.report_fields['title']
+ assert UPDATE_FAILED_TITLE == reporting.create_report.reports[0]['title']
+ assert all(dev in reporting.create_report.reports[0]['summary'] for dev in devices)
+
+
+def test_update_grub_success_and_fail(monkeypatch):
+ monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
+
+ def run_mocked(args):
+ if args == ['grub2-install', '/dev/vdb']:
+ raise_call_error(args)
+ else:
+ assert args == ['grub2-install', '/dev/vda']
+
+ monkeypatch.setattr(updategrubcore, 'run', run_mocked)
+
+ devices = ['/dev/vda', '/dev/vdb']
+ updategrubcore.update_grub_core(devices)
+
+ assert reporting.create_report.called
+ assert UPDATE_FAILED_TITLE == reporting.create_report.reports[0]['title']
+ assert '/dev/vdb' in reporting.create_report.reports[0]['summary']
+ assert UPDATE_OK_TITLE == reporting.create_report.reports[1]['title']
+ assert '/dev/vda' in reporting.create_report.reports[1]['summary']
def test_update_grub_negative(current_actor_context):
diff --git a/repos/system_upgrade/common/libraries/grub.py b/repos/system_upgrade/common/libraries/grub.py
index f6b00f65..79b3be39 100644
--- a/repos/system_upgrade/common/libraries/grub.py
+++ b/repos/system_upgrade/common/libraries/grub.py
@@ -1,7 +1,9 @@
import os
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
def has_grub(blk_dev):
@@ -59,6 +61,32 @@ def get_boot_partition():
return boot_partition
+def get_grub_devices():
+ """
+ Get block devices where GRUB is located. We assume GRUB is on the same device
+ as /boot partition is. In case that device is an md (Multiple Device) device, all
+ of the component devices of such a device are considered.
+
+ :return: Devices where GRUB is located
+ :rtype: list
+ """
+ boot_device = get_boot_partition()
+ devices = []
+ if mdraid.is_mdraid_dev(boot_device):
+ component_devs = mdraid.get_component_devices(boot_device)
+ blk_devs = [blk_dev_from_partition(dev) for dev in component_devs]
+ # remove duplicates as there might be raid on partitions on the same drive
+ # even if that's very unusual
+ devices = sorted(list(set(blk_devs)))
+ else:
+ devices.append(blk_dev_from_partition(boot_device))
+
+ have_grub = [dev for dev in devices if has_grub(dev)]
+ api.current_logger().info('GRUB is installed on {}'.format(",".join(have_grub)))
+ return have_grub
+
+
+@deprecated(since='2023-06-23', message='This function has been replaced by get_grub_devices')
def get_grub_device():
"""
Get block device where GRUB is located. We assume GRUB is on the same device
diff --git a/repos/system_upgrade/common/libraries/mdraid.py b/repos/system_upgrade/common/libraries/mdraid.py
new file mode 100644
index 00000000..5eb89c56
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/mdraid.py
@@ -0,0 +1,48 @@
+from leapp.libraries.stdlib import api, CalledProcessError, run
+
+
+def is_mdraid_dev(dev):
+ """
+ Check if a given device is an md (Multiple Device) device
+
+ It is expected that the "mdadm" command is available,
+ if it's not it is assumed the device is not an md device.
+
+ :return: True if the device is an md device, False otherwise
+ :raises CalledProcessError: If an error occurred
+ """
+ fail_msg = 'Could not check if device "{}" is an md device: {}'
+ try:
+ result = run(['mdadm', '--query', dev])
+ except OSError as err:
+ api.current_logger().warning(fail_msg.format(dev, err))
+ return False
+ except CalledProcessError as err:
+ err.message = fail_msg.format(dev, err)
+ raise # let the calling actor handle the exception
+
+ return '--detail' in result['stdout']
+
+
+def get_component_devices(raid_dev):
+ """
+ Get list of component devices in an md (Multiple Device) array
+
+ :return: The list of component devices or None in case of error
+ :raises ValueError: If the device is not an mdraid device
+ """
+ try:
+ # using both --verbose and --brief for medium verbosity
+ result = run(['mdadm', '--detail', '--verbose', '--brief', raid_dev])
+ except (OSError, CalledProcessError) as err:
+ api.current_logger().warning(
+ 'Could not get md array component devices: {}'.format(err)
+ )
+ return None
+ # example output:
+ # ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 name=localhost.localdomain:0 UUID=c4acea6e:d56e1598:91822e3f:fb26832c # noqa: E501; pylint: disable=line-too-long
+ # devices=/dev/vda1,/dev/vdb1
+ if 'does not appear to be an md device' in result['stdout']:
+ raise ValueError("Expected md device, but got: {}".format(raid_dev))
+
+ return sorted(result['stdout'].rsplit('=', 2)[-1].strip().split(','))
diff --git a/repos/system_upgrade/common/libraries/tests/test_grub.py b/repos/system_upgrade/common/libraries/tests/test_grub.py
index ba086854..9ced1147 100644
--- a/repos/system_upgrade/common/libraries/tests/test_grub.py
+++ b/repos/system_upgrade/common/libraries/tests/test_grub.py
@@ -3,7 +3,7 @@ import os
import pytest
from leapp.exceptions import StopActorExecution
-from leapp.libraries.common import grub
+from leapp.libraries.common import grub, mdraid
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import DefaultGrub, DefaultGrubInfo
@@ -11,6 +11,9 @@ from leapp.models import DefaultGrub, DefaultGrubInfo
BOOT_PARTITION = '/dev/vda1'
BOOT_DEVICE = '/dev/vda'
+MD_BOOT_DEVICE = '/dev/md0'
+MD_BOOT_DEVICES_WITH_GRUB = ['/dev/sda', '/dev/sdb']
+
VALID_DD = b'GRUB GeomHard DiskRead Error'
INVALID_DD = b'Nothing to see here!'
@@ -27,10 +30,11 @@ def raise_call_error(args=None):
class RunMocked(object):
- def __init__(self, raise_err=False):
+ def __init__(self, raise_err=False, boot_on_raid=False):
self.called = 0
self.args = None
self.raise_err = raise_err
+ self.boot_on_raid = boot_on_raid
def __call__(self, args, encoding=None):
self.called += 1
@@ -39,18 +43,22 @@ class RunMocked(object):
raise_call_error(args)
if self.args == ['grub2-probe', '--target=device', '/boot']:
- stdout = BOOT_PARTITION
+ stdout = MD_BOOT_DEVICE if self.boot_on_raid else BOOT_PARTITION
elif self.args == ['lsblk', '-spnlo', 'name', BOOT_PARTITION]:
stdout = BOOT_DEVICE
+ elif self.args[:-1] == ['lsblk', '-spnlo', 'name']:
+ stdout = self.args[-1][:-1]
return {'stdout': stdout}
def open_mocked(fn, flags):
- return open(
- os.path.join(CUR_DIR, 'grub_valid') if fn == BOOT_DEVICE else os.path.join(CUR_DIR, 'grub_invalid'), 'r'
- )
+ if fn == BOOT_DEVICE or fn in MD_BOOT_DEVICES_WITH_GRUB:
+ path = os.path.join(CUR_DIR, 'grub_valid')
+ else:
+ path = os.path.join(CUR_DIR, 'grub_invalid')
+ return open(path, 'r')
def open_invalid(fn, flags):
@@ -122,3 +130,54 @@ def test_is_blscfg_library(monkeypatch, enabled):
assert result
else:
assert not result
+
+
+def is_mdraid_dev_mocked(dev):
+ return dev == '/dev/md0'
+
+
+def test_get_grub_devices_one_device(monkeypatch):
+ run_mocked = RunMocked()
+ monkeypatch.setattr(grub, 'run', run_mocked)
+ monkeypatch.setattr(os, 'open', open_mocked)
+ monkeypatch.setattr(os, 'read', read_mocked)
+ monkeypatch.setattr(os, 'close', close_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+ monkeypatch.setattr(mdraid, 'is_mdraid_dev', is_mdraid_dev_mocked)
+
+ result = grub.get_grub_devices()
+ assert grub.run.called == 2
+ assert [BOOT_DEVICE] == result
+ assert not api.current_logger.warnmsg
+ assert 'GRUB is installed on {}'.format(",".join(result)) in api.current_logger.infomsg
+
+
+@pytest.mark.parametrize(
+ ',component_devs,expected',
+ [
+ (['/dev/sda1', '/dev/sdb1'], MD_BOOT_DEVICES_WITH_GRUB),
+ (['/dev/sda1', '/dev/sdb1', '/dev/sdc1', '/dev/sdd1'], MD_BOOT_DEVICES_WITH_GRUB),
+ (['/dev/sda2', '/dev/sdc1'], ['/dev/sda']),
+ (['/dev/sdd3', '/dev/sdb2'], ['/dev/sdb']),
+ ]
+)
+def test_get_grub_devices_raid_device(monkeypatch, component_devs, expected):
+ run_mocked = RunMocked(boot_on_raid=True)
+ monkeypatch.setattr(grub, 'run', run_mocked)
+ monkeypatch.setattr(os, 'open', open_mocked)
+ monkeypatch.setattr(os, 'read', read_mocked)
+ monkeypatch.setattr(os, 'close', close_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+ monkeypatch.setattr(mdraid, 'is_mdraid_dev', is_mdraid_dev_mocked)
+
+ def get_component_devices_mocked(raid_dev):
+ assert raid_dev == MD_BOOT_DEVICE
+ return component_devs
+
+ monkeypatch.setattr(mdraid, 'get_component_devices', get_component_devices_mocked)
+
+ result = grub.get_grub_devices()
+ assert grub.run.called == 1 + len(component_devs) # grub2-probe + Nx lsblk
+ assert sorted(expected) == result
+ assert not api.current_logger.warnmsg
+ assert 'GRUB is installed on {}'.format(",".join(result)) in api.current_logger.infomsg
diff --git a/repos/system_upgrade/common/libraries/tests/test_mdraid.py b/repos/system_upgrade/common/libraries/tests/test_mdraid.py
new file mode 100644
index 00000000..6a25d736
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/tests/test_mdraid.py
@@ -0,0 +1,94 @@
+import os
+
+import pytest
+
+from leapp.libraries.common import mdraid
+from leapp.libraries.common.testutils import logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+
+MD_DEVICE = '/dev/md0'
+NOT_MD_DEVICE = '/dev/sda'
+
+CUR_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def raise_call_error(args=None):
+ raise CalledProcessError(
+ message='A Leapp Command Error occurred.',
+ command=args,
+ result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
+ )
+
+
+class RunMocked(object):
+
+ def __init__(self, raise_err=False):
+ self.called = 0
+ self.args = None
+ self.raise_err = raise_err
+
+ def __call__(self, args, encoding=None):
+ self.called += 1
+ self.args = args
+ if self.raise_err:
+ raise_call_error(args)
+
+ if self.args == ['mdadm', '--query', MD_DEVICE]:
+ stdout = '/dev/md0: 1022.00MiB raid1 2 devices, 0 spares. Use mdadm --detail for more detail.'
+ elif self.args == ['mdadm', '--query', NOT_MD_DEVICE]:
+ stdout = '/dev/sda: is not an md array'
+
+ elif self.args == ['mdadm', '--detail', '--verbose', '--brief', MD_DEVICE]:
+ stdout = 'ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 name=localhost.localdomain:0 UUID=c4acea6e:d56e1598:91822e3f:fb26832c\n devices=/dev/sda1,/dev/sdb1' # noqa: E501; pylint: disable=line-too-long
+ elif self.args == ['mdadm', '--detail', '--verbose', '--brief', NOT_MD_DEVICE]:
+ stdout = 'mdadm: /dev/sda does not appear to be an md device'
+
+ return {'stdout': stdout}
+
+
+@pytest.mark.parametrize('dev,expected', [(MD_DEVICE, True), (NOT_MD_DEVICE, False)])
+def test_is_mdraid_dev(monkeypatch, dev, expected):
+ run_mocked = RunMocked()
+ monkeypatch.setattr(mdraid, 'run', run_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ result = mdraid.is_mdraid_dev(dev)
+ assert mdraid.run.called == 1
+ assert expected == result
+ assert not api.current_logger.warnmsg
+
+
+def test_is_mdraid_dev_error(monkeypatch):
+ run_mocked = RunMocked(raise_err=True)
+ monkeypatch.setattr(mdraid, 'run', run_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ with pytest.raises(CalledProcessError) as err:
+ mdraid.is_mdraid_dev(MD_DEVICE)
+
+ assert mdraid.run.called == 1
+ expect_msg = 'Could not check if device "{}" is an md device:'.format(MD_DEVICE)
+ assert expect_msg in err.value.message
+
+
+def test_get_component_devices_ok(monkeypatch):
+ run_mocked = RunMocked()
+ monkeypatch.setattr(mdraid, 'run', run_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ result = mdraid.get_component_devices(MD_DEVICE)
+ assert mdraid.run.called == 1
+ assert ['/dev/sda1', '/dev/sdb1'] == result
+ assert not api.current_logger.warnmsg
+
+
+def test_get_component_devices_not_md_device(monkeypatch):
+ run_mocked = RunMocked()
+ monkeypatch.setattr(mdraid, 'run', run_mocked)
+
+ with pytest.raises(ValueError) as err:
+ mdraid.get_component_devices(NOT_MD_DEVICE)
+
+ assert mdraid.run.called == 1
+ expect_msg = 'Expected md device, but got: {}'.format(NOT_MD_DEVICE)
+ assert expect_msg in str(err.value)
diff --git a/repos/system_upgrade/common/models/grubinfo.py b/repos/system_upgrade/common/models/grubinfo.py
index 952d01c1..f89770b4 100644
--- a/repos/system_upgrade/common/models/grubinfo.py
+++ b/repos/system_upgrade/common/models/grubinfo.py
@@ -8,6 +8,8 @@ class GrubInfo(Model):
"""
topic = SystemFactsTopic
+ # NOTE: @deprecated is not supported on fields
+ # @deprecated(since='2023-06-23', message='This field has been replaced by orig_devices')
orig_device_name = fields.Nullable(fields.String())
"""
Original name of the block device where Grub is located.
@@ -17,3 +19,13 @@ class GrubInfo(Model):
it's recommended to use `leapp.libraries.common.grub.get_grub_device()` anywhere
else.
"""
+
+ orig_devices = fields.List(fields.String(), default=[])
+ """
+ Original names of the block devices where Grub is located.
+
+ The names are persistent during the boot of the system so it's safe to be used during
+ preupgrade phases. However the names could be different after the reboot, so
+ it's recommended to use `leapp.libraries.common.grub.get_grub_devices()` everywhere
+ else.
+ """
--
2.41.0