leapp-repository/SOURCES/0093-raid-mdadm-include-configuration-in-initramfs.patch
2026-07-17 04:37:07 -04:00

492 lines
17 KiB
Diff

From 28fd5ea3fccb9d62bb7de90916eaa7ee8943a50e Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Thu, 28 May 2026 15:30:14 +0200
Subject: [PATCH 093/108] raid(mdadm): include configuration in initramfs
Add code to handle detection of mdraid arrays based on the outputs of
`mdadm --detail --scan`. The code is structured to handle raids in
general (there is also dm-raid), i.e., it follows the usual pattern:
scan, check; boot entry addition is not being modified for now. New
model RAIDInfo is introduced, which provides basic information about
RAID technologies used. To activate all arrays during boot, it is
necessary to have rd.md.uuid=UUID on the kernel cmdline for every RAID
array.
Jira-ref: RHEL-36249
---
.../common/actors/checkraid/actor.py | 22 ++++
.../actors/checkraid/libraries/checkraid.py | 33 +++++
.../actors/checkraid/tests/test_checkraid.py | 112 ++++++++++++++++
.../upgradeinitramfsgenerator/actor.py | 2 +
.../libraries/upgradeinitramfsgenerator.py | 5 +
.../common/actors/scanraid/actor.py | 21 +++
.../actors/scanraid/libraries/scanraid.py | 40 ++++++
.../actors/scanraid/tests/test_scanraid.py | 122 ++++++++++++++++++
.../system_upgrade/common/models/raidinfo.py | 19 +++
9 files changed, 376 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/checkraid/actor.py
create mode 100644 repos/system_upgrade/common/actors/checkraid/libraries/checkraid.py
create mode 100644 repos/system_upgrade/common/actors/checkraid/tests/test_checkraid.py
create mode 100644 repos/system_upgrade/common/actors/scanraid/actor.py
create mode 100644 repos/system_upgrade/common/actors/scanraid/libraries/scanraid.py
create mode 100644 repos/system_upgrade/common/actors/scanraid/tests/test_scanraid.py
create mode 100644 repos/system_upgrade/common/models/raidinfo.py
diff --git a/repos/system_upgrade/common/actors/checkraid/actor.py b/repos/system_upgrade/common/actors/checkraid/actor.py
new file mode 100644
index 00000000..7af1124f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkraid/actor.py
@@ -0,0 +1,22 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import checkraid
+from leapp.models import RAIDInfo, TargetUserSpaceUpgradeTasks
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckRaid(Actor):
+ """
+ Ensure RAID configuration files are available in the target userspace.
+
+ If mdadm software RAID is in use, copies present mdadm configuration
+ files and directories into the target userspace container so that
+ dracut can include them in the upgrade initramfs.
+ """
+
+ name = 'check_raid'
+ consumes = (RAIDInfo,)
+ produces = (TargetUserSpaceUpgradeTasks,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ checkraid.process()
diff --git a/repos/system_upgrade/common/actors/checkraid/libraries/checkraid.py b/repos/system_upgrade/common/actors/checkraid/libraries/checkraid.py
new file mode 100644
index 00000000..dbd02f54
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkraid/libraries/checkraid.py
@@ -0,0 +1,33 @@
+import os
+
+from leapp.libraries.stdlib import api
+from leapp.models import CopyFile, RAIDInfo, TargetUserSpaceUpgradeTasks
+
+# Host paths for mdadm configuration (see mdadm.conf(5) FILES section).
+MDADM_CONFIG_PATHS = (
+ '/etc/mdadm.conf',
+ '/etc/mdadm.conf.d',
+ '/etc/mdadm/mdadm.conf',
+ '/etc/mdadm/mdadm.conf.d',
+)
+
+
+def _mdadm_config_paths_present():
+ for path in MDADM_CONFIG_PATHS:
+ if os.path.isfile(path) or os.path.isdir(path):
+ yield path
+
+
+def process():
+ raid_info = next(api.consume(RAIDInfo), None)
+ if not raid_info or not raid_info.md_arrays:
+ return
+
+ copy_files = [CopyFile(src=path) for path in _mdadm_config_paths_present()]
+ if not copy_files:
+ api.current_logger().warning(
+ 'mdadm software RAID is in use but no mdadm configuration was found under: %s',
+ ', '.join(MDADM_CONFIG_PATHS),
+ )
+ else:
+ api.produce(TargetUserSpaceUpgradeTasks(copy_files=copy_files))
diff --git a/repos/system_upgrade/common/actors/checkraid/tests/test_checkraid.py b/repos/system_upgrade/common/actors/checkraid/tests/test_checkraid.py
new file mode 100644
index 00000000..02a6122a
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkraid/tests/test_checkraid.py
@@ -0,0 +1,112 @@
+import os
+
+from leapp.libraries.actor import checkraid
+from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import MDArray, RAIDInfo, TargetUserSpaceUpgradeTasks
+
+
+def _mock_path_checks(monkeypatch, files=(), directories=()):
+ def isfile(path):
+ return path in files
+
+ def isdir(path):
+ return path in directories
+
+ monkeypatch.setattr(os.path, 'isfile', isfile)
+ monkeypatch.setattr(os.path, 'isdir', isdir)
+
+
+def _md_arrays(*uuids):
+ return [MDArray(uuid=uuid) for uuid in uuids]
+
+
+def test_md_arrays_with_conf_dir(monkeypatch):
+ _mock_path_checks(
+ monkeypatch,
+ files=('/etc/mdadm.conf',),
+ directories=('/etc/mdadm.conf.d',),
+ )
+
+ msgs = [RAIDInfo(md_arrays=_md_arrays('aaa:bbb'))]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ assert api.produce.called == 1
+ copy_tasks = [m for m in api.produce.model_instances if isinstance(m, TargetUserSpaceUpgradeTasks)]
+ assert len(copy_tasks) == 1
+ assert [task.src for task in copy_tasks[0].copy_files] == [
+ '/etc/mdadm.conf',
+ '/etc/mdadm.conf.d',
+ ]
+
+
+def test_md_arrays_without_conf_dir(monkeypatch):
+ _mock_path_checks(monkeypatch, files=('/etc/mdadm.conf',))
+
+ msgs = [RAIDInfo(md_arrays=_md_arrays('aaa:bbb'))]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ copy_tasks = [m for m in api.produce.model_instances if isinstance(m, TargetUserSpaceUpgradeTasks)]
+ assert len(copy_tasks) == 1
+ assert [task.src for task in copy_tasks[0].copy_files] == ['/etc/mdadm.conf']
+
+
+def test_md_arrays_alternative_mdadm_conf(monkeypatch):
+ _mock_path_checks(
+ monkeypatch,
+ files=('/etc/mdadm/mdadm.conf',),
+ directories=('/etc/mdadm/mdadm.conf.d',),
+ )
+
+ msgs = [RAIDInfo(md_arrays=_md_arrays('aaa:bbb'))]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ copy_tasks = [m for m in api.produce.model_instances if isinstance(m, TargetUserSpaceUpgradeTasks)]
+ assert len(copy_tasks) == 1
+ assert [task.src for task in copy_tasks[0].copy_files] == [
+ '/etc/mdadm/mdadm.conf',
+ '/etc/mdadm/mdadm.conf.d',
+ ]
+
+
+def test_md_arrays_no_config_paths(monkeypatch):
+ _mock_path_checks(monkeypatch)
+
+ msgs = [RAIDInfo(md_arrays=_md_arrays('aaa:bbb'))]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ copy_tasks = [m for m in api.produce.model_instances if isinstance(m, TargetUserSpaceUpgradeTasks)]
+ assert not copy_tasks
+ assert not api.produce.called
+
+
+def test_no_raid_info(monkeypatch):
+ msgs = []
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ assert not api.produce.called
+
+
+def test_no_md_arrays(monkeypatch):
+ msgs = [RAIDInfo(md_arrays=[])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkraid.process()
+
+ assert not api.produce.called
diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
index c0c93036..32016ab0 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
@@ -7,6 +7,7 @@ from leapp.models import (
FIPSInfo,
LiveModeConfig,
LVMConfig,
+ RaidInfo,
TargetOSInstallationImage,
TargetUserSpaceInfo,
TargetUserSpaceUpgradeTasks,
@@ -33,6 +34,7 @@ class UpgradeInitramfsGenerator(Actor):
FIPSInfo,
LiveModeConfig,
LVMConfig,
+ RaidInfo,
RequiredUpgradeInitramPackages, # deprecated
TargetOSInstallationImage,
TargetUserSpaceInfo,
diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
index 24f35847..f2b45ae7 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
@@ -14,6 +14,7 @@ from leapp.models import (
BootContent,
LiveModeConfig,
LVMConfig,
+ RaidInfo,
TargetOSInstallationImage,
TargetUserSpaceInfo,
TargetUserSpaceUpgradeTasks,
@@ -385,6 +386,10 @@ def generate_initram_disk(context):
if next(api.consume(LVMConfig), None):
env_variables.append('LEAPP_DRACUT_LVMCONF="1"')
+ raid_info = next(api.consume(RaidInfo), None)
+ if raid_info and raid_info.md_arrays:
+ env_variables.append('LEAPP_DRACUT_MDADMCONF="1"')
+
env_variables = ' '.join(env_variables)
env_variables = env_variables.format(
kernel_version=_get_target_kernel_version(context),
diff --git a/repos/system_upgrade/common/actors/scanraid/actor.py b/repos/system_upgrade/common/actors/scanraid/actor.py
new file mode 100644
index 00000000..62234b93
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scanraid/actor.py
@@ -0,0 +1,21 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import scanraid
+from leapp.models import DistributionSignedRPM, RAIDInfo
+from leapp.tags import FactsPhaseTag, IPUWorkflowTag
+
+
+class ScanRaid(Actor):
+ """
+ Detect whether software RAID is in use on the system.
+
+ Checks if the mdadm package is installed and whether any MD arrays
+ are currently assembled and active by scanning mdadm configuration.
+ """
+
+ name = 'scan_raid'
+ consumes = (DistributionSignedRPM,)
+ produces = (RAIDInfo,)
+ tags = (FactsPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ scanraid.process()
diff --git a/repos/system_upgrade/common/actors/scanraid/libraries/scanraid.py b/repos/system_upgrade/common/actors/scanraid/libraries/scanraid.py
new file mode 100644
index 00000000..a810a238
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scanraid/libraries/scanraid.py
@@ -0,0 +1,40 @@
+import re
+
+from leapp.libraries.common.rpms import has_package
+from leapp.libraries.stdlib import api, CalledProcessError, run
+from leapp.models import DistributionSignedRPM, MDArray, RAIDInfo
+
+MDADM_SCAN_CMD = ['mdadm', '--detail', '--scan', '--verbose']
+UUID_PATTERN = re.compile(r'UUID=([0-9a-fA-F:]+)')
+
+
+def _scan_md_array_uuids():
+ try:
+ result = run(MDADM_SCAN_CMD)
+ except CalledProcessError as err:
+ api.current_logger().warning('Failed to scan mdadm arrays: %s', err)
+ return []
+
+ uuids = []
+ for line in result['stdout'].splitlines():
+ if not line.startswith('ARRAY '):
+ continue
+ match = UUID_PATTERN.search(line)
+ if match:
+ uuids.append(match.group(1))
+
+ return uuids
+
+
+def process():
+ if not has_package(DistributionSignedRPM, 'mdadm'):
+ api.current_logger().debug('The mdadm package is not installed. Skipping.')
+ return
+
+ uuids = _scan_md_array_uuids()
+ if not uuids:
+ api.current_logger().debug('No active mdadm software RAID arrays found.')
+ return
+
+ api.current_logger().info('Detected active mdadm software RAID arrays.')
+ api.produce(RAIDInfo(md_arrays=[MDArray(uuid=uuid) for uuid in uuids]))
diff --git a/repos/system_upgrade/common/actors/scanraid/tests/test_scanraid.py b/repos/system_upgrade/common/actors/scanraid/tests/test_scanraid.py
new file mode 100644
index 00000000..255be095
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scanraid/tests/test_scanraid.py
@@ -0,0 +1,122 @@
+import pytest
+
+from leapp.libraries.actor import scanraid
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+from leapp.models import DistributionSignedRPM, MDArray, RAIDInfo, RPM
+
+_MDADM_RPM = RPM(
+ name='mdadm',
+ version='4.2',
+ release='1.el9',
+ epoch='0',
+ packager='',
+ arch='x86_64',
+ pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'
+)
+
+MDADM_SCAN_WITH_ARRAY = (
+ 'ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 '
+ 'name=localhost.localdomain:0 UUID=c4acea6e:d56e1598:91822e3f:fb26832c\n'
+)
+
+MDADM_SCAN_WITH_TWO_ARRAYS = (
+ 'ARRAY /dev/md0 metadata=1.2 UUID=c4acea6e:d56e1598:91822e3f:fb26832c\n'
+ 'ARRAY /dev/md1 metadata=1.2 UUID=5eb8cf98:a8d13c2e:3e91b4ca:2e1ac678\n'
+)
+
+
+class RunMocked:
+
+ def __init__(self, stdout='', raise_err=False):
+ self.called = 0
+ self.args = None
+ self.stdout = stdout
+ self.raise_err = raise_err
+
+ def __call__(self, args, encoding=None):
+ self.called += 1
+ self.args = args
+ if self.raise_err:
+ raise CalledProcessError(
+ message='A Leapp Command Error occurred.',
+ command=args,
+ result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
+ )
+ assert args == scanraid.MDADM_SCAN_CMD
+ return {'stdout': self.stdout}
+
+
+def test_mdadm_not_installed(monkeypatch):
+ msgs = [DistributionSignedRPM(items=[])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ scanraid.process()
+
+ assert not api.produce.called
+
+
+def test_mdadm_installed_with_active_arrays(monkeypatch):
+ run_mocked = RunMocked(stdout=MDADM_SCAN_WITH_ARRAY)
+ monkeypatch.setattr(scanraid, 'run', run_mocked)
+
+ msgs = [DistributionSignedRPM(items=[_MDADM_RPM])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ scanraid.process()
+
+ assert run_mocked.called == 1
+ assert api.produce.called == 1
+ assert len(api.produce.model_instances) == 1
+ produced = api.produce.model_instances[0]
+ assert isinstance(produced, RAIDInfo)
+ assert produced.md_arrays == [MDArray(uuid='c4acea6e:d56e1598:91822e3f:fb26832c')]
+
+
+def test_mdadm_installed_with_multiple_arrays(monkeypatch):
+ run_mocked = RunMocked(stdout=MDADM_SCAN_WITH_TWO_ARRAYS)
+ monkeypatch.setattr(scanraid, 'run', run_mocked)
+
+ msgs = [DistributionSignedRPM(items=[_MDADM_RPM])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ scanraid.process()
+
+ produced = api.produce.model_instances[0]
+ assert [md_array.uuid for md_array in produced.md_arrays] == [
+ 'c4acea6e:d56e1598:91822e3f:fb26832c',
+ '5eb8cf98:a8d13c2e:3e91b4ca:2e1ac678',
+ ]
+
+
+def test_mdadm_installed_no_active_arrays(monkeypatch):
+ run_mocked = RunMocked(stdout='')
+ monkeypatch.setattr(scanraid, 'run', run_mocked)
+
+ msgs = [DistributionSignedRPM(items=[_MDADM_RPM])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ scanraid.process()
+
+ assert run_mocked.called == 1
+ assert not api.produce.called
+
+
+def test_mdadm_installed_scan_failure(monkeypatch):
+ run_mocked = RunMocked(raise_err=True)
+ monkeypatch.setattr(scanraid, 'run', run_mocked)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ msgs = [DistributionSignedRPM(items=[_MDADM_RPM])]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ scanraid.process()
+
+ assert run_mocked.called == 1
+ assert not api.produce.called
+ assert api.current_logger.warnmsg
diff --git a/repos/system_upgrade/common/models/raidinfo.py b/repos/system_upgrade/common/models/raidinfo.py
new file mode 100644
index 00000000..69188f1e
--- /dev/null
+++ b/repos/system_upgrade/common/models/raidinfo.py
@@ -0,0 +1,19 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class MDArray(Model):
+ """Information about a single mdadm software RAID array."""
+
+ topic = SystemInfoTopic
+
+ uuid = fields.String()
+ """UUID of the mdadm array."""
+
+
+class RAIDInfo(Model):
+ """Information about RAID usage on the source system."""
+ topic = SystemInfoTopic
+
+ md_arrays = fields.List(fields.Model(MDArray), default=[])
+ """List of active mdadm software RAID arrays."""
--
2.54.0