IPU 9.9 -> 10.3: CTC2 candidate 1

- Requires leapp-framework 6.6+
- Initial implementation of upgrades on systems with configured Software RAID
- Include multipath related configuration in the upgrade environment
- Fix upgrades on systems with multiple LUKS devices
- Fix upgrades for systems with /boot/efi on a Software RAID
- Fix upgrade crashing when dnf repofiles contain URL encoded characters
- Fix mount failures for FSTAB entries with the `nofail` option specified
- Fix upgrade incorrectly resuming during SELinux relabeling
- Fix AVC SELinux warnings
- LiveMode: Fix upgrade getting stuck when the `.leapp_upgrade_failed` file exists
- LiveMode: Fix system version determination in the upgrade environment on CentOS Stream
- Fix target system initramfs containing outdated configuration files in some circumstances
- Inhibit the upgrade when network devices configured with ifcfg files could depend on NM
- Ensure SELinux is set to permissive mode during the upgrade even when enforcing=1 is set on the kernel cmdline
- Fix source and target distribution names in /etc/migration-results
- Fix upgrades with the RealTime kernel on CentOS Stream
- Install the correct kernel for the source system kernel page size on ARM systems
- Introduce rhui.obsolete_gpg_keys configuration option for the removal of obsoleted RPM GPG keys of RHUI Cloud providers
- Detect misconfigured kernel & systemd API mountpoints in FSTAB
- Detect misconfigured /var/run on the source system
- Unify behaviour of upgrades on systems with enabled CRB repositories and explicitly required CRB repositories by user during the upgrade
- Drop the inconsistent report about the use of CRB (unsupported by Red Hat) repositories
- Check and migrate the multipath configuration when possible
- Fix unwanted suspend mode after 15min of inactivity after the upgrade on systems with graphical environment
- Resolves: RHEL-111860, RHEL-115867, RHEL-121205, RHEL-147438, RHEL-148697, RHEL-151509,
 RHEL-154369, RHEL-154370, RHEL-156580, RHEL-161560, RHEL-161684, RHEL-174874
This commit is contained in:
karolinku 2026-06-25 09:51:54 +02:00 committed by Petr Stodulka
parent 5152897f93
commit dc04575ed7
62 changed files with 20883 additions and 4 deletions

View File

@ -0,0 +1,72 @@
From 97bdaa4d90bdbb951de9d920036d93c08579bf2e Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Fri, 17 Apr 2026 16:56:28 +0530
Subject: [PATCH 045/105] repofileutils: disable ConfigParser interpolation
when parsing .repo files
This occurs when parsing repository definitions containing percent-encoded
values (e.g. '%252B'in baseurls). Python's ConfigParser enables
interpolation by default and treats '%' as a special token, which causes
valid URLs to fail during parsing.
Disable interpolation by using parse_config(..., no_interpolation=True)
so that repository values are handled literally, consistent with DNF/YUM
behavior.
Jira-ref: RHEL-162328
---
.../common/libraries/repofileutils.py | 2 +-
repos/system_upgrade/common/libraries/utils.py | 14 ++++++++++++--
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/repofileutils.py b/repos/system_upgrade/common/libraries/repofileutils.py
index d8e087c1..4518b091 100644
--- a/repos/system_upgrade/common/libraries/repofileutils.py
+++ b/repos/system_upgrade/common/libraries/repofileutils.py
@@ -48,7 +48,7 @@ def parse_repofile(repofile):
"""
data = []
with open(repofile, mode='r') as fp:
- cp = utils.parse_config(fp, strict=False)
+ cp = utils.parse_config(fp, strict=False, no_interpolation=True)
for repoid in cp.sections():
try:
data.append(_parse_repository(repoid, dict(cp.items(repoid))))
diff --git a/repos/system_upgrade/common/libraries/utils.py b/repos/system_upgrade/common/libraries/utils.py
index b7aa9c74..bd5337c5 100644
--- a/repos/system_upgrade/common/libraries/utils.py
+++ b/repos/system_upgrade/common/libraries/utils.py
@@ -10,7 +10,7 @@ from leapp.libraries.stdlib import api, CalledProcessError, config, run, STDOUT
from leapp.utils.deprecation import deprecated
-def parse_config(cfg=None, strict=True):
+def parse_config(cfg=None, strict=True, no_interpolation=False):
"""
Applies a workaround to parse a config file using py3 AND py2
@@ -18,10 +18,20 @@ def parse_config(cfg=None, strict=True):
the old ones (Py2) obsoletes, these function was created to use the
ConfigParser on Py2 and Py3
+ When no_interpolation is True, values are read literally. Use this for
+ inputs that may contain percent-encoded URLs (e.g. yum .repo baseurl),
+ since default ConfigParser treats ``%`` as interpolation syntax.
+
:type cfg: str
:type strict: bool
+ :type no_interpolation: bool
"""
- if six.PY3:
+ if no_interpolation:
+ if six.PY3:
+ parser = six.moves.configparser.RawConfigParser(strict=strict) # pylint: disable=unexpected-keyword-arg
+ else:
+ parser = six.moves.configparser.RawConfigParser()
+ elif six.PY3:
parser = six.moves.configparser.ConfigParser(strict=strict) # pylint: disable=unexpected-keyword-arg
else:
parser = six.moves.configparser.ConfigParser()
--
2.54.0

View File

@ -0,0 +1,157 @@
From 4ccbb21ba2fec74400ad846e41248a0606fd256e Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Mon, 20 Apr 2026 14:15:50 +0530
Subject: [PATCH 046/105] Drop six dependency and simplify config parsing to
Python 3 native configparser
- Replaced six.moves.configparser with standard configparser
- Removed Python 2 compatibility code paths
- Simplified string/file handling using read_string and read_file
- Updated parse_config implementation for Python 3 only
---
.../libraries/tests/test_repofileutils.py | 20 +++++++++
.../tests/test_utils_parse_config.py | 30 +++++++++++++
.../system_upgrade/common/libraries/utils.py | 42 +++++++------------
3 files changed, 65 insertions(+), 27 deletions(-)
create mode 100644 repos/system_upgrade/common/libraries/tests/test_utils_parse_config.py
diff --git a/repos/system_upgrade/common/libraries/tests/test_repofileutils.py b/repos/system_upgrade/common/libraries/tests/test_repofileutils.py
index d161e4bc..f9fb5623 100644
--- a/repos/system_upgrade/common/libraries/tests/test_repofileutils.py
+++ b/repos/system_upgrade/common/libraries/tests/test_repofileutils.py
@@ -75,3 +75,23 @@ def test_parse_repofile():
repos_duplicate = [repo for repo in repofile.data if repo.repoid == 'duplicate']
assert len(repos_duplicate) == 1 # only one instance got through
assert repos_duplicate[0].name == 'Duplicate 2' # and it's the latter one
+
+
+def test_parse_repofile_preserves_url_encoded_characters(tmp_path):
+ """Percent signs from URL encoding must not be treated as config interpolation."""
+ repo_path = tmp_path / 'urlencoded.repo'
+ expected_baseurl = (
+ 'https://cdn.example.com/content/dist/rhel8/%24releasever/x86_64/os/'
+ '?token=foo%2Fbar%3Dbaz'
+ )
+ repo_path.write_text(
+ '[urlencoded]\n'
+ 'name=URL-encoded baseurl\n'
+ 'baseurl={}\n'
+ 'enabled=0\n'.format(expected_baseurl),
+ encoding='utf-8',
+ )
+
+ repofile = repofileutils.parse_repofile(str(repo_path))
+ repo = next(r for r in repofile.data if r.repoid == 'urlencoded')
+ assert repo.baseurl == expected_baseurl
diff --git a/repos/system_upgrade/common/libraries/tests/test_utils_parse_config.py b/repos/system_upgrade/common/libraries/tests/test_utils_parse_config.py
new file mode 100644
index 00000000..64c9e735
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/tests/test_utils_parse_config.py
@@ -0,0 +1,30 @@
+from configparser import InterpolationSyntaxError
+
+import pytest
+
+from leapp.libraries.common import utils
+
+
+def test_parse_config_no_interpolation_preserves_url_encoding():
+ """Values with URL-encoded sequences must be read literally (no % interpolation)."""
+ cfg_text = (
+ '[repo]\n'
+ 'name=test\n'
+ 'baseurl=https://example.com/path%20with%20spaces/repo%3Fquery=1\n'
+ )
+ parser = utils.parse_config(cfg_text, strict=False, no_interpolation=True)
+ assert parser.get('repo', 'baseurl') == (
+ 'https://example.com/path%20with%20spaces/repo%3Fquery=1'
+ )
+
+
+def test_parse_config_interpolation_rejects_bare_percent_sequences():
+ """Default ConfigParser treats '%' as interpolation; invalid sequences must raise."""
+ cfg_text = (
+ '[repo]\n'
+ 'name=test\n'
+ 'baseurl=https://example.com/path%20with%20spaces/repo\n'
+ )
+ parser = utils.parse_config(cfg_text, strict=False, no_interpolation=False)
+ with pytest.raises(InterpolationSyntaxError):
+ parser.get('repo', 'baseurl')
diff --git a/repos/system_upgrade/common/libraries/utils.py b/repos/system_upgrade/common/libraries/utils.py
index bd5337c5..2c9aaee9 100644
--- a/repos/system_upgrade/common/libraries/utils.py
+++ b/repos/system_upgrade/common/libraries/utils.py
@@ -1,8 +1,7 @@
import functools
import os
import sys
-
-import six
+from configparser import ConfigParser, RawConfigParser
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting
@@ -12,44 +11,33 @@ from leapp.utils.deprecation import deprecated
def parse_config(cfg=None, strict=True, no_interpolation=False):
"""
- Applies a workaround to parse a config file using py3 AND py2
+ Parse a config input.
- ConfigParser has a new def to read strings/files in Py3, making
- the old ones (Py2) obsoletes, these function was created to use the
- ConfigParser on Py2 and Py3
+ When ``no_interpolation`` is True, ``RawConfigParser`` is used to read
+ values literally. This is useful for inputs that may contain URLs with
+ '%' characters for URL encoding (e.g. yum .repo baseurl).
- When no_interpolation is True, values are read literally. Use this for
- inputs that may contain percent-encoded URLs (e.g. yum .repo baseurl),
- since default ConfigParser treats ``%`` as interpolation syntax.
+ When set to False, the default ``ConfigParser`` is used, where '%' is treated
+ as interpolation syntax and must be escaped (%%) if used literally.
- :type cfg: str
+ :param cfg: Config data as a string or a file-like object
+ :type cfg: str or file-like object
+ :param strict: Enable strict parsing (no duplicate sections/options)
:type strict: bool
+ :param no_interpolation: Disable interpolation and read values literally
:type no_interpolation: bool
"""
if no_interpolation:
- if six.PY3:
- parser = six.moves.configparser.RawConfigParser(strict=strict) # pylint: disable=unexpected-keyword-arg
- else:
- parser = six.moves.configparser.RawConfigParser()
- elif six.PY3:
- parser = six.moves.configparser.ConfigParser(strict=strict) # pylint: disable=unexpected-keyword-arg
+ parser = RawConfigParser(strict=strict)
else:
- parser = six.moves.configparser.ConfigParser()
+ parser = ConfigParser(strict=strict)
# we do not handle exception here, handle with it when these function is called
- if cfg and six.PY3:
- # Python 3
- if isinstance(cfg, six.string_types):
+ if cfg:
+ if isinstance(cfg, str):
parser.read_string(cfg)
else:
parser.read_file(cfg)
- elif cfg:
- # Python 2
- from cStringIO import StringIO # pylint: disable=import-outside-toplevel
- if isinstance(cfg, six.string_types):
- parser.readfp(StringIO(cfg)) # pylint: disable=deprecated-method
- else:
- parser.readfp(cfg) # pylint: disable=deprecated-method
return parser
--
2.54.0

View File

@ -0,0 +1,102 @@
From 966e7854ec63a977656743b53b1b2811f255a006 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Thu, 16 Apr 2026 17:57:17 +0200
Subject: [PATCH 047/105] mounting: ignore failures when callling mount -a
The `mount -a` calls are implemented just as a safeguard. However, the
semantics of `nofail` as seen by `mount` are different as the ones seen
by `systemd` in that they can result in a non-zero exit code e.g. if the
block device does not contain a valid filesystem. In contrast, systemd
threads `nofail` so that the unit does not fail (even if the filesystem
is broken). This patch adds error handling that ignores non-zero exit
codes.
Jira-ref: RHEL-56176
---
.../files/dracut/85sys-upgrade-redhat/do-upgrade.sh | 8 +++++---
.../libraries/removeupgradebootentry.py | 13 ++++++++++---
.../libraries/removeupgradeefientry.py | 11 ++++++++++-
3 files changed, 25 insertions(+), 7 deletions(-)
diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
index 683131ec..41eb1625 100755
--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
+++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
@@ -236,7 +236,8 @@ do_upgrade() {
# NOTE: in case we would need to run leapp before pivot, we would need to
# specify where the root is, e.g. --root=/sysroot
# TODO: update: systemd-nspawn
- /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a; $LEAPPBIN upgrade --resume $args"
+ # shellcheck disable=SC2086 # The NSPAWN_OPTS and args variables are not quoted since they are supposed to expand into multiple arguments
+ /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a || : ; $LEAPPBIN upgrade --resume $args"
rv=$?
# NOTE: flush the cached content to disk to ensure everything is written
@@ -272,7 +273,8 @@ do_upgrade() {
# all FSTAB partitions. As mount was working before, hopefully will
# work now as well. Later this should be probably modified as we will
# need to handle more stuff around storage at all.
- /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a; /usr/bin/python3 -B $LEAPP3_BIN upgrade --resume $args"
+ # shellcheck disable=SC2086 # The NSPAWN_OPTS and args variables are not quoted since they are supposed to expand into multiple arguments
+ /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a || : ; /usr/bin/python3 -B $LEAPP3_BIN upgrade --resume $args"
rv=$?
fi
@@ -328,7 +330,7 @@ save_journal() {
# We need to run the actual saving of leapp-upgrade.log in a container and mount everything before, to be
# sure /var/log is mounted in case it is on a separate partition.
- local store_cmd="mount -a"
+ local store_cmd="mount -a || : "
local store_cmd="$store_cmd; cat /tmp-leapp-upgrade.log >> /var/log/leapp/leapp-upgrade.log"
/usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "$store_cmd"
diff --git a/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py b/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py
index 7434e48c..71be42c0 100644
--- a/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py
+++ b/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py
@@ -45,9 +45,16 @@ def remove_boot_entry():
# 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'
- ])
+ try:
+ run(['/bin/mount', '-a'])
+ except CalledProcessError as err:
+ # Every mount that is not marked with 'nofail' should be mounted when this code is executed, so mount -a can
+ # fail only on corrupted nofail devices. We ignore errors here so that our mount -a behaves consistently
+ # with systemd's mount units marked with 'nofail' -- failures of such units will not prevent the system from
+ # booting. If a mount mounts a block dev with a corrupted FS, then the system could not have used the device,
+ # so it does not contain anything important for the upgrade.
+ api.current_logger().warning('Failed to execute `mount -a` with the following error: {}.'.format(err))
+ pass
def get_upgrade_kernel_filepath():
diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py
index 7e2972e5..0e2346f0 100644
--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py
+++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py
@@ -55,7 +55,16 @@ def remove_upgrade_efi_entry():
# 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'])
+ try:
+ run(['/bin/mount', '-a'])
+ except CalledProcessError as err:
+ # Every mount that is not marked with 'nofail' should be mounted when this code is executed, so mount -a can
+ # fail only on corrupted nofail devices. We ignore errors here so that our mount -a behaves consistently
+ # with systemd's mount units marked with 'nofail' -- failures of such units will not prevent the system from
+ # booting. If a mount mounts a block dev with a corrupted FS, then the system could not have used the device,
+ # so it does not contain anything important for the upgrade.
+ api.current_logger().warning('Failed to execute `mount -a` with the following error: {}.'.format(err))
+ pass
def _remove_upgrade_blsdir(bootloader_info):
--
2.54.0

View File

@ -0,0 +1,142 @@
From 417ef75d311af2506c4ba568c43698d949479169 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Tue, 21 Apr 2026 11:30:19 +0200
Subject: [PATCH 048/105] mount_unit_generator: do not make nofail mounts
required
---
.../libraries/mount_unit_generator.py | 28 ++++++++++++++++++-
.../tests/files/unit-nofail.mount | 15 ++++++++++
.../tests/files/unit.mount | 14 ++++++++++
.../tests/test_mount_unit_generation.py | 17 +++++++++++
4 files changed, 73 insertions(+), 1 deletion(-)
create mode 100644 repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit-nofail.mount
create mode 100644 repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit.mount
diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py
index e3070986..900132f2 100644
--- a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py
+++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py
@@ -30,7 +30,7 @@ def run_systemd_fstab_generator(output_directory):
details = {'details': str(error)}
raise StopActorExecutionError(
'Failed to generate mount units using systemd-fstab-generator',
- details
+ details=details
)
api.current_logger().debug(
@@ -110,6 +110,27 @@ def prefix_all_mount_units_with_sysroot(dir_containing_units):
api.current_logger().debug('Original mount unit {} removed.'.format(unit_file_path))
+def is_unit_marked_with_nofail(unit_path: str) -> bool:
+ try:
+ with open(unit_path) as in_file:
+ lines = [line.strip() for line in in_file.readlines()]
+ except OSError:
+ api.current_logger().debug(
+ 'Could not read the unit {} to determine whether it is nofail.'.format(unit_path)
+ )
+ return False # This way, the unit will end up in target's '.requires', so it is a safe choice
+
+ for line in lines:
+ if line.startswith('Options'):
+ line_fragments = line.split('=', 1)
+ if len(line_fragments) <= 1:
+ continue # Should not happen
+ used_options = [opt.strip() for opt in line_fragments[1].split(',')]
+ if 'nofail' in used_options:
+ return True
+ return False
+
+
def _fix_symlinks_in_dir(dir_containing_mount_units, target_dir):
"""
Fix broken symlinks in given target_dir due to us modifying (renaming) the mount units.
@@ -137,11 +158,16 @@ def _fix_symlinks_in_dir(dir_containing_mount_units, target_dir):
os.mkdir(target_dir_path)
api.current_logger().debug('Populating {} with new symlinks.'.format(target_dir))
+ will_units_be_required = target_dir.endswith('.requires')
for unit_file in os.listdir(dir_containing_mount_units):
if not unit_file.endswith('.mount'):
continue
+ is_nofail = is_unit_marked_with_nofail(os.path.join(dir_containing_mount_units, unit_file))
+ if is_nofail and will_units_be_required:
+ continue
+
place_fastlink_at = os.path.join(target_dir_path, unit_file)
fastlink_points_to = os.path.join('../', unit_file)
try:
diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit-nofail.mount b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit-nofail.mount
new file mode 100644
index 00000000..b87ec340
--- /dev/null
+++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit-nofail.mount
@@ -0,0 +1,15 @@
+# Automatically generated by systemd-fstab-generator
+
+[Unit]
+Documentation=man:fstab(5) man:systemd-fstab-generator(8)
+SourcePath=/etc/fstab
+Before=local-fs.target
+Requires=systemd-fsck@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.service
+After=systemd-fsck@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.service
+After=blockdev@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.target
+
+[Mount]
+What=/dev/disk/by-uuid/1ca63aee-37b5-42b7-b1a5-f752ba09e010
+Where=/boot
+Type=ext4
+Options=nofail,noexec
diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit.mount b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit.mount
new file mode 100644
index 00000000..ad1994e3
--- /dev/null
+++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/files/unit.mount
@@ -0,0 +1,14 @@
+# Automatically generated by systemd-fstab-generator
+
+[Unit]
+Documentation=man:fstab(5) man:systemd-fstab-generator(8)
+SourcePath=/etc/fstab
+Before=local-fs.target
+Requires=systemd-fsck@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.service
+After=systemd-fsck@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.service
+After=blockdev@dev-disk-by\x2duuid-1ca63aee\x2d37b5\x2d42b7\x2db1a5\x2df752ba09e010.target
+
+[Mount]
+What=/dev/disk/by-uuid/1ca63aee-37b5-42b7-b1a5-f752ba09e010
+Where=/boot
+Type=ext4
diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py
index eb90a75d..99f26ad1 100644
--- a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py
+++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py
@@ -326,3 +326,20 @@ def test_injection_of_sysroot_boot_bindmount_unit(monkeypatch, has_separate_boot
if has_separate_boot:
assert was_copyfile_for_sysroot_boot_called
+
+
+TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files')
+
+
+@pytest.mark.parametrize(
+ ('unit_filename', 'is_nofail'),
+ [
+ ('unit.mount', False),
+ ('unit-nofail.mount', True),
+ ('non-existing-unit.mount', False), # This file does not exist in the files/
+ ]
+)
+def test_is_unit_marked_with_nofail(unit_filename, is_nofail):
+ unit_path = os.path.join(TEST_DIR, unit_filename)
+ determined_no_fail = mount_unit_generator.is_unit_marked_with_nofail(unit_path)
+ assert determined_no_fail == is_nofail
--
2.54.0

View File

@ -0,0 +1,133 @@
From d41629259c916a1205eccbafa3b03884cff5949f Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 27 Apr 2026 09:03:57 +0200
Subject: [PATCH 049/105] leapp_resume.service: use multi-user.target instead
of default.target
When selinux autorelabel is triggered, the selinux-autorelabel-generator
links default.target to selinux-autorelabel.target. During boot,
the relabel is performed and then the system is rebooted, resulting in
an intermediate reboot before the system fully starts.
Since leapp_resume.service was linked into default.target.wants/ and
ordered After=default.target, it was started during this intermediate
autorelabel boot. When the relabel finished and the reboot was
triggered, leapp_resume.service was terminated, preventing post-upgrade
tasks from completing. The service was then rerun on the next boot.
This patch makes leapp_resume.service rely on multi-user.target instead
of the default.target which is not reached during the autorelabel boot,
ensuring leapp_resume.service only runs on the actual first boot into
the upgraded OS.
Jira: RHEL-60060
---
.../common/actors/createresumeservice/actor.py | 13 ++++++++-----
.../createresumeservice/files/leapp_resume.service | 2 +-
.../tests/test_createresumeservice.py | 13 +++++--------
.../common/actors/removeresumeservice/actor.py | 13 ++++++++-----
4 files changed, 22 insertions(+), 19 deletions(-)
diff --git a/repos/system_upgrade/common/actors/createresumeservice/actor.py b/repos/system_upgrade/common/actors/createresumeservice/actor.py
index 8ac07a06..d7a0a5bc 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/actor.py
+++ b/repos/system_upgrade/common/actors/createresumeservice/actor.py
@@ -22,18 +22,21 @@ class CreateSystemdResumeService(Actor):
tags = (FinalizationPhaseTag, IPUWorkflowTag)
def process(self):
- service_name = 'leapp_resume.service'
systemd_dir = '/etc/systemd/system'
+ service_name = 'leapp_resume.service'
+ target_name = 'multi-user.target'
service_templ_fpath = self.get_file_path(service_name)
shutil.copyfile(service_templ_fpath, os.path.join(systemd_dir, service_name))
- service_path = '/etc/systemd/system/{}'.format(service_name)
- symlink_path = '/etc/systemd/system/default.target.wants/{}'.format(service_name)
+ target_wants_path = os.path.join(systemd_dir, '{}.wants'.format(target_name))
+
+ service_path = os.path.join(systemd_dir, service_name)
+ symlink_path = os.path.join(target_wants_path, service_name)
- # in case nothing is enabled in the default target, the directory does not exist
+ # in case nothing is enabled in the target, the directory does not exist
try:
- os.mkdir(os.path.join(systemd_dir, 'default.target.wants'))
+ os.mkdir(target_wants_path)
except OSError:
pass
diff --git a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
index 859237a1..749b4b84 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
+++ b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
@@ -1,6 +1,6 @@
[Unit]
Description=Temporary Leapp service which resumes execution after reboot
-After=default.target
+After=multi-user.target
DefaultDependencies=no
After=dbus.service
After=network-online.target
diff --git a/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py b/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
index c1cefc37..f2590291 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
+++ b/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
@@ -1,21 +1,18 @@
import os
-import distro
import pytest
@pytest.mark.skipif(os.getuid() != 0, reason='User is not a root')
-@pytest.mark.skipif(
- distro.id() == 'fedora',
- reason='default.target.wants does not exists on Fedora distro',
-)
def test_create_resume_service(current_actor_context):
-
current_actor_context.run()
+ systemd_dir = '/etc/systemd/system'
service_name = 'leapp_resume.service'
- service_path = '/etc/systemd/system/{}'.format(service_name)
- symlink_path = '/etc/systemd/system/default.target.wants/{}'.format(service_name)
+ target_name = 'multi-user.target'
+
+ service_path = os.path.join(systemd_dir, service_name)
+ symlink_path = os.path.join(systemd_dir, '{}.wants'.format(target_name), service_name)
try:
assert os.path.isfile(service_path)
diff --git a/repos/system_upgrade/common/actors/removeresumeservice/actor.py b/repos/system_upgrade/common/actors/removeresumeservice/actor.py
index 59935394..cf22ad07 100644
--- a/repos/system_upgrade/common/actors/removeresumeservice/actor.py
+++ b/repos/system_upgrade/common/actors/removeresumeservice/actor.py
@@ -21,13 +21,16 @@ class RemoveSystemdResumeService(Actor):
tags = (FirstBootPhaseTag.After, IPUWorkflowTag)
def process(self):
+ systemd_dir = '/etc/systemd/system'
service_name = 'leapp_resume.service'
- if os.path.isfile('/etc/systemd/system/{}'.format(service_name)):
+ target_name = 'multi-user.target'
+
+ service_path = os.path.join(systemd_dir, service_name)
+ target_wants_path = os.path.join(systemd_dir, '{}.wants'.format(target_name), service_name)
+
+ if os.path.isfile(service_path):
run(['systemctl', 'disable', service_name])
- paths_to_unlink = [
- '/etc/systemd/system/{}'.format(service_name),
- '/etc/systemd/system/default.target.wants/{}'.format(service_name),
- ]
+ paths_to_unlink = [service_path, target_wants_path]
for path in paths_to_unlink:
try:
os.unlink(path)
--
2.54.0

View File

@ -0,0 +1,334 @@
From 985cc58bb3942d5061b92c84bcfe93788e80bca6 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Wed, 22 Apr 2026 14:01:23 +0200
Subject: [PATCH 050/105] LiveMode: update do-upgrade.sh to reflect upstream
changes
- treat the existence of .leapp_upgrade_failed file the same as in the default solution. Reflects changes from 1f8b8f3.
- updates the regex used to extract OS major version so that it is able to extract version without a minor version. Reflects changes from 64ec2ec.
- ignore failures when calling mount -a, makes mount treat nofail option same as systemd. Reflects changes from 966e785.
Jira: RHEL-104384, RHEL-56040
---
.../dracut/85sys-upgrade-redhat/do-upgrade.sh | 10 +-
.../files/do-upgrade.sh | 184 ++----------------
2 files changed, 19 insertions(+), 175 deletions(-)
diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
index 41eb1625..7d470459 100755
--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
+++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
@@ -28,6 +28,7 @@ export RHEL_OS_MAJOR_RELEASE
export LEAPPBIN=/usr/bin/leapp
export LEAPPHOME=/root/tmp_leapp_py3
export LEAPP3_BIN=$LEAPPHOME/leapp3
+export LEAPP_FAILED_FLAG_FILE="/root/tmp_leapp_py3/.leapp_upgrade_failed"
export NEWROOT=${NEWROOT:-"/sysroot"}
@@ -46,7 +47,6 @@ fi
export NSPAWN_OPTS="$NSPAWN_OPTS --keep-unit --register=no --timezone=off --resolv-conf=off"
-export LEAPP_FAILED_FLAG_FILE="/root/tmp_leapp_py3/.leapp_upgrade_failed"
#
# Temp for collecting and preparing tarball
@@ -249,10 +249,6 @@ do_upgrade() {
}
if [ "$rv" -eq 0 ]; then
- # run leapp to proceed phases after the upgrade with Python3
- #PY_LEAPP_PATH=/usr/lib/python2.7/site-packages/leapp/
- #$NEWROOT/bin/systemd-nspawn $NSPAWN_OPTS -D $NEWROOT -E PYTHONPATH="${PYTHONPATH}:${PY_LEAPP_PATH}" /usr/bin/python3 $LEAPPBIN upgrade --resume $args
-
# on aarch64 systems during el8 to el9 upgrades the swap is broken due to change in page size (64K to 4k)
# adjust the page size before booting into the new system, as it is possible the swap is necessary for to boot
# `arch` command is not available in the dracut shell, using uname -m instead
@@ -287,7 +283,7 @@ do_upgrade() {
echo >&2 "Creating file $NEWROOT$LEAPP_FAILED_FLAG_FILE"
echo >&2 "Warning: Leapp upgrade failed and there is an issue blocking the upgrade."
- echo >&2 "Please file a support case with /var/log/leapp/leapp-upgrade.log attached"
+ echo >&2 "Please file a support case with /var/log/leapp/leapp-upgrade.log attached."
"$NEWROOT/bin/touch" "$NEWROOT$LEAPP_FAILED_FLAG_FILE"
fi
@@ -380,7 +376,7 @@ mount -o "remount,rw" "$NEWROOT"
)
result=$?
-##### safe the data and remount $NEWROOT as it was previously mounted #####
+##### save the data and remount $NEWROOT as it was previously mounted #####
save_journal
# NOTE: For debugging purposis. It's possible it will be changed in future.
diff --git a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
index 51ebddb5..6f2e37ef 100755
--- a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
+++ b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
@@ -7,13 +7,13 @@ warn() {
get_rhel_major_release() {
local os_version
- os_version=$(grep -o '^VERSION="[0-9][0-9]*\.' /etc/os-release | grep -o '[0-9]*')
+ os_version=$(grep -o '^VERSION="[0-9][0-9]*' /etc/os-release | grep -o '[0-9]*')
[ -z "$os_version" ] && {
# This should not happen as /etc/initrd-release is supposed to have API
# stability, but check is better than broken system.
warn "Cannot determine the major RHEL version."
warn "The upgrade environment cannot be setup reliably."
- echo "Content of the /etc/initrd-release:"
+ echo "Content of the /etc/os-release:"
cat /etc/os-release
exit 1
}
@@ -26,6 +26,7 @@ export RHEL_OS_MAJOR_RELEASE
export LEAPPBIN=/usr/bin/leapp
export LEAPPHOME=/root/tmp_leapp_py3
export LEAPP3_BIN=$LEAPPHOME/leapp3
+export LEAPP_FAILED_FLAG_FILE="/root/tmp_leapp_py3/.leapp_upgrade_failed"
# this was initially a dracut script, hence $NEWROOT.
# the rootfs is mounted on /run/upgrade when booted with dmsquash-live
@@ -48,155 +49,6 @@ fi
export NSPAWN_OPTS="$NSPAWN_OPTS --keep-unit --register=no --timezone=off --resolv-conf=off"
-export LEAPP_FAILED_FLAG_FILE="/root/tmp_leapp_py3/.leapp_upgrade_failed"
-
-#
-# Temp for collecting and preparing tarball
-#
-LEAPP_DEBUG_TMP="/tmp/leapp-debug-root"
-
-#
-# Number of times to emit all chunks
-#
-# To avoid spammy parts of console log, second and later emissions
-# take longer delay in-between. For example, with N being 3,
-# first emission is done immediately, second after 10s, and the
-# third one after 20s.
-#
-IBDMP_ITER=3
-
-#
-# Size of one payload chunk
-#
-# IOW, amount of characters in a single chunk of the base64-encoded
-# payload. (By base64 standard, these characters are inherently ASCII,
-# so ie. they correspond to bytes.)
-#
-IBDMP_CHUNKSIZE=40
-
-collect_and_dump_debug_data() {
- #
- # Collect various debug files and dump tarball using ibdmp
- #
- local tmp=$LEAPP_DEBUG_TMP
- local data=$tmp/data
- mkdir -p "$data" || { echo >&2 "fatal: cannot create leapp dump data dir: $data"; exit 4; }
- journalctl -amo verbose >"$data/journalctl.log"
- mkdir -p "$data/var/lib/leapp"
- mkdir -p "$data/var/log"
- cp -vr "$NEWROOT/var/lib/leapp/leapp.db" \
- "$data/var/lib/leapp"
- cp -vr "$NEWROOT/var/log/leapp" \
- "$data/var/log"
- tar -cJf "$tmp/data.tar.xz" "$data"
- ibdmp "$tmp/data.tar.xz"
- rm -r "$tmp"
-}
-
-want_inband_dump() {
- #
- # True if dump collection is needed given leapp exit status $1 and kernel option
- #
- local leapp_es=$1
- local mode
- local kopt
- kopt=$(getarg 'rd.upgrade.inband')
- case $kopt in
- always|never|onerror) mode="$kopt" ;;
- "") mode="never" ;;
- *) warn "ignoring unknown value of rd.upgrade.inband (dump will be disabled): '$kopt'"
- return 2 ;;
- esac
- case $mode:$leapp_es in
- always:*) return 0 ;;
- never:*) return 1 ;;
- onerror:0) return 1 ;;
- onerror:*) return 0 ;;
- esac
-}
-
-ibdmp() {
- #
- # Dump tarball $1 in base64 to stdout
- #
- # Tarball is encoded in a way that:
- #
- # * final data can be printed to plain text terminal,
- # * tarball can be restored by scanning the saved
- # terminal output,
- # * corruptions caused by extra terminal noise
- # (extra lines, extra characters within lines,
- # line splits..) can be corrected.
- #
- # That is,
- #
- # 1. encode tarball using base64
- #
- # 2. prepend line `chunks=CHUNKS,md5=MD5` where
- # MD5 is the MD5 digest of original tarball and
- # CHUNKS is number of upcoming Base64 chunks
- #
- # 3. decorate each chunk with prefix `N:` where
- # N is number of given chunk.
- #
- # 4. Finally print all lines (prepended "header"
- # line and all chunks) several times, where
- # every iteration should be prefixed by
- # `_ibdmp:I/TTL|` and suffixed by `|`.
- # where `I` is iteration number and `TTL` is
- # total iteration numbers.
- #
- # Decoder should look for strings like this:
- #
- # _ibdmp:I/J|CN:PAYLOAD|
- #
- # where I, J and CN are integers and PAYLOAD is a slice of a
- # base64 string.
- #
- # Here, I represents number of iteration, J total of iterations
- # ($IBDMP_ITER), and CN is number of given chunk within this
- # iteration. CN goes from 1 up to number of chunks (CHUNKS)
- # predicted by header.
- #
- # Each set corresponds to one dump of the tarball and error
- # correction is achieved by merging sets using these rules:
- #
- # 1. each set has to contain header (`chunks=CHUNKS,
- # md5=MD5`) prevalent header wins.
- #
- # 2. each set has to contain number of chunks
- # as per header
- #
- # 3. chunks are numbered so they can be compared across
- # sets; prevalent chunk wins.
- #
- # Finally the merged set of chunks is decoded as base64.
- # Resulting data has to match md5 sum or we're hosed.
- #
- local tarball=$1
- local tmp=$LEAPP_DEBUG_TMP/ibdmp
- local md5
- local i
- mkdir -p "$tmp"
- base64 -w "$IBDMP_CHUNKSIZE" "$tarball" > "$tmp/b64"
- md5=$(md5sum "$tarball" | sed 's/ .*//')
- chunks=$(wc -l <"$tmp/b64")
- (
- set +x
- echo "chunks=$chunks,md5=$md5"
- cnum=1
- while read -r chunk; do
- echo "$cnum:$chunk"
- ((cnum++))
- done <"$tmp/b64"
- ) >"$tmp/report"
- i=0
- while test "$i" -lt "$IBDMP_ITER"; do
- sleep "$((i * 10))"
- ((i++))
- sed "s%^%_ibdmp:$i/$IBDMP_ITER|%; s%$%|%; " <"$tmp/report"
- done
-}
do_upgrade() {
local args="" rv=0
@@ -218,7 +70,7 @@ do_upgrade() {
# NOTE: We disable shell-check since we want to word-break NSPAWN_OPTS
# shellcheck disable=SC2086
- /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a; $LEAPPBIN upgrade --resume $args"
+ /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a || : ; $LEAPPBIN upgrade --resume $args"
rv=$?
# NOTE: flush the cached content to disk to ensure everything is written
@@ -227,10 +79,6 @@ do_upgrade() {
## TODO: implement "Break after LEAPP upgrade stop"
if [ "$rv" -eq 0 ]; then
- # run leapp to proceed phases after the upgrade with Python3
- #PY_LEAPP_PATH=/usr/lib/python2.7/site-packages/leapp/
- #$NEWROOT/bin/systemd-nspawn $NSPAWN_OPTS -D $NEWROOT -E PYTHONPATH="${PYTHONPATH}:${PY_LEAPP_PATH}" /usr/bin/python3 $LEAPPBIN upgrade --resume $args
-
# on aarch64 systems during el8 to el9 upgrades the swap is broken due to change in page size (64K to 4k)
# adjust the page size before booting into the new system, as it is possible the swap is necessary for to boot
# `arch` command is not available in the dracut shell, using uname -m instead
@@ -255,7 +103,7 @@ do_upgrade() {
# NOTE: We disable shell-check since we want to word-break NSPAWN_OPTS
# shellcheck disable=SC2086
- /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a; /usr/bin/python3 -B $LEAPP3_BIN upgrade --resume $args"
+ /usr/bin/systemd-nspawn $NSPAWN_OPTS -D "$NEWROOT" /usr/bin/bash -c "mount -a || : ; /usr/bin/python3 -B $LEAPP3_BIN upgrade --resume $args"
rv=$?
fi
@@ -265,14 +113,14 @@ do_upgrade() {
local dirname
dirname="$("$NEWROOT/bin/dirname" "$NEWROOT$LEAPP_FAILED_FLAG_FILE")"
[ -d "$dirname" ] || mkdir "$dirname"
+
+ echo >&2 "Creating file $NEWROOT$LEAPP_FAILED_FLAG_FILE"
+ echo >&2 "Warning: Leapp upgrade failed and there is an issue blocking the upgrade."
+ echo >&2 "Please file a support case with /var/log/leapp/leapp-upgrade.log attached."
+
"$NEWROOT/bin/touch" "$NEWROOT$LEAPP_FAILED_FLAG_FILE"
fi
- # Dump debug data in case something went wrong
- ##if want_inband_dump "$rv"; then
- ## collect_and_dump_debug_data
- ##fi
-
# NOTE: THIS SHOULD BE AGAIN PART OF LEAPP IDEALLY
## backup old product id certificates
#chroot $NEWROOT /bin/sh -c 'mkdir /etc/pki/product_old; mv -f /etc/pki/product/*.pem /etc/pki/product_old/'
@@ -306,7 +154,7 @@ save_journal() {
# We need to run the actual saving of leapp-upgrade.log in a container and mount everything before, to be
# sure /var/log is mounted in case it is on a separate partition.
- local store_cmd="mount -a"
+ local store_cmd="mount -a || : "
local store_cmd="$store_cmd; cat /tmp-leapp-upgrade.log >> /var/log/leapp/leapp-upgrade.log"
# NOTE: We disable shell-check since we want to word-break NSPAWN_OPTS
@@ -333,10 +181,10 @@ awk '{print $1}' /proc/cmdline \
# check if leapp previously failed in the initramfs, if it did return to the emergency shell
[ -f "$NEWROOT$LEAPP_FAILED_FLAG_FILE" ] && {
echo >&2 "Found file $NEWROOT$LEAPP_FAILED_FLAG_FILE"
- echo >&2 "Error: Leapp previously failed and cannot continue, returning back to emergency shell"
- echo >&2 "Please file a support case with $NEWROOT/var/log/leapp/leapp-upgrade.log attached"
- echo >&2 "To rerun the upgrade upon exiting the dracut shell remove the $NEWROOT$LEAPP_FAILED_FLAG_FILE file"
- exit 1
+ echo >&2 "Warning: Leapp failed on a previous execution and something might be blocking the upgrade."
+ echo >&2 "Continuing with the upgrade anyway. Note that any subsequent error might be potentially misleading due to a previous failure."
+ echo >&2 "A log file will be generated at $NEWROOT/var/log/leapp/leapp-upgrade.log."
+ echo >&2 "In case of persisting failure, if possible, try to boot to the original system and file a support case with /var/log/leapp/leapp-upgrade.log attached."
}
[ ! -x "$NEWROOT$LEAPPBIN" ] && {
@@ -348,7 +196,7 @@ awk '{print $1}' /proc/cmdline \
)
result=$?
-##### safe the data #####
+##### save the data #####
save_journal
# NOTE: flush the cached content to disk to ensure everything is written
--
2.54.0

View File

@ -0,0 +1,43 @@
From 72ec8a691678f5097f6fc9290c5ae1a6b030dac9 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Thu, 30 Apr 2026 11:53:26 +0200
Subject: [PATCH 051/105] LiveMode: improve do-upgrade.sh output when .noreboot
file exists
The output of do-upgrade.sh suggested that a reboot is going to happen
even though it wasn't as .noreboot file stopped it. This patch makes
sure the output is not misleading users.
---
.../modify_userspace_for_livemode/files/do-upgrade.sh | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
index 6f2e37ef..b6154573 100755
--- a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
+++ b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/files/do-upgrade.sh
@@ -137,7 +137,7 @@ do_upgrade() {
save_journal() {
# Q: would it be possible that journal will not be flushed completely yet?
- echo "writing logs to disk and rebooting"
+ echo "writing logs to disk"
local logfile="/sysroot/tmp-leapp-upgrade.log"
@@ -213,7 +213,12 @@ sync
#Failed to talk to init daemon: Host is down
#"""
if [ "$result" == "0" ]; then
- [ -f "${NEWROOT}/.noreboot" ] || reboot
+ if [ -f "${NEWROOT}/.noreboot" ]; then
+ echo "Reboot suppressed by ${NEWROOT}/.noreboot"
+ else
+ echo "Rebooting..."
+ reboot
+ fi
else
echo >&2 "The upgrade container returned a non-zero exit code."
exit $result
--
2.54.0

View File

@ -0,0 +1,47 @@
From a9aa8a8037a1705a9f967d5f62f6b2db428bf8fc Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Mon, 16 Mar 2026 19:05:54 -0400
Subject: [PATCH 052/105] multipathutil: handle the overrides:protocol
subsection
Starting in rhel-8, multipath.conf supports a protocol subection of the
overrides configuration section. Accept this when parsing the config,
and add a test for it.
Jira: RHEL-151509
---
repos/system_upgrade/common/libraries/multipathutil.py | 3 ++-
.../common/libraries/tests/test_multipathutil.py | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/multipathutil.py b/repos/system_upgrade/common/libraries/multipathutil.py
index cb5c3693..78ed5f13 100644
--- a/repos/system_upgrade/common/libraries/multipathutil.py
+++ b/repos/system_upgrade/common/libraries/multipathutil.py
@@ -7,7 +7,8 @@ _sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
'overrides', 'multipaths')
_subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
- 'devices': 'device', 'multipaths': 'multipath'}
+ 'devices': 'device', 'multipaths': 'multipath',
+ 'overrides': 'protocol'}
def read_config(path):
diff --git a/repos/system_upgrade/common/libraries/tests/test_multipathutil.py b/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
index 3ddfcddf..852f3463 100644
--- a/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
+++ b/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
@@ -38,7 +38,8 @@ def test_section_start():
sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
'overrides', 'multipaths')
subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
- 'devices': 'device', 'multipaths': 'multipath'}
+ 'devices': 'device', 'multipaths': 'multipath',
+ 'overrides': 'protocol'}
for section in sections:
data = lib.LineData(section + ' {', None, False)
assert data.type == data.TYPE_SECTION_START
--
2.54.0

View File

@ -0,0 +1,324 @@
From 71431ef1404f2eb817a37a9574e9fbcf55d6d367 Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Tue, 17 Mar 2026 13:53:47 -0400
Subject: [PATCH 053/105] multipath: move config_reader Actor and 8to9 Models
to el8toel9 repo
The config_reader actor is mostly decidated to checking for issues that
can happen during an el8 to el9 upgrade and produces
MultipathConfFacts8to9 messages. The MultipathConfig8to9 and
MultipathConfFacts8to9 models are exclusively for el8 to el9 upgrades.
All of these should live in the el8toel9 repo.
Jira: RHEL-151509
---
.../system_upgrade/common/models/multipath.py | 42 -------------------
.../actors/multipathconfread}/actor.py | 10 ++---
.../libraries/multipathconfread.py | 11 ++---
.../tests/files/all_the_things.conf | 0
.../tests/files/allow_usb.conf | 0
.../tests/files/complicated.conf | 0
.../tests/files/conf1.d/empty.conf | 0
.../files/conf1.d/nothing_important.conf | 0
.../tests/files/conf2.d/all_true.conf | 0
.../tests/files/conf3.d/README | 0
.../tests/files/converted_the_things.conf | 0
.../tests/files/default_rhel8.conf | 0
.../multipathconfread}/tests/files/empty.conf | 0
.../tests/files/empty_dir.conf | 0
.../tests/files/missing_dir.conf | 0
.../tests/files/no_defaults.conf | 0
.../tests/files/no_foreign.conf | 0
.../tests/files/not_set_dir.conf | 0
.../tests/files/set_in_dir.conf | 0
.../tests/files/two_defaults.conf | 0
.../tests/test_multipath_conf_read_8to9.py | 30 -------------
.../el8toel9/models/multipath8to9.py | 34 +++++++++++++++
22 files changed, 42 insertions(+), 85 deletions(-)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/actor.py (68%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/libraries/multipathconfread.py (88%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/all_the_things.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/allow_usb.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/complicated.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/conf1.d/empty.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/conf1.d/nothing_important.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/conf2.d/all_true.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/conf3.d/README (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/converted_the_things.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/default_rhel8.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/empty.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/empty_dir.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/missing_dir.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/no_defaults.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/no_foreign.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/not_set_dir.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/set_in_dir.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/files/two_defaults.conf (100%)
rename repos/system_upgrade/{common/actors/multipath/config_reader => el8toel9/actors/multipathconfread}/tests/test_multipath_conf_read_8to9.py (83%)
create mode 100644 repos/system_upgrade/el8toel9/models/multipath8to9.py
diff --git a/repos/system_upgrade/common/models/multipath.py b/repos/system_upgrade/common/models/multipath.py
index 1d1c53b5..9ccaa290 100644
--- a/repos/system_upgrade/common/models/multipath.py
+++ b/repos/system_upgrade/common/models/multipath.py
@@ -34,45 +34,3 @@ class MultipathConfigUpdatesInfo(Model):
updates = fields.List(fields.Model(UpdatedMultipathConfig), default=[])
""" Collection of multipath config updates that must be performed during the upgrade. """
-
-
-class MultipathConfig8to9(Model):
- """
- Model information about multipath configuration file important for the 8>9 upgrade path.
-
- Note: This model is in the common repository due to the technical reasons
- (reusing parser code in a single actor), and it should not be emitted on
- non-8to9 upgrade paths. In the future, this model will likely be moved into
- el8toel9 repository.
- """
- topic = SystemInfoTopic
-
- pathname = fields.String()
- """Config file path name"""
-
- config_dir = fields.Nullable(fields.String())
- """Value of config_dir in the defaults section. None if not set"""
-
- enable_foreign_exists = fields.Boolean(default=False)
- """True if enable_foreign is set in the defaults section"""
-
- invalid_regexes_exist = fields.Boolean(default=False)
- """True if any regular expressions have the value of "*" """
-
- allow_usb_exists = fields.Boolean(default=False)
- """True if allow_usb_devices is set in the defaults section."""
-
-
-class MultipathConfFacts8to9(Model):
- """
- Model representing information from multipath configuration files important for the 8>9 upgrade path.
-
- Note: This model is in the common repository due to the technical reasons
- (reusing parser code in a single actor), and it should not be emitted on
- non-8to9 upgrade paths. In the future, this model will likely be moved into
- el8toel9 repository.
- """
- topic = SystemInfoTopic
-
- configs = fields.List(fields.Model(MultipathConfig8to9), default=[])
- """List of multipath configuration files"""
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/actor.py b/repos/system_upgrade/el8toel9/actors/multipathconfread/actor.py
similarity index 68%
rename from repos/system_upgrade/common/actors/multipath/config_reader/actor.py
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/actor.py
index a7238a25..2e326614 100644
--- a/repos/system_upgrade/common/actors/multipath/config_reader/actor.py
+++ b/repos/system_upgrade/el8toel9/actors/multipathconfread/actor.py
@@ -4,7 +4,7 @@ from leapp.models import DistributionSignedRPM, MultipathConfFacts8to9, Multipat
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
-class MultipathConfRead(Actor):
+class MultipathConfRead8to9(Actor):
"""
Read multipath configuration files and extract the necessary information
@@ -13,13 +13,11 @@ class MultipathConfRead(Actor):
- /etc/multipath/ - any files inside the directory
- /etc/xdrdevices.conf
- Two kinds of messages are generated:
- - MultipathInfo - general information about multipath, version agnostic
- - upgrade-path-specific messages such as MultipathConfFacts8to9 (produced only
- when upgrading from 8 to 9)
+ Produces MultipathInfo with general information about multipath, and
+ MultipathConfFacts8to9 with details needed for the 8 to 9 upgrade.
"""
- name = 'multipath_conf_read'
+ name = 'multipath_conf_read_8to9'
consumes = (DistributionSignedRPM,)
produces = (MultipathInfo, MultipathConfFacts8to9)
tags = (FactsPhaseTag, IPUWorkflowTag)
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/libraries/multipathconfread.py b/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
similarity index 88%
rename from repos/system_upgrade/common/actors/multipath/config_reader/libraries/multipathconfread.py
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
index e733500b..f2f39293 100644
--- a/repos/system_upgrade/common/actors/multipath/config_reader/libraries/multipathconfread.py
+++ b/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
@@ -2,7 +2,6 @@ import errno
import os
from leapp.libraries.common import multipathutil
-from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import DistributionSignedRPM, MultipathConfFacts8to9, MultipathConfig8to9, MultipathInfo
@@ -103,10 +102,8 @@ def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
)
api.produce(multipath_info)
- # Handle upgrade-path-specific config actions
- if get_source_major_version() == '8':
- secondary_configs = _parse_config_dir(multipath_info.config_dir)
- all_configs = [primary_config] + secondary_configs
+ secondary_configs = _parse_config_dir(multipath_info.config_dir)
+ all_configs = [primary_config] + secondary_configs
- config_facts_for_8to9 = MultipathConfFacts8to9(configs=all_configs)
- api.produce(config_facts_for_8to9)
+ config_facts_for_8to9 = MultipathConfFacts8to9(configs=all_configs)
+ api.produce(config_facts_for_8to9)
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/all_the_things.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/all_the_things.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/all_the_things.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/all_the_things.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/allow_usb.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/allow_usb.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/allow_usb.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/allow_usb.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/complicated.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/complicated.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/complicated.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/complicated.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf1.d/empty.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf1.d/empty.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf1.d/empty.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf1.d/empty.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf1.d/nothing_important.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf1.d/nothing_important.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf1.d/nothing_important.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf1.d/nothing_important.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf2.d/all_true.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf2.d/all_true.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf2.d/all_true.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf2.d/all_true.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf3.d/README b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf3.d/README
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/conf3.d/README
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/conf3.d/README
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/converted_the_things.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/converted_the_things.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/converted_the_things.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/converted_the_things.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/default_rhel8.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/default_rhel8.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/default_rhel8.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/default_rhel8.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/empty.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/empty.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/empty.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/empty.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/empty_dir.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/empty_dir.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/empty_dir.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/empty_dir.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/missing_dir.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/missing_dir.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/missing_dir.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/missing_dir.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/no_defaults.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/no_defaults.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/no_defaults.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/no_defaults.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/no_foreign.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/no_foreign.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/no_foreign.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/no_foreign.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/not_set_dir.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/not_set_dir.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/not_set_dir.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/not_set_dir.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/set_in_dir.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/set_in_dir.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/set_in_dir.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/set_in_dir.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/files/two_defaults.conf b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/two_defaults.conf
similarity index 100%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/files/two_defaults.conf
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/files/two_defaults.conf
diff --git a/repos/system_upgrade/common/actors/multipath/config_reader/tests/test_multipath_conf_read_8to9.py b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
similarity index 83%
rename from repos/system_upgrade/common/actors/multipath/config_reader/tests/test_multipath_conf_read_8to9.py
rename to repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
index e593a857..2da74927 100644
--- a/repos/system_upgrade/common/actors/multipath/config_reader/tests/test_multipath_conf_read_8to9.py
+++ b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
@@ -142,33 +142,3 @@ def test_get_facts_missing_dir(monkeypatch, primary_config, expected_configs):
for actual_config, expected_config in zip(actual_configs, expected_configs):
assert_config(actual_config, expected_config)
-
-
-def test_only_general_info_is_produced_on_9to10(monkeypatch):
- default_config_path = '/etc/multipath.conf'
-
- def parse_config_mock(path):
- assert path == default_config_path
- return MultipathConfig8to9(pathname=path)
-
- monkeypatch.setattr(multipathconfread, '_parse_config', parse_config_mock)
- monkeypatch.setattr(multipathconfread, 'is_processable', lambda: True)
-
- produce_mock = produce_mocked()
- monkeypatch.setattr(api, 'produce', produce_mock)
-
- actor_mock = CurrentActorMocked(src_ver='9.6', dst_ver='10.0')
- monkeypatch.setattr(api, 'current_actor', actor_mock)
-
- multipathconfread.scan_and_emit_multipath_info(default_config_path)
-
- assert produce_mock.called
-
- general_info_msgs = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathInfo)]
- assert len(general_info_msgs) == 1
- general_info = general_info_msgs[0]
- assert general_info.is_configured
- assert general_info.config_dir == '/etc/multipath/conf.d'
-
- msgs = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathConfFacts8to9)]
- assert not msgs
diff --git a/repos/system_upgrade/el8toel9/models/multipath8to9.py b/repos/system_upgrade/el8toel9/models/multipath8to9.py
new file mode 100644
index 00000000..a02f45db
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/models/multipath8to9.py
@@ -0,0 +1,34 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class MultipathConfig8to9(Model):
+ """
+ Model information about multipath configuration file important for the 8>9 upgrade path.
+ """
+ topic = SystemInfoTopic
+
+ pathname = fields.String()
+ """Config file path name"""
+
+ config_dir = fields.Nullable(fields.String())
+ """Value of config_dir in the defaults section. None if not set"""
+
+ enable_foreign_exists = fields.Boolean(default=False)
+ """True if enable_foreign is set in the defaults section"""
+
+ invalid_regexes_exist = fields.Boolean(default=False)
+ """True if any regular expressions have the value of "*" """
+
+ allow_usb_exists = fields.Boolean(default=False)
+ """True if allow_usb_devices is set in the defaults section."""
+
+
+class MultipathConfFacts8to9(Model):
+ """
+ Model representing information from multipath configuration files important for the 8>9 upgrade path.
+ """
+ topic = SystemInfoTopic
+
+ configs = fields.List(fields.Model(MultipathConfig8to9), default=[])
+ """List of multipath configuration files"""
--
2.54.0

View File

@ -0,0 +1,83 @@
From b1bf3ce6175e007f304443830a49b641fe0aa96f Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Wed, 18 Mar 2026 16:38:43 -0400
Subject: [PATCH 054/105] multipath: Add MultipathConfig9to10 and
MultipathConfFacts9to10
Add el9toel10 models to track the necessary configuration changes for
the upgrade from RHEL-9 to RHEL-10
Jira: RHEL-151509
---
.../el9toel10/models/multipath9to10.py | 59 +++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 repos/system_upgrade/el9toel10/models/multipath9to10.py
diff --git a/repos/system_upgrade/el9toel10/models/multipath9to10.py b/repos/system_upgrade/el9toel10/models/multipath9to10.py
new file mode 100644
index 00000000..19ef24cf
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/models/multipath9to10.py
@@ -0,0 +1,59 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class MultipathConfig9to10(Model):
+ """
+ Model information about multipath configuration file important for the 9>10 upgrade path.
+ """
+ topic = SystemInfoTopic
+
+ pathname = fields.String()
+ """Config file path name"""
+
+ config_dir = fields.Nullable(fields.String())
+ """
+ Value of config_dir in the defaults section. None if not set.
+ Used both to track config lines that need commenting out and
+ to determine the actual directory location.
+ """
+
+ bindings_file = fields.Nullable(fields.String())
+ """
+ Value of bindings_file in the defaults section. None if not set.
+ Used both to track config lines that need commenting out and
+ to determine the actual file location for copying.
+ """
+
+ wwids_file = fields.Nullable(fields.String())
+ """
+ Value of wwids_file in the defaults section. None if not set.
+ Used both to track config lines that need commenting out and
+ to determine the actual file location for copying.
+ """
+
+ prkeys_file = fields.Nullable(fields.String())
+ """
+ Value of prkeys_file in the defaults section. None if not set.
+ Used both to track config lines that need commenting out and
+ to determine the actual file location for copying.
+ """
+
+ has_socket_activation = fields.Boolean(default=True)
+ """True if multipathd socket activation is enabled"""
+
+ has_dm_nvme_multipathing = fields.Boolean(default=False)
+ """True if DM NVMe multipathing is enabled"""
+
+ has_getuid = fields.Boolean(default=False)
+ """True if the getuid option is set anywhere in the multipath config"""
+
+
+class MultipathConfFacts9to10(Model):
+ """
+ Model representing information from multipath configuration files important for the 9>10 upgrade path.
+ """
+ topic = SystemInfoTopic
+
+ configs = fields.List(fields.Model(MultipathConfig9to10), default=[])
+ """List of multipath configuration files"""
--
2.54.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,522 @@
From 29de17c95fe77ebd4da83f55d12ef294ecd29544 Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Tue, 24 Mar 2026 17:31:26 -0400
Subject: [PATCH 056/105] multipath: Add MultipathConfCheck9to10
mpath_conf_check actor
Add the el9toel10 mpath_conf_check actor that consumes
MultipathConfFacts9to10 and reports on multipath configuration changes
needed for the RHEL 9 to RHEL 10 upgrade: deprecated config_dir,
deprecated file paths (bindings/wwids/prkeys), socket activation
disabled by default, unsupported DM NVMe multipathing, and
getuid_callout as an upgrade inhibitor.
Also, if config_dir is not /etc/multipath/conf.d, the upgrade will be
inhibited if /etc/multipath/conf.d already exists and has config files
in it. Otherwise they would become part of the new config, after the
upgrade.
Jira: RHEL-151509
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
.../multipath/mpath_conf_check/actor.py | 25 ++
.../libraries/mpath_conf_check.py | 188 +++++++++++++
.../tests/test_multipath_conf_check_9to10.py | 258 ++++++++++++++++++
3 files changed, 471 insertions(+)
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/actor.py
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/tests/test_multipath_conf_check_9to10.py
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/actor.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/actor.py
new file mode 100644
index 00000000..20feb767
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/actor.py
@@ -0,0 +1,25 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import mpath_conf_check
+from leapp.models import MultipathConfFacts9to10
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class MultipathConfCheck9to10(Actor):
+ """
+ Checks if changes to the multipath configuration files are necessary
+ for upgrading to RHEL10, and reports the results.
+ """
+
+ name = 'multipath_conf_check_9to10'
+ consumes = (MultipathConfFacts9to10,)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ facts = next(self.consume(MultipathConfFacts9to10), None)
+ if facts is None:
+ self.log.debug('Skipping execution. No MultipathConfFacts9to10 has '
+ 'been produced')
+ return
+ mpath_conf_check.check_configs(facts)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
new file mode 100644
index 00000000..49aefc5a
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
@@ -0,0 +1,188 @@
+import os
+
+from leapp import reporting
+from leapp.reporting import create_report
+
+_DEFAULT_CONFIG_DIR = '/etc/multipath/conf.d'
+_DEFAULT_BINDINGS_FILE = '/etc/multipath/bindings'
+_DEFAULT_WWIDS_FILE = '/etc/multipath/wwids'
+_DEFAULT_PRKEYS_FILE = '/etc/multipath/prkeys'
+
+
+def _default_config_dir_has_conf_files():
+ if not os.path.exists(_DEFAULT_CONFIG_DIR):
+ return False
+ for filename in os.listdir(_DEFAULT_CONFIG_DIR):
+ if filename.endswith('.conf'):
+ return True
+ return False
+
+
+def _report_config_dir(config_dir):
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath custom config_dir is deprecated'
+ ),
+ reporting.Summary(
+ 'The multipath configuration option "config_dir" is set to '
+ '"{cfg_dir}". In RHEL-10, this option is deprecated and unused. '
+ 'The only valid configuration directory is "{def_cfg_dir}". Any '
+ 'configuration files in "{cfg_dir}" will be moved to '
+ '"{def_cfg_dir}".'.format(
+ cfg_dir=config_dir, def_cfg_dir=_DEFAULT_CONFIG_DIR)),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.SERVICES]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def _report_config_dir_conflict(config_dir):
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath config_dir conflict'
+ ),
+ reporting.Summary(
+ 'The multipath configuration option "config_dir" is set to '
+ '"{cfg_dir}". During the upgrade, configuration files from '
+ '"{cfg_dir}" will be moved to "{def_cfg_dir}". However, '
+ '"{def_cfg_dir}" already contains .conf files. These existing '
+ 'files would be added to the multipath configuration after the '
+ 'upgrade. Please remove or relocate the files in "{def_cfg_dir}" '
+ 'before upgrading.'.format(
+ cfg_dir=config_dir, def_cfg_dir=_DEFAULT_CONFIG_DIR)),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def _report_files(file_list):
+ details = ', '.join(
+ '{} (currently "{}") will be moved to "{}"'.format(name, current, default)
+ for name, current, default in file_list
+ )
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath configuration files will be moved'
+ ),
+ reporting.Summary(
+ 'The following multipath configuration file locations are '
+ 'deprecated and unused in RHEL-10. The files will be moved '
+ 'to their default locations: {}.'.format(details)),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.SERVICES]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def _report_socket_activation():
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath socket activation is disabled by default'
+ ),
+ reporting.Summary(
+ 'In RHEL-10, multipathd socket activation is disabled by '
+ 'default. If you wish to re-enable it, uncomment '
+ '"WantedBy=sockets.target" in '
+ '/lib/systemd/system/multipathd.socket'),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.SERVICES]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def _report_dm_nvme_multipathing():
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath NVMe multipathing is no longer supported'
+ ),
+ reporting.Summary(
+ 'Only Native NVMe multipathing is supported in RHEL-10. Any '
+ 'multipath NVMe devices will still work, but they will no '
+ 'longer be managed by dm-multipath.'),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.SERVICES]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def _create_paths_str(paths):
+ if len(paths) < 2:
+ return paths[0]
+ return '{} and {}'.format(', '.join(paths[0:-1]), paths[-1])
+
+
+def _report_getuid(paths):
+ paths_str = _create_paths_str(paths)
+ create_report([
+ reporting.Title(
+ 'device-mapper-multipath configuration contains getuid_callout'
+ ),
+ reporting.Summary(
+ 'The "getuid_callout" option is no longer supported in '
+ 'RHEL-10. It must be removed from the multipath '
+ 'configuration before upgrading. The option was found in '
+ '{}.'.format(paths_str)),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.RelatedResource('package', 'device-mapper-multipath')
+ ])
+
+
+def check_configs(facts):
+ if not facts.configs:
+ return
+
+ primary = facts.configs[0]
+
+ # config_dir: only valid in primary config
+ config_dir = primary.config_dir
+ if (
+ config_dir is not None and
+ os.path.normpath(config_dir) != _DEFAULT_CONFIG_DIR
+ ):
+ _report_config_dir(config_dir)
+ if _default_config_dir_has_conf_files():
+ _report_config_dir_conflict(config_dir)
+
+ # bindings_file, wwids_file, prkeys_file: last non-None value wins
+ bindings_file = None
+ wwids_file = None
+ prkeys_file = None
+ for conf in facts.configs:
+ if conf.bindings_file is not None:
+ bindings_file = conf.bindings_file
+ if conf.wwids_file is not None:
+ wwids_file = conf.wwids_file
+ if conf.prkeys_file is not None:
+ prkeys_file = conf.prkeys_file
+
+ file_list = []
+ if (
+ bindings_file is not None and
+ os.path.normpath(bindings_file) != _DEFAULT_BINDINGS_FILE
+ ):
+ file_list.append(('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE))
+ if (
+ wwids_file is not None and
+ os.path.normpath(wwids_file) != _DEFAULT_WWIDS_FILE
+ ):
+ file_list.append(('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE))
+ if (
+ prkeys_file is not None and
+ os.path.normpath(prkeys_file) != _DEFAULT_PRKEYS_FILE
+ ):
+ file_list.append(('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE))
+ if file_list:
+ _report_files(file_list)
+
+ # socket activation and dm nvme multipathing: system-level, primary only
+ if primary.has_socket_activation:
+ _report_socket_activation()
+ if primary.has_dm_nvme_multipathing:
+ _report_dm_nvme_multipathing()
+
+ # getuid: per-file, report if any config has it
+ getuid_paths = [conf.pathname for conf in facts.configs if conf.has_getuid]
+ if getuid_paths:
+ _report_getuid(getuid_paths)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/tests/test_multipath_conf_check_9to10.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/tests/test_multipath_conf_check_9to10.py
new file mode 100644
index 00000000..bb03e5dd
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/tests/test_multipath_conf_check_9to10.py
@@ -0,0 +1,258 @@
+from leapp.libraries.actor import mpath_conf_check
+from leapp.models import MultipathConfFacts9to10, MultipathConfig9to10
+from leapp.reporting import Report
+
+
+def _assert_config_dir_report(report):
+ assert report['title'] == \
+ 'device-mapper-multipath custom config_dir is deprecated'
+ assert report['severity'] == 'info'
+
+
+def _assert_config_dir_conflict_report(report):
+ assert report['title'] == \
+ 'device-mapper-multipath config_dir conflict'
+ assert report['severity'] == 'high'
+
+
+def _assert_files_report(report):
+ assert report['title'] == \
+ 'device-mapper-multipath configuration files will be moved'
+ assert report['severity'] == 'info'
+
+
+def _assert_socket_activation_report(report):
+ assert report['title'] == \
+ 'device-mapper-multipath socket activation is disabled by default'
+ assert report['severity'] == 'info'
+
+
+def _assert_dm_nvme_report(report):
+ assert report['title'] == \
+ 'device-mapper-multipath NVMe multipathing is no longer supported'
+ assert report['severity'] == 'info'
+
+
+def _assert_getuid_report(report, paths_str):
+ assert report['title'] == \
+ 'device-mapper-multipath configuration contains getuid_callout'
+ assert report['severity'] == 'high'
+ assert paths_str in report['summary']
+
+
+def _build_config(pathname, config_dir=None, bindings_file=None,
+ wwids_file=None, prkeys_file=None,
+ has_socket_activation=False, has_dm_nvme_multipathing=False,
+ has_getuid=False):
+ return MultipathConfig9to10(
+ pathname=pathname,
+ config_dir=config_dir,
+ bindings_file=bindings_file,
+ wwids_file=wwids_file,
+ prkeys_file=prkeys_file,
+ has_socket_activation=has_socket_activation,
+ has_dm_nvme_multipathing=has_dm_nvme_multipathing,
+ has_getuid=has_getuid,
+ )
+
+
+def _build_facts(confs):
+ return MultipathConfFacts9to10(configs=confs)
+
+
+def test_no_issues(current_actor_context):
+ config = _build_config('no_issues.conf')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert not reports
+
+
+def test_all_issues(current_actor_context, monkeypatch):
+ monkeypatch.setattr(mpath_conf_check, '_default_config_dir_has_conf_files', lambda: False)
+ config = _build_config(
+ 'all_issues.conf',
+ config_dir='/etc/multipath/foo.d',
+ bindings_file='/tmp/bindings',
+ has_socket_activation=True,
+ has_dm_nvme_multipathing=True,
+ has_getuid=True)
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 5
+ _assert_config_dir_report(reports[0].report)
+ _assert_files_report(reports[1].report)
+ _assert_socket_activation_report(reports[2].report)
+ _assert_dm_nvme_report(reports[3].report)
+ _assert_getuid_report(reports[4].report, 'all_issues.conf')
+
+
+def test_config_dir_default(current_actor_context):
+ config = _build_config('default_dir.conf',
+ config_dir='/etc/multipath/conf.d')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert not reports
+
+
+def test_config_dir_nondefault(current_actor_context, monkeypatch):
+ monkeypatch.setattr(mpath_conf_check, '_default_config_dir_has_conf_files', lambda: False)
+ config = _build_config('custom_dir.conf',
+ config_dir='/etc/multipath/foo.d')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_config_dir_report(reports[0].report)
+
+
+def test_files_nondefault(current_actor_context):
+ config = _build_config('custom_files.conf',
+ bindings_file='/tmp/bindings',
+ wwids_file='/tmp/wwids')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_files_report(reports[0].report)
+ assert '/tmp/bindings' in reports[0].report['summary']
+ assert '/tmp/wwids' in reports[0].report['summary']
+
+
+def test_files_overridden_by_secondary(current_actor_context):
+ primary = _build_config('primary.conf',
+ bindings_file='/tmp/bindings')
+ secondary = _build_config('secondary.conf',
+ bindings_file='/etc/multipath/bindings')
+ facts = _build_facts([primary, secondary])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert not reports
+
+
+def test_files_secondary_overrides_to_nondefault(current_actor_context):
+ primary = _build_config('primary.conf')
+ secondary = _build_config('secondary.conf',
+ bindings_file='/tmp/bindings')
+ facts = _build_facts([primary, secondary])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_files_report(reports[0].report)
+
+
+def test_socket_activation(current_actor_context):
+ config = _build_config('socket.conf', has_socket_activation=True)
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_socket_activation_report(reports[0].report)
+
+
+def test_no_socket_activation(current_actor_context):
+ config = _build_config('no_socket.conf', has_socket_activation=False)
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert not reports
+
+
+def test_dm_nvme(current_actor_context):
+ config = _build_config('nvme.conf', has_dm_nvme_multipathing=True)
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_dm_nvme_report(reports[0].report)
+
+
+def test_getuid_inhibitor(current_actor_context):
+ config = _build_config('getuid.conf', has_getuid=True)
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_getuid_report(reports[0].report, 'getuid.conf')
+
+
+def test_getuid_in_secondary(current_actor_context):
+ primary = _build_config('primary.conf')
+ secondary = _build_config('secondary.conf', has_getuid=True)
+ facts = _build_facts([primary, secondary])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_getuid_report(reports[0].report, 'secondary.conf')
+
+
+def test_multiple_secondaries(current_actor_context):
+ # primary: non-default bindings, no getuid
+ primary = _build_config('primary.conf',
+ bindings_file='/tmp/bindings')
+ # second1: overrides bindings back to default, sets non-default wwids,
+ # has getuid
+ second1 = _build_config('second1.conf',
+ bindings_file='/etc/multipath/bindings',
+ wwids_file='/tmp/wwids',
+ has_getuid=True)
+ # second2: overrides wwids back to default, sets non-default prkeys,
+ # no getuid
+ second2 = _build_config('second2.conf',
+ wwids_file='/etc/multipath/wwids',
+ prkeys_file='/tmp/prkeys')
+ # second3: has getuid only
+ second3 = _build_config('second3.conf', has_getuid=True)
+ facts = _build_facts([primary, second1, second2, second3])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ # Expect: files report (only prkeys non-default) + getuid inhibitor
+ assert reports and len(reports) == 2
+ _assert_files_report(reports[0].report)
+ # bindings was overridden to default, wwids was overridden to default,
+ # only prkeys should appear
+ assert '/tmp/prkeys' in reports[0].report['summary']
+ assert '/tmp/bindings' not in reports[0].report['summary']
+ assert '/tmp/wwids' not in reports[0].report['summary']
+ # getuid found in second1 and second3
+ _assert_getuid_report(reports[1].report, 'second1.conf and second3.conf')
+
+
+def test_config_dir_conflict_inhibitor(current_actor_context, monkeypatch):
+ monkeypatch.setattr(mpath_conf_check, '_default_config_dir_has_conf_files', lambda: True)
+ config = _build_config('custom_dir.conf',
+ config_dir='/etc/multipath/foo.d')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 2
+ _assert_config_dir_report(reports[0].report)
+ _assert_config_dir_conflict_report(reports[1].report)
+
+
+def test_config_dir_no_conflict(current_actor_context, monkeypatch):
+ monkeypatch.setattr(mpath_conf_check, '_default_config_dir_has_conf_files', lambda: False)
+ config = _build_config('custom_dir.conf',
+ config_dir='/etc/multipath/foo.d')
+ facts = _build_facts([config])
+ current_actor_context.feed(facts)
+ current_actor_context.run()
+ reports = list(current_actor_context.consume(Report))
+ assert reports and len(reports) == 1
+ _assert_config_dir_report(reports[0].report)
--
2.54.0

View File

@ -0,0 +1,659 @@
From beffa9b5decef2639af93adf014ca63071900e3e Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Mon, 30 Mar 2026 18:39:57 -0400
Subject: [PATCH 057/105] multipath: Add MultipathUpgradeConfUpdate9to10
mpath_upgrade_conf_patcher actor
Removes deprecated defaults section options (config_dir, bindings_file,
wwids_file, prkeys_file) from multipath configuration files during the
RHEL 9 to RHEL 10 upgrade. Relocates secondary configs to
/etc/multipath/conf.d/ when config_dir is non-default, and creates
entries to move bindings, wwids, and prkeys files to their default
RHEL-10 locations.
Also make multipath_system_config_patcher create directories if they
don't already exist when copying files, to deal with the case where
config files need to be copied to /etc/multipath/conf.d, but it doesn't
exist.
Jira: RHEL-151509
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
.../libraries/system_config_patcher.py | 5 +
.../mpath_upgrade_conf_patcher/actor.py | 29 ++
.../libraries/mpathconfupdate.py | 143 ++++++++++
.../tests/files/after/all_deprecated.conf | 7 +
.../tests/files/after/has_config_dir.conf | 4 +
.../tests/files/after/has_files.conf | 6 +
.../after/secondary_with_deprecated.conf | 9 +
.../tests/files/before/all_deprecated.conf | 7 +
.../tests/files/before/empty.conf | 1 +
.../tests/files/before/has_config_dir.conf | 4 +
.../tests/files/before/has_files.conf | 6 +
.../tests/files/before/no_defaults.conf | 3 +
.../tests/files/before/no_deprecated.conf | 3 +
.../tests/files/before/secondary_simple.conf | 6 +
.../before/secondary_with_deprecated.conf | 9 +
.../tests/test_mpath_conf_update_9to10.py | 256 ++++++++++++++++++
16 files changed, 498 insertions(+)
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/actor.py
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/all_deprecated.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_config_dir.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_files.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/secondary_with_deprecated.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/all_deprecated.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/empty.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_config_dir.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_files.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_defaults.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_deprecated.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_simple.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_with_deprecated.conf
create mode 100644 repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/test_mpath_conf_update_9to10.py
diff --git a/repos/system_upgrade/common/actors/multipath/system_conf_patcher/libraries/system_config_patcher.py b/repos/system_upgrade/common/actors/multipath/system_conf_patcher/libraries/system_config_patcher.py
index 0d873322..0d1fd070 100644
--- a/repos/system_upgrade/common/actors/multipath/system_conf_patcher/libraries/system_config_patcher.py
+++ b/repos/system_upgrade/common/actors/multipath/system_conf_patcher/libraries/system_config_patcher.py
@@ -1,3 +1,4 @@
+import os
import shutil
from leapp.libraries.stdlib import api
@@ -14,4 +15,8 @@ def patch_system_configs():
)
)
+ os.makedirs(
+ os.path.dirname(modified_config.target_path),
+ exist_ok=True
+ )
shutil.copy(modified_config.updated_config_location, modified_config.target_path)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/actor.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/actor.py
new file mode 100644
index 00000000..f72d2936
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/actor.py
@@ -0,0 +1,29 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import mpathconfupdate
+from leapp.models import MultipathConfFacts9to10, MultipathConfigUpdatesInfo
+from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag
+
+
+class MultipathUpgradeConfUpdate9to10(Actor):
+ """
+ Modifies multipath configuration files for the RHEL-10 upgrade.
+
+ Removes deprecated options (config_dir, bindings_file, wwids_file,
+ prkeys_file) from multipath configuration files. If config_dir is
+ set to a non-default directory, ensures all secondary configs are
+ moved to /etc/multipath/conf.d/. Creates entries to relocate
+ bindings, wwids, and prkeys files to their default RHEL-10
+ locations if necessary.
+ """
+
+ name = 'multipath_upgrade_conf_update_9to10'
+ consumes = (MultipathConfFacts9to10,)
+ produces = (MultipathConfigUpdatesInfo,)
+ tags = (TargetTransactionChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ facts = next(self.consume(MultipathConfFacts9to10), None)
+ if facts is None:
+ self.log.debug('Skipping execution. No MultipathConfFacts9to10 has been produced')
+ return
+ mpathconfupdate.update_configs(facts)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
new file mode 100644
index 00000000..a2e3bafc
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
@@ -0,0 +1,143 @@
+import os
+import shutil
+
+from leapp.libraries.common import multipathutil
+from leapp.libraries.stdlib import api
+from leapp.models import MultipathConfigUpdatesInfo, UpdatedMultipathConfig
+
+MODIFICATIONS_STORE_PATH = '/var/lib/leapp/proposed_modifications'
+
+_DEFAULT_CONFIG_DIR = '/etc/multipath/conf.d'
+_DEFAULT_BINDINGS_FILE = '/etc/multipath/bindings'
+_DEFAULT_WWIDS_FILE = '/etc/multipath/wwids'
+_DEFAULT_PRKEYS_FILE = '/etc/multipath/prkeys'
+
+_deprecated_options = ('config_dir', 'bindings_file', 'wwids_file', 'prkeys_file')
+
+
+def _update_config(config):
+ contents = multipathutil.read_config(config.pathname)
+ if contents is None:
+ return None
+ lines = contents.split('\n')
+
+ section = None
+ in_subsection = False
+ updated_file = False
+ comment_lines = []
+ for i, line in enumerate(lines):
+ try:
+ data = multipathutil.LineData(line, section, in_subsection)
+ except ValueError:
+ continue
+ if data.type == data.TYPE_SECTION_END:
+ if in_subsection:
+ in_subsection = False
+ elif section is not None:
+ section = None
+ elif data.type == data.TYPE_SECTION_START:
+ if section is None:
+ section = data.section
+ elif not in_subsection:
+ in_subsection = True
+ elif data.type == data.TYPE_OPTION:
+ if section == 'defaults' and data.option in _deprecated_options:
+ comment_lines.append(i)
+ updated_file = True
+
+ if not updated_file:
+ return None
+
+ for i in reversed(comment_lines):
+ lines[i] = '#{} # line commented out by leapp'.format(lines[i])
+
+ return '\n'.join(lines)
+
+
+def _get_file_locations(facts):
+ bindings_file = None
+ wwids_file = None
+ prkeys_file = None
+ for conf in facts.configs:
+ if conf.bindings_file is not None:
+ bindings_file = os.path.normpath(conf.bindings_file)
+ if conf.wwids_file is not None:
+ wwids_file = os.path.normpath(conf.wwids_file)
+ if conf.prkeys_file is not None:
+ prkeys_file = os.path.normpath(conf.prkeys_file)
+
+ file_updates = []
+ if bindings_file is not None and bindings_file != _DEFAULT_BINDINGS_FILE:
+ file_updates.append((bindings_file, _DEFAULT_BINDINGS_FILE))
+ if wwids_file is not None and wwids_file != _DEFAULT_WWIDS_FILE:
+ file_updates.append((wwids_file, _DEFAULT_WWIDS_FILE))
+ if prkeys_file is not None and prkeys_file != _DEFAULT_PRKEYS_FILE:
+ file_updates.append((prkeys_file, _DEFAULT_PRKEYS_FILE))
+ return file_updates
+
+
+def prepare_destination_for_file(file_path):
+ dirname = os.path.dirname(file_path)
+ os.makedirs(dirname, exist_ok=True)
+
+
+def prepare_place_for_config_modifications(workspace_path=MODIFICATIONS_STORE_PATH):
+ if os.path.exists(workspace_path):
+ shutil.rmtree(workspace_path)
+ os.mkdir(workspace_path)
+
+
+def update_configs(facts):
+ if not facts.configs:
+ return
+
+ config_updates = []
+ prepare_place_for_config_modifications()
+
+ primary = facts.configs[0]
+ non_default_config_dir = (
+ primary.config_dir is not None
+ and os.path.normpath(primary.config_dir) != _DEFAULT_CONFIG_DIR
+ )
+
+ for idx, config in enumerate(facts.configs):
+ is_secondary = idx > 0
+
+ if is_secondary:
+ target_path = os.path.join(
+ _DEFAULT_CONFIG_DIR, os.path.basename(config.pathname)
+ )
+ else:
+ target_path = config.pathname
+
+ contents = _update_config(config)
+
+ if contents is not None:
+ rootless_path = config.pathname.lstrip('/')
+ updated_config_location = os.path.join(
+ MODIFICATIONS_STORE_PATH, rootless_path
+ )
+ api.current_logger().debug(
+ 'Instead of modifying {}, preparing modified config at {}'.format(
+ config.pathname, updated_config_location
+ )
+ )
+ prepare_destination_for_file(updated_config_location)
+ multipathutil.write_config(updated_config_location, contents)
+ config_updates.append(UpdatedMultipathConfig(
+ updated_config_location=updated_config_location,
+ target_path=target_path
+ ))
+ elif is_secondary and non_default_config_dir:
+ config_updates.append(UpdatedMultipathConfig(
+ updated_config_location=config.pathname,
+ target_path=target_path
+ ))
+
+ for source_path, default_path in _get_file_locations(facts):
+ config_updates.append(UpdatedMultipathConfig(
+ updated_config_location=source_path,
+ target_path=default_path
+ ))
+
+ api.produce(MultipathConfigUpdatesInfo(updates=config_updates))
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/all_deprecated.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/all_deprecated.conf
new file mode 100644
index 00000000..7489ed9e
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/all_deprecated.conf
@@ -0,0 +1,7 @@
+defaults {
+ polling_interval 5
+# config_dir "/etc/multipath/custom.d" # line commented out by leapp
+# bindings_file "/tmp/bindings" # line commented out by leapp
+# wwids_file "/tmp/wwids" # line commented out by leapp
+# prkeys_file "/tmp/prkeys" # line commented out by leapp
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_config_dir.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_config_dir.conf
new file mode 100644
index 00000000..94c54ded
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_config_dir.conf
@@ -0,0 +1,4 @@
+defaults {
+ polling_interval 5
+# config_dir "/etc/multipath/custom.d" # line commented out by leapp
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_files.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_files.conf
new file mode 100644
index 00000000..fb74a86e
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/has_files.conf
@@ -0,0 +1,6 @@
+defaults {
+ polling_interval 5
+# bindings_file "/tmp/bindings" # line commented out by leapp
+# wwids_file "/tmp/wwids" # line commented out by leapp
+# prkeys_file "/tmp/prkeys" # line commented out by leapp
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/secondary_with_deprecated.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/secondary_with_deprecated.conf
new file mode 100644
index 00000000..68bfcfca
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/after/secondary_with_deprecated.conf
@@ -0,0 +1,9 @@
+defaults {
+# bindings_file "/tmp/bindings" # line commented out by leapp
+}
+devices {
+ device {
+ vendor "VENDOR"
+ product "PRODUCT"
+ }
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/all_deprecated.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/all_deprecated.conf
new file mode 100644
index 00000000..15e39d34
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/all_deprecated.conf
@@ -0,0 +1,7 @@
+defaults {
+ polling_interval 5
+ config_dir "/etc/multipath/custom.d"
+ bindings_file "/tmp/bindings"
+ wwids_file "/tmp/wwids"
+ prkeys_file "/tmp/prkeys"
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/empty.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/empty.conf
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/empty.conf
@@ -0,0 +1 @@
+
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_config_dir.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_config_dir.conf
new file mode 100644
index 00000000..7e8295ee
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_config_dir.conf
@@ -0,0 +1,4 @@
+defaults {
+ polling_interval 5
+ config_dir "/etc/multipath/custom.d"
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_files.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_files.conf
new file mode 100644
index 00000000..9404312b
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/has_files.conf
@@ -0,0 +1,6 @@
+defaults {
+ polling_interval 5
+ bindings_file "/tmp/bindings"
+ wwids_file "/tmp/wwids"
+ prkeys_file "/tmp/prkeys"
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_defaults.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_defaults.conf
new file mode 100644
index 00000000..dfc28f43
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_defaults.conf
@@ -0,0 +1,3 @@
+blacklist {
+ devnode "^sd[a-z]"
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_deprecated.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_deprecated.conf
new file mode 100644
index 00000000..8c0e1510
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/no_deprecated.conf
@@ -0,0 +1,3 @@
+defaults {
+ polling_interval 5
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_simple.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_simple.conf
new file mode 100644
index 00000000..092e7cdc
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_simple.conf
@@ -0,0 +1,6 @@
+devices {
+ device {
+ vendor "VENDOR"
+ product "PRODUCT"
+ }
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_with_deprecated.conf b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_with_deprecated.conf
new file mode 100644
index 00000000..534c6f07
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/files/before/secondary_with_deprecated.conf
@@ -0,0 +1,9 @@
+defaults {
+ bindings_file "/tmp/bindings"
+}
+devices {
+ device {
+ vendor "VENDOR"
+ product "PRODUCT"
+ }
+}
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/test_mpath_conf_update_9to10.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/test_mpath_conf_update_9to10.py
new file mode 100644
index 00000000..ad6668b9
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/tests/test_mpath_conf_update_9to10.py
@@ -0,0 +1,256 @@
+import os
+
+import pytest
+
+from leapp.libraries.actor import mpathconfupdate
+from leapp.libraries.common import multipathutil
+from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import MultipathConfFacts9to10, MultipathConfig9to10
+
+BEFORE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files/before')
+AFTER_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files/after')
+
+
+def build_config(pathname, config_dir=None, bindings_file=None,
+ wwids_file=None, prkeys_file=None):
+ return MultipathConfig9to10(
+ pathname=pathname,
+ config_dir=config_dir,
+ bindings_file=bindings_file,
+ wwids_file=wwids_file,
+ prkeys_file=prkeys_file,
+ )
+
+
+def build_facts(confs):
+ return MultipathConfFacts9to10(configs=confs)
+
+
+def mock_read_config(path):
+ return multipathutil.read_config_orig(os.path.join(BEFORE_DIR, path))
+
+
+# Configs with no changes needed
+no_deprecated_conf = build_config('no_deprecated.conf')
+empty_conf = build_config('empty.conf')
+no_defaults_conf = build_config('no_defaults.conf')
+
+# Configs with deprecated options
+has_config_dir_conf = build_config(
+ 'has_config_dir.conf', config_dir='/etc/multipath/custom.d')
+has_files_conf = build_config(
+ 'has_files.conf', bindings_file='/tmp/bindings',
+ wwids_file='/tmp/wwids', prkeys_file='/tmp/prkeys')
+all_deprecated_conf = build_config(
+ 'all_deprecated.conf', config_dir='/etc/multipath/custom.d',
+ bindings_file='/tmp/bindings', wwids_file='/tmp/wwids',
+ prkeys_file='/tmp/prkeys')
+
+# Secondary configs
+secondary_simple_conf = build_config('secondary_simple.conf')
+secondary_with_deprecated_conf = build_config(
+ 'secondary_with_deprecated.conf', bindings_file='/tmp/bindings')
+
+
+@pytest.mark.parametrize(
+ 'config_facts',
+ [
+ build_facts([no_deprecated_conf]),
+ build_facts([empty_conf]),
+ build_facts([no_defaults_conf]),
+ build_facts([has_config_dir_conf]),
+ build_facts([has_files_conf]),
+ build_facts([all_deprecated_conf]),
+ build_facts([has_config_dir_conf, secondary_simple_conf]),
+ build_facts([has_config_dir_conf, secondary_with_deprecated_conf]),
+ build_facts([no_deprecated_conf, secondary_simple_conf]),
+ build_facts([no_deprecated_conf, secondary_with_deprecated_conf]),
+ ]
+)
+def test_all_facts(monkeypatch, config_facts):
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ config_writes = {}
+
+ def write_config_mock(location, contents):
+ config_writes[location] = contents
+
+ monkeypatch.setattr(multipathutil, 'read_config_orig', multipathutil.read_config, raising=False)
+ monkeypatch.setattr(multipathutil, 'read_config', mock_read_config)
+ monkeypatch.setattr(multipathutil, 'write_config', write_config_mock)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_destination_for_file', lambda file_path: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_place_for_config_modifications', lambda: None)
+
+ mpathconfupdate.update_configs(config_facts)
+
+ config_updates = {}
+ for config_updates_msg in produce_mock.model_instances:
+ for update in config_updates_msg.updates:
+ config_updates[update.target_path] = update.updated_config_location
+
+ primary = config_facts.configs[0]
+ non_default_config_dir = (
+ primary.config_dir is not None
+ and os.path.normpath(primary.config_dir) != '/etc/multipath/conf.d'
+ )
+
+ for idx, config in enumerate(config_facts.configs):
+ is_secondary = idx > 0
+
+ if is_secondary:
+ target_path = os.path.join(
+ '/etc/multipath/conf.d', os.path.basename(config.pathname)
+ )
+ else:
+ target_path = config.pathname
+
+ expected_conf_location = os.path.join(AFTER_DIR, config.pathname)
+
+ if target_path not in config_updates:
+ # No update for this config - verify no expected after file exists
+ # and it's not a secondary that should have been relocated
+ assert not os.path.exists(expected_conf_location)
+ assert not (is_secondary and non_default_config_dir)
+ continue
+
+ updated_config_location = config_updates[target_path]
+
+ if os.path.exists(expected_conf_location):
+ # Config was modified - check contents
+ assert updated_config_location in config_writes
+ actual_contents = config_writes[updated_config_location]
+
+ updated_config_expected_location = os.path.join(
+ mpathconfupdate.MODIFICATIONS_STORE_PATH,
+ config.pathname.lstrip('/')
+ )
+ assert updated_config_location == updated_config_expected_location
+
+ expected_contents = multipathutil.read_config_orig(expected_conf_location)
+ assert actual_contents == expected_contents
+ else:
+ # Unmodified secondary relocated - source is original path
+ assert updated_config_location == config.pathname
+
+
+def test_file_relocation(monkeypatch):
+ """Check that non-default file locations produce UpdatedMultipathConfig entries."""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ monkeypatch.setattr(multipathutil, 'read_config_orig', multipathutil.read_config, raising=False)
+ monkeypatch.setattr(multipathutil, 'read_config', mock_read_config)
+ monkeypatch.setattr(multipathutil, 'write_config', lambda loc, contents: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_destination_for_file', lambda file_path: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_place_for_config_modifications', lambda: None)
+
+ facts = build_facts([has_files_conf])
+ mpathconfupdate.update_configs(facts)
+
+ file_updates = {}
+ for config_updates_msg in produce_mock.model_instances:
+ for update in config_updates_msg.updates:
+ file_updates[update.target_path] = update.updated_config_location
+
+ assert file_updates['/etc/multipath/bindings'] == '/tmp/bindings'
+ assert file_updates['/etc/multipath/wwids'] == '/tmp/wwids'
+ assert file_updates['/etc/multipath/prkeys'] == '/tmp/prkeys'
+
+
+def test_default_file_locations_no_relocation(monkeypatch):
+ """Check that default file locations don't produce relocation entries."""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ conf = build_config(
+ 'has_files.conf',
+ bindings_file='/etc/multipath/bindings',
+ wwids_file='/etc/multipath/wwids',
+ prkeys_file='/etc/multipath/prkeys',
+ )
+
+ monkeypatch.setattr(multipathutil, 'read_config_orig', multipathutil.read_config, raising=False)
+ monkeypatch.setattr(multipathutil, 'read_config', mock_read_config)
+ monkeypatch.setattr(multipathutil, 'write_config', lambda loc, contents: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_destination_for_file', lambda file_path: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_place_for_config_modifications', lambda: None)
+
+ facts = build_facts([conf])
+ mpathconfupdate.update_configs(facts)
+
+ file_targets = set()
+ for config_updates_msg in produce_mock.model_instances:
+ for update in config_updates_msg.updates:
+ file_targets.add(update.target_path)
+
+ # Config itself is modified (has deprecated options in before file), but no file relocations
+ assert '/etc/multipath/bindings' not in file_targets
+ assert '/etc/multipath/wwids' not in file_targets
+ assert '/etc/multipath/prkeys' not in file_targets
+
+
+def test_last_value_wins_for_files(monkeypatch):
+ """Check that the last non-None value wins for file locations."""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ primary = build_config(
+ 'no_deprecated.conf', bindings_file='/first/bindings')
+ secondary = build_config(
+ 'secondary_simple.conf', bindings_file='/second/bindings')
+
+ monkeypatch.setattr(multipathutil, 'read_config_orig', multipathutil.read_config, raising=False)
+ monkeypatch.setattr(multipathutil, 'read_config', mock_read_config)
+ monkeypatch.setattr(multipathutil, 'write_config', lambda loc, contents: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_destination_for_file', lambda file_path: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_place_for_config_modifications', lambda: None)
+
+ facts = build_facts([primary, secondary])
+ mpathconfupdate.update_configs(facts)
+
+ file_updates = {}
+ for config_updates_msg in produce_mock.model_instances:
+ for update in config_updates_msg.updates:
+ file_updates[update.target_path] = update.updated_config_location
+
+ # Last value (/second/bindings) should win
+ assert file_updates['/etc/multipath/bindings'] == '/second/bindings'
+
+
+def test_proposed_config_updates_store(monkeypatch):
+ """Check whether configs are being stored in the expected path."""
+ config = MultipathConfig9to10(
+ pathname='/etc/multipath.conf.d/xy.conf',
+ config_dir='',
+ )
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ monkeypatch.setattr(multipathutil, 'write_config', lambda loc, contents: None)
+ monkeypatch.setattr(mpathconfupdate, '_update_config', lambda *args: 'new config content')
+ monkeypatch.setattr(mpathconfupdate, 'prepare_destination_for_file', lambda file_path: None)
+ monkeypatch.setattr(mpathconfupdate, 'prepare_place_for_config_modifications', lambda: None)
+
+ mpathconfupdate.update_configs(MultipathConfFacts9to10(configs=[config]))
+
+ expected_updated_config_path = os.path.join(
+ mpathconfupdate.MODIFICATIONS_STORE_PATH,
+ 'etc/multipath.conf.d/xy.conf'
+ )
+ found = False
+ for config_updates_msg in produce_mock.model_instances:
+ for update in config_updates_msg.updates:
+ if update.updated_config_location == expected_updated_config_path:
+ found = True
+ assert found
--
2.54.0

View File

@ -0,0 +1,567 @@
From 3c45b90ec3a053bae88f6bf829317c36bb914b71 Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Wed, 1 Apr 2026 14:24:42 -0400
Subject: [PATCH 058/105] multipath: Add bindings/wwids/prkeys file copying to
target userspace
The request_multipath_conf_in_target_userspace actor was not copying
bindings, wwids, and prkeys files into the target userspace. Add
bindings_file, wwids_file, and prkeys_file fields to MultipathInfo so
the config reader actors can communicate the file locations to the
target_uspace_configs actor.
For el8toel9, MultipathInfo always gets all three file fields set to
whatever the effective location is (default or configured), so the
files are always copied at their current location.
For el9toel10, MultipathInfo file fields are only set when the
effective value is at the default location (or unset). Non-default
file locations are not set on MultipathInfo, since the patcher actor
handles relocating them to the default location via
UpdatedMultipathConfig entries.
Also normalize config_dir paths in the el8toel9 config reader with
os.path.normpath() for consistency with el9toel10.
Jira: RHEL-151509
Tests-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
.../target_uspace_multipath_configs.py | 7 ++
.../tests/test_target_uspace_configs.py | 117 ++++++++++++++++++
.../system_upgrade/common/models/multipath.py | 27 +++-
.../libraries/multipathconfread.py | 38 ++++--
.../tests/test_multipath_conf_read_8to9.py | 42 ++++++-
.../libraries/multipathconfread.py | 36 +++++-
.../tests/test_multipath_conf_read_9to10.py | 99 +++++++++++++++
7 files changed, 354 insertions(+), 12 deletions(-)
diff --git a/repos/system_upgrade/common/actors/multipath/target_uspace_configs/libraries/target_uspace_multipath_configs.py b/repos/system_upgrade/common/actors/multipath/target_uspace_configs/libraries/target_uspace_multipath_configs.py
index bc685fa2..f294a15e 100644
--- a/repos/system_upgrade/common/actors/multipath/target_uspace_configs/libraries/target_uspace_multipath_configs.py
+++ b/repos/system_upgrade/common/actors/multipath/target_uspace_configs/libraries/target_uspace_multipath_configs.py
@@ -22,6 +22,13 @@ def request_mpath_confs(multipath_info):
'/etc/multipath.conf': '/etc/multipath.conf' # default config
}
+ for conf_file in (
+ multipath_info.bindings_file,
+ multipath_info.wwids_file,
+ multipath_info.prkeys_file
+ ):
+ if conf_file and os.path.exists(conf_file):
+ files_to_put_into_uspace[conf_file] = conf_file
if multipath_info.config_dir and os.path.exists(multipath_info.config_dir):
for filename in os.listdir(multipath_info.config_dir):
config_path = os.path.join(multipath_info.config_dir, filename)
diff --git a/repos/system_upgrade/common/actors/multipath/target_uspace_configs/tests/test_target_uspace_configs.py b/repos/system_upgrade/common/actors/multipath/target_uspace_configs/tests/test_target_uspace_configs.py
index ffb63322..ee199df4 100644
--- a/repos/system_upgrade/common/actors/multipath/target_uspace_configs/tests/test_target_uspace_configs.py
+++ b/repos/system_upgrade/common/actors/multipath/target_uspace_configs/tests/test_target_uspace_configs.py
@@ -84,3 +84,120 @@ def test_production_conditions(monkeypatch, multipath_info, should_produce):
assert dracut_modules == ['multipath']
else:
assert not produce_mock.called
+
+
+def test_file_locations_copied(monkeypatch):
+ """Test that bindings/wwids/prkeys files are copied to target userspace."""
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ multipath_info = MultipathInfo(
+ is_configured=True,
+ config_dir='/etc/multipath/conf.d',
+ bindings_file='/etc/multipath/bindings',
+ wwids_file='/etc/multipath/wwids',
+ prkeys_file='/etc/multipath/prkeys',
+ )
+ msgs = [multipath_info, MultipathConfigUpdatesInfo(updates=[])]
+ actor_mock = CurrentActorMocked(msgs=msgs)
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ existing_files = {
+ '/etc/multipath/conf.d',
+ '/etc/multipath/bindings',
+ '/etc/multipath/wwids',
+ '/etc/multipath/prkeys',
+ }
+
+ def exists_mock(path):
+ return path in existing_files
+
+ monkeypatch.setattr(os.path, 'exists', exists_mock)
+ monkeypatch.setattr(os, 'listdir', lambda path: [])
+
+ actor_lib.process()
+
+ _target_uspace_tasks = [
+ msg for msg in produce_mock.model_instances if isinstance(msg, TargetUserSpaceUpgradeTasks)
+ ]
+ assert len(_target_uspace_tasks) == 1
+
+ copies = {(copy.src, copy.dst) for copy in _target_uspace_tasks[0].copy_files}
+ assert ('/etc/multipath/bindings', '/etc/multipath/bindings') in copies
+ assert ('/etc/multipath/wwids', '/etc/multipath/wwids') in copies
+ assert ('/etc/multipath/prkeys', '/etc/multipath/prkeys') in copies
+
+
+def test_file_locations_not_copied_when_missing(monkeypatch):
+ """Test that non-existent files are not copied."""
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ multipath_info = MultipathInfo(
+ is_configured=True,
+ config_dir='/etc/multipath/conf.d',
+ bindings_file='/etc/multipath/bindings',
+ )
+ msgs = [multipath_info, MultipathConfigUpdatesInfo(updates=[])]
+ actor_mock = CurrentActorMocked(msgs=msgs)
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ def exists_mock(path):
+ return path == '/etc/multipath/conf.d'
+
+ monkeypatch.setattr(os.path, 'exists', exists_mock)
+ monkeypatch.setattr(os, 'listdir', lambda path: [])
+
+ actor_lib.process()
+
+ _target_uspace_tasks = [
+ msg for msg in produce_mock.model_instances if isinstance(msg, TargetUserSpaceUpgradeTasks)
+ ]
+ assert len(_target_uspace_tasks) == 1
+
+ copies = {copy.src for copy in _target_uspace_tasks[0].copy_files}
+ # bindings_file doesn't exist on disk, so should not be copied
+ assert '/etc/multipath/bindings' not in copies
+
+
+def test_file_locations_overridden_by_updates(monkeypatch):
+ """Test that UpdatedMultipathConfig entries override file copy sources."""
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ multipath_info = MultipathInfo(
+ is_configured=True,
+ config_dir='/etc/multipath/conf.d',
+ bindings_file='/etc/multipath/bindings',
+ )
+ # The patcher says to move /tmp/bindings -> /etc/multipath/bindings
+ update = UpdatedMultipathConfig(
+ updated_config_location='/tmp/bindings',
+ target_path='/etc/multipath/bindings'
+ )
+ msgs = [multipath_info, MultipathConfigUpdatesInfo(updates=[update])]
+ actor_mock = CurrentActorMocked(msgs=msgs)
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ existing_files = {
+ '/etc/multipath/conf.d',
+ '/etc/multipath/bindings',
+ }
+
+ def exists_mock(path):
+ return path in existing_files
+
+ monkeypatch.setattr(os.path, 'exists', exists_mock)
+ monkeypatch.setattr(os, 'listdir', lambda path: [])
+
+ actor_lib.process()
+
+ _target_uspace_tasks = [
+ msg for msg in produce_mock.model_instances if isinstance(msg, TargetUserSpaceUpgradeTasks)
+ ]
+ assert len(_target_uspace_tasks) == 1
+
+ copies = {(copy.src, copy.dst) for copy in _target_uspace_tasks[0].copy_files}
+ # Original bindings file should be removed and replaced by the update source
+ assert ('/etc/multipath/bindings', '/etc/multipath/bindings') not in copies
+ assert ('/tmp/bindings', '/etc/multipath/bindings') in copies
diff --git a/repos/system_upgrade/common/models/multipath.py b/repos/system_upgrade/common/models/multipath.py
index 9ccaa290..aaa11801 100644
--- a/repos/system_upgrade/common/models/multipath.py
+++ b/repos/system_upgrade/common/models/multipath.py
@@ -14,7 +14,32 @@ class MultipathInfo(Model):
"""
config_dir = fields.Nullable(fields.String())
- """ Value of config_dir in the defaults section. None if not set. """
+ """
+ The location of config_dir, if it should be copied to the same location
+ in the target userspace. None if it should be copied to a different
+ location (handled by upgrade-specific models).
+ """
+
+ bindings_file = fields.Nullable(fields.String())
+ """
+ The location of bindings_file, if it should be copied to the same location
+ in the target userspace. None if it should be copied to a different
+ location (handled by upgrade-specific models).
+ """
+
+ wwids_file = fields.Nullable(fields.String())
+ """
+ The location of wwids_file, if it should be copied to the same location
+ in the target userspace. None if it should be copied to a different
+ location (handled by upgrade-specific models).
+ """
+
+ prkeys_file = fields.Nullable(fields.String())
+ """
+ The location of prkeys_file, if it should be copied to the same location
+ in the target userspace. None if it should be copied to a different
+ location (handled by upgrade-specific models).
+ """
class UpdatedMultipathConfig(Model):
diff --git a/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py b/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
index f2f39293..2c81ed52 100644
--- a/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
+++ b/repos/system_upgrade/el8toel9/actors/multipathconfread/libraries/multipathconfread.py
@@ -9,8 +9,12 @@ from leapp.models import DistributionSignedRPM, MultipathConfFacts8to9, Multipat
_regexes = ('vendor', 'product', 'revision', 'product_blacklist', 'devnode',
'wwid', 'property', 'protocol')
+_DEFAULT_BINDINGS_FILE = '/etc/multipath/bindings'
+_DEFAULT_WWIDS_FILE = '/etc/multipath/wwids'
+_DEFAULT_PRKEYS_FILE = '/etc/multipath/prkeys'
-def _parse_config(path):
+
+def _parse_config(path, file_locs):
contents = multipathutil.read_config(path)
if contents is None:
return None
@@ -44,20 +48,22 @@ def _parse_config(path):
elif data.option == 'allow_usb_devices':
conf.allow_usb_exists = True
elif data.option == 'config_dir':
- conf.config_dir = data.value
+ conf.config_dir = os.path.normpath(data.value)
+ elif data.option in ('bindings_file', 'wwids_file', 'prkeys_file'):
+ file_locs[data.option] = os.path.normpath(data.value)
if data.option in _regexes and data.value == '*':
conf.invalid_regexes_exist = True
return conf
-def _parse_config_dir(config_dir):
+def _parse_config_dir(config_dir, file_locs):
res = []
try:
for config_file in sorted(os.listdir(config_dir)):
path = os.path.join(config_dir, config_file)
if not path.endswith('.conf'):
continue
- conf = _parse_config(path)
+ conf = _parse_config(path, file_locs)
if conf:
res.append(conf)
except OSError as e:
@@ -82,11 +88,22 @@ def is_processable():
return res
+def setup_default_file_locations():
+ file_locs = {
+ "bindings_file": _DEFAULT_BINDINGS_FILE,
+ "wwids_file": _DEFAULT_WWIDS_FILE,
+ "prkeys_file": _DEFAULT_PRKEYS_FILE,
+ }
+ return file_locs
+
+
def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
if not is_processable():
return
- primary_config = _parse_config(default_config_path)
+ file_locs = setup_default_file_locations()
+
+ primary_config = _parse_config(default_config_path, file_locs)
if not primary_config:
api.current_logger().debug(
'Primary multipath config /etc/multipath.conf is not present - multipath '
@@ -100,10 +117,17 @@ def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
is_configured=True,
config_dir=primary_config.config_dir or '/etc/multipath/conf.d'
)
+
+ secondary_configs = _parse_config_dir(
+ multipath_info.config_dir,
+ file_locs
+ )
+
+ multipath_info.bindings_file = file_locs['bindings_file']
+ multipath_info.wwids_file = file_locs['wwids_file']
+ multipath_info.prkeys_file = file_locs['prkeys_file']
api.produce(multipath_info)
- secondary_configs = _parse_config_dir(multipath_info.config_dir)
all_configs = [primary_config] + secondary_configs
-
config_facts_for_8to9 = MultipathConfFacts8to9(configs=all_configs)
api.produce(config_facts_for_8to9)
diff --git a/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
index 2da74927..cd8036ec 100644
--- a/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
+++ b/repos/system_upgrade/el8toel9/actors/multipathconfread/tests/test_multipath_conf_read_8to9.py
@@ -78,9 +78,9 @@ two_defaults_conf = build_config(
os.path.join(TEST_DIR, 'two_defaults.conf'), None, True, False, False)
-def mock_parse_config(path):
+def mock_parse_config(path, file_locs):
"""Convert config_dir into full pathname"""
- conf = multipathconfread._parse_config_orig(path)
+ conf = multipathconfread._parse_config_orig(path, file_locs)
if not conf:
return None
if conf.config_dir:
@@ -99,7 +99,9 @@ def test_parse_config():
'two_defaults.conf': two_defaults_conf,
'empty.conf': empty_conf}
for config_name, expected_data in test_map.items():
- config = multipathconfread._parse_config(os.path.join(TEST_DIR, config_name))
+ file_locs = multipathconfread.setup_default_file_locations()
+ config = multipathconfread._parse_config(
+ os.path.join(TEST_DIR, config_name), file_locs)
assert config
assert_config(config, expected_data)
@@ -133,6 +135,9 @@ def test_get_facts_missing_dir(monkeypatch, primary_config, expected_configs):
assert len(general_info) == 1
assert general_info[0].is_configured
# general_info[0].config_dir is with the MultipathConfFacts8to9 messages below
+ assert general_info[0].bindings_file == '/etc/multipath/bindings'
+ assert general_info[0].wwids_file == '/etc/multipath/wwids'
+ assert general_info[0].prkeys_file == '/etc/multipath/prkeys'
msgs = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathConfFacts8to9)]
assert len(msgs) == 1
@@ -142,3 +147,34 @@ def test_get_facts_missing_dir(monkeypatch, primary_config, expected_configs):
for actual_config, expected_config in zip(actual_configs, expected_configs):
assert_config(actual_config, expected_config)
+
+
+def test_file_locations_nondefault(monkeypatch):
+ """Check that non-default file locations are tracked correctly."""
+ monkeypatch.setattr(multipathconfread, 'is_processable', lambda: True)
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ actor_mock = CurrentActorMocked(src_ver='8.10', dst_ver='9.6')
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ # Create a test config with non-default file locations
+ file_locs = multipathconfread.setup_default_file_locations()
+ file_locs['bindings_file'] = '/tmp/bindings'
+ file_locs['wwids_file'] = '/tmp/wwids'
+
+ def mock_parse(path, fl):
+ fl.update(file_locs)
+ return MultipathConfig8to9(pathname=path)
+
+ monkeypatch.setattr(multipathconfread, '_parse_config', mock_parse)
+ monkeypatch.setattr(multipathconfread, '_parse_config_dir', lambda d, fl: [])
+
+ multipathconfread.scan_and_emit_multipath_info('/etc/multipath.conf')
+
+ general_info = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathInfo)]
+ assert len(general_info) == 1
+ assert general_info[0].bindings_file == '/tmp/bindings'
+ assert general_info[0].wwids_file == '/tmp/wwids'
+ assert general_info[0].prkeys_file == '/etc/multipath/prkeys'
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
index 50c87d86..7d85bdbc 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
@@ -6,6 +6,10 @@ from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import DistributionSignedRPM, MultipathConfFacts9to10, MultipathConfig9to10, MultipathInfo
+_DEFAULT_BINDINGS_FILE = '/etc/multipath/bindings'
+_DEFAULT_WWIDS_FILE = '/etc/multipath/wwids'
+_DEFAULT_PRKEYS_FILE = '/etc/multipath/prkeys'
+
def _parse_config(path):
contents = multipathutil.read_config(path)
@@ -98,6 +102,35 @@ def is_processable():
return res
+def _add_file_locations(configs, mpath_info):
+ bindings_file = None
+ wwids_file = None
+ prkeys_file = None
+ for conf in configs:
+ if conf.bindings_file is not None:
+ bindings_file = conf.bindings_file
+ if conf.wwids_file is not None:
+ wwids_file = conf.wwids_file
+ if conf.prkeys_file is not None:
+ prkeys_file = conf.prkeys_file
+
+ if (
+ bindings_file is None or
+ os.path.normpath(bindings_file) == _DEFAULT_BINDINGS_FILE
+ ):
+ mpath_info.bindings_file = _DEFAULT_BINDINGS_FILE
+ if (
+ wwids_file is None or
+ os.path.normpath(wwids_file) == _DEFAULT_WWIDS_FILE
+ ):
+ mpath_info.wwids_file = _DEFAULT_WWIDS_FILE
+ if (
+ prkeys_file is None or
+ os.path.normpath(prkeys_file) == _DEFAULT_PRKEYS_FILE
+ ):
+ mpath_info.prkeys_file = _DEFAULT_PRKEYS_FILE
+
+
def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
if not is_processable():
return
@@ -124,7 +157,6 @@ def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
os.path.normpath(primary_config.config_dir) == '/etc/multipath/conf.d'
):
multipath_info.config_dir = '/etc/multipath/conf.d'
- api.produce(multipath_info)
primary_config.has_socket_activation = _check_socket_activation()
primary_config.has_dm_nvme_multipathing = _check_dm_nvme_multipathing()
@@ -133,6 +165,8 @@ def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
primary_config.config_dir or '/etc/multipath/conf.d'
)
all_configs = [primary_config] + secondary_configs
+ _add_file_locations(all_configs, multipath_info)
+ api.produce(multipath_info)
config_facts_for_9to10 = MultipathConfFacts9to10(configs=all_configs)
api.produce(config_facts_for_9to10)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/tests/test_multipath_conf_read_9to10.py b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/tests/test_multipath_conf_read_9to10.py
index cb90fcc3..f8f039a0 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/tests/test_multipath_conf_read_9to10.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/tests/test_multipath_conf_read_9to10.py
@@ -162,6 +162,9 @@ def test_get_primary_facts_default_config_dir(monkeypatch, primary_config, expec
assert len(general_info) == 1
assert general_info[0].is_configured
assert general_info[0].config_dir == '/etc/multipath/conf.d'
+ assert general_info[0].bindings_file == '/etc/multipath/bindings'
+ assert general_info[0].wwids_file == '/etc/multipath/wwids'
+ assert general_info[0].prkeys_file == '/etc/multipath/prkeys'
msgs = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathConfFacts9to10)]
assert len(msgs) == 1
@@ -214,6 +217,102 @@ def test_get_facts_with_config_dir(monkeypatch, primary_config, expected_configs
assert_config(actual_config, expected_config)
+def test_file_locations_default(monkeypatch):
+ """Default or unset file locations should be set on MultipathInfo."""
+ monkeypatch.setattr(multipathconfread, 'is_processable', lambda: True)
+ monkeypatch.setattr(multipathconfread, '_check_socket_activation', lambda: False)
+ monkeypatch.setattr(multipathconfread, '_check_dm_nvme_multipathing', lambda: False)
+ monkeypatch.setattr(multipathconfread, '_parse_config_dir', mock_parse_config_dir)
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ actor_mock = CurrentActorMocked(src_ver='9.6', dst_ver='10.0')
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ config_to_use = os.path.join(TEST_DIR, 'all_options.conf')
+ multipathconfread.scan_and_emit_multipath_info(config_to_use)
+
+ general_info = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathInfo)]
+ assert len(general_info) == 1
+ assert general_info[0].bindings_file == '/etc/multipath/bindings'
+ assert general_info[0].wwids_file == '/etc/multipath/wwids'
+ assert general_info[0].prkeys_file == '/etc/multipath/prkeys'
+
+
+def test_file_locations_nondefault(monkeypatch):
+ """Non-default file locations should NOT be set on MultipathInfo."""
+ monkeypatch.setattr(multipathconfread, 'is_processable', lambda: True)
+ monkeypatch.setattr(multipathconfread, '_check_socket_activation', lambda: False)
+ monkeypatch.setattr(multipathconfread, '_check_dm_nvme_multipathing', lambda: False)
+ monkeypatch.setattr(multipathconfread, '_parse_config_dir', mock_parse_config_dir)
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ actor_mock = CurrentActorMocked(src_ver='9.6', dst_ver='10.0')
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ # Use a config that sets files to non-default paths
+ def mock_parse(path):
+ return MultipathConfig9to10(
+ pathname=path,
+ bindings_file='/tmp/bindings',
+ wwids_file='/tmp/wwids',
+ prkeys_file='/tmp/prkeys',
+ )
+
+ monkeypatch.setattr(multipathconfread, '_parse_config', mock_parse)
+
+ multipathconfread.scan_and_emit_multipath_info('/etc/multipath.conf')
+
+ general_info = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathInfo)]
+ assert len(general_info) == 1
+ # Non-default file locations should not be set on MultipathInfo
+ assert general_info[0].bindings_file is None
+ assert general_info[0].wwids_file is None
+ assert general_info[0].prkeys_file is None
+
+
+def test_file_locations_secondary_overrides(monkeypatch):
+ """Last non-None value wins for file locations across configs."""
+ monkeypatch.setattr(multipathconfread, 'is_processable', lambda: True)
+ monkeypatch.setattr(multipathconfread, '_check_socket_activation', lambda: False)
+ monkeypatch.setattr(multipathconfread, '_check_dm_nvme_multipathing', lambda: False)
+
+ produce_mock = produce_mocked()
+ monkeypatch.setattr(api, 'produce', produce_mock)
+
+ actor_mock = CurrentActorMocked(src_ver='9.6', dst_ver='10.0')
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ primary = MultipathConfig9to10(
+ pathname='/etc/multipath.conf',
+ bindings_file='/tmp/bindings',
+ )
+
+ secondary = MultipathConfig9to10(
+ pathname='/etc/multipath/conf.d/secondary.conf',
+ bindings_file='/etc/multipath/bindings',
+ )
+
+ def mock_parse(path):
+ return primary
+
+ def mock_parse_dir(config_dir):
+ return [secondary]
+
+ monkeypatch.setattr(multipathconfread, '_parse_config', mock_parse)
+ monkeypatch.setattr(multipathconfread, '_parse_config_dir', mock_parse_dir)
+
+ multipathconfread.scan_and_emit_multipath_info('/etc/multipath.conf')
+
+ general_info = [msg for msg in produce_mock.model_instances if isinstance(msg, MultipathInfo)]
+ assert len(general_info) == 1
+ # Secondary overrides primary to default, so it should be set
+ assert general_info[0].bindings_file == '/etc/multipath/bindings'
+
+
def test_check_socket_activation(monkeypatch):
monkeypatch.setattr(os.path, 'exists', lambda path: True)
assert multipathconfread._check_socket_activation() is True
--
2.54.0

View File

@ -0,0 +1,228 @@
From 6fc76ee600c896b92f06cb65d9f1bbc774e4c9f8 Mon Sep 17 00:00:00 2001
From: Benjamin Marzinski <bmarzins@redhat.com>
Date: Wed, 15 Apr 2026 02:30:37 -0400
Subject: [PATCH 059/105] multipath: Add mpathfiles library and use it in
el9toel10 actors
All of the el9toel10 actors need to read through all the configs to
determine the value of bindings_file, wwids_file, and prkeys_file for
the overall configuration. Split this code out into a library function
and call it from all of the actors. Also move the code to use these
files into a loop, since the same action needs to be done on all three
files.
---
.../libraries/multipathconfread.py | 47 ++++++++-----------
.../libraries/mpath_conf_check.py | 41 ++++++----------
.../libraries/mpathconfupdate.py | 31 ++++++------
.../el9toel10/libraries/mpathfiles.py | 22 +++++++++
4 files changed, 71 insertions(+), 70 deletions(-)
create mode 100644 repos/system_upgrade/el9toel10/libraries/mpathfiles.py
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
index 7d85bdbc..0f070c1a 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/config_reader/libraries/multipathconfread.py
@@ -1,7 +1,7 @@
import errno
import os
-from leapp.libraries.common import multipathutil
+from leapp.libraries.common import mpathfiles, multipathutil
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import DistributionSignedRPM, MultipathConfFacts9to10, MultipathConfig9to10, MultipathInfo
@@ -103,32 +103,25 @@ def is_processable():
def _add_file_locations(configs, mpath_info):
- bindings_file = None
- wwids_file = None
- prkeys_file = None
- for conf in configs:
- if conf.bindings_file is not None:
- bindings_file = conf.bindings_file
- if conf.wwids_file is not None:
- wwids_file = conf.wwids_file
- if conf.prkeys_file is not None:
- prkeys_file = conf.prkeys_file
-
- if (
- bindings_file is None or
- os.path.normpath(bindings_file) == _DEFAULT_BINDINGS_FILE
- ):
- mpath_info.bindings_file = _DEFAULT_BINDINGS_FILE
- if (
- wwids_file is None or
- os.path.normpath(wwids_file) == _DEFAULT_WWIDS_FILE
- ):
- mpath_info.wwids_file = _DEFAULT_WWIDS_FILE
- if (
- prkeys_file is None or
- os.path.normpath(prkeys_file) == _DEFAULT_PRKEYS_FILE
- ):
- mpath_info.prkeys_file = _DEFAULT_PRKEYS_FILE
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(configs)
+
+ attrs_to_populate = [
+ ('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE),
+ ('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE),
+ ('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ # Set the attribute value only if it is not configured or equal to default.
+ # Setting mpath_info.X = Y would cause the config at Y to be copied into
+ # target_uspace/Y. However, we want to place Y at the default location due
+ # to 9>10 deprecations and there is no easy way to remove target_uspace/Y
+ # once created.
+ for mpath_info_attr, configured_value, default_value in attrs_to_populate:
+ if (
+ configured_value is None or
+ os.path.normpath(configured_value) == default_value
+ ):
+ setattr(mpath_info, mpath_info_attr, default_value)
def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
index 49aefc5a..8c4dc531 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
@@ -1,6 +1,7 @@
import os
from leapp import reporting
+from leapp.libraries.common import mpathfiles
from leapp.reporting import create_report
_DEFAULT_CONFIG_DIR = '/etc/multipath/conf.d'
@@ -145,34 +146,22 @@ def check_configs(facts):
if _default_config_dir_has_conf_files():
_report_config_dir_conflict(config_dir)
- # bindings_file, wwids_file, prkeys_file: last non-None value wins
- bindings_file = None
- wwids_file = None
- prkeys_file = None
- for conf in facts.configs:
- if conf.bindings_file is not None:
- bindings_file = conf.bindings_file
- if conf.wwids_file is not None:
- wwids_file = conf.wwids_file
- if conf.prkeys_file is not None:
- prkeys_file = conf.prkeys_file
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(facts.configs)
file_list = []
- if (
- bindings_file is not None and
- os.path.normpath(bindings_file) != _DEFAULT_BINDINGS_FILE
- ):
- file_list.append(('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE))
- if (
- wwids_file is not None and
- os.path.normpath(wwids_file) != _DEFAULT_WWIDS_FILE
- ):
- file_list.append(('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE))
- if (
- prkeys_file is not None and
- os.path.normpath(prkeys_file) != _DEFAULT_PRKEYS_FILE
- ):
- file_list.append(('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE))
+ files_to_report = [
+ ('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE),
+ ('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE),
+ ('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ for file_name, configured_path, default_path in files_to_report:
+ if (
+ configured_path is not None and
+ os.path.normpath(configured_path) != default_path
+ ):
+ file_list.append((file_name, configured_path, default_path))
+
if file_list:
_report_files(file_list)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
index a2e3bafc..d5618018 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
@@ -1,7 +1,7 @@
import os
import shutil
-from leapp.libraries.common import multipathutil
+from leapp.libraries.common import mpathfiles, multipathutil
from leapp.libraries.stdlib import api
from leapp.models import MultipathConfigUpdatesInfo, UpdatedMultipathConfig
@@ -55,24 +55,21 @@ def _update_config(config):
def _get_file_locations(facts):
- bindings_file = None
- wwids_file = None
- prkeys_file = None
- for conf in facts.configs:
- if conf.bindings_file is not None:
- bindings_file = os.path.normpath(conf.bindings_file)
- if conf.wwids_file is not None:
- wwids_file = os.path.normpath(conf.wwids_file)
- if conf.prkeys_file is not None:
- prkeys_file = os.path.normpath(conf.prkeys_file)
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(facts.configs)
file_updates = []
- if bindings_file is not None and bindings_file != _DEFAULT_BINDINGS_FILE:
- file_updates.append((bindings_file, _DEFAULT_BINDINGS_FILE))
- if wwids_file is not None and wwids_file != _DEFAULT_WWIDS_FILE:
- file_updates.append((wwids_file, _DEFAULT_WWIDS_FILE))
- if prkeys_file is not None and prkeys_file != _DEFAULT_PRKEYS_FILE:
- file_updates.append((prkeys_file, _DEFAULT_PRKEYS_FILE))
+ files_to_move = [
+ (bindings_file, _DEFAULT_BINDINGS_FILE),
+ (wwids_file, _DEFAULT_WWIDS_FILE),
+ (prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ for configured_path, default_path in files_to_move:
+ if configured_path is not None:
+ configured_path = os.path.normpath(configured_path)
+ if configured_path != default_path:
+ file_updates.append((configured_path, default_path))
+
return file_updates
diff --git a/repos/system_upgrade/el9toel10/libraries/mpathfiles.py b/repos/system_upgrade/el9toel10/libraries/mpathfiles.py
new file mode 100644
index 00000000..ada276a3
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/libraries/mpathfiles.py
@@ -0,0 +1,22 @@
+def mpath_file_locations(configs):
+ """
+ Returns the configured location of the bindings_file, wwids_file, and
+ prkeys_file multipath files, handling cases where the location is set
+ multiple times (the last set value is used) or not set at all (None is
+ used).
+
+ :param configs: The ordered list of all multipath config files data
+ :type configs: List[MultipathConfig9to10]
+ :return: The locations of bindings_file, wwids_file, and prkeys_file
+ :rtype: Tuple[Optional[str], Optional[str], Optional[str]]
+
+ """
+
+ bindings_file = None
+ wwids_file = None
+ prkeys_file = None
+ for conf in configs:
+ bindings_file = conf.bindings_file or bindings_file
+ wwids_file = conf.wwids_file or wwids_file
+ prkeys_file = conf.prkeys_file or prkeys_file
+ return (bindings_file, wwids_file, prkeys_file)
--
2.54.0

View File

@ -0,0 +1,96 @@
From ecb06c6fd4ad365ecfe0808a616cd3b3b9cf18d0 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Wed, 29 Apr 2026 21:05:52 +0200
Subject: [PATCH 060/105] model(upgrade_cmdline): add to_remove field
Extend the UpgradeKernelCmdlineArgTasks model to have a `to_remove`
field that list kernel cmdline arguments that should be removed from the
upgrade boot entry. Args listed in the field have precedence over
arguments listed in to_add, i.e., if 'X' is in both `to_remove` and
`to_add`, it is guaranteed to be removed.
Jira-ref: RHEL-145136
---
.../libraries/addupgradebootentry.py | 4 ++-
.../tests/unit_test_addupgradebootentry.py | 26 ++++++++++++++++++-
.../common/models/kernelcmdlineargs.py | 4 +++
3 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
index 5b635a83..8fb2e404 100644
--- a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
+++ b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
@@ -50,7 +50,9 @@ def collect_undesired_args(livemode_enabled):
args = dict(zip(('ro', 'rhgb', 'quiet'), itertools.repeat(None)))
args['rd.lvm.lv'] = _get_rdlvm_arg_values()
- return set(args.items())
+ undesired = set(args.items())
+ undesired |= collect_set_of_kernel_args_from_msgs(UpgradeKernelCmdlineArgTasks, 'to_remove')
+ return undesired
def format_grubby_args_from_args_set(args_dict):
diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py
index 79c05a5d..09632c0c 100644
--- a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py
+++ b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py
@@ -18,7 +18,8 @@ from leapp.models import (
LateTargetKernelCmdlineArgTasks,
LiveModeArtifacts,
LiveModeConfig,
- TargetKernelCmdlineArgTasks
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
)
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -395,3 +396,26 @@ def test_modify_grubenv_to_have_separate_blsdir(monkeypatch, has_separate_boot):
monkeypatch.setattr(addupgradebootentry, 'run', run_mocked)
addupgradebootentry.modify_our_grubenv_to_have_separate_blsdir(efi_info)
+
+
+def test_collect_undesired_args_includes_upgrade_to_remove(monkeypatch):
+ msgs = [
+ UpgradeKernelCmdlineArgTasks(to_remove=[
+ KernelCmdlineArg(key='rd.luks.uuid', value='luks-aaa'),
+ KernelCmdlineArg(key='rd.luks.uuid', value='luks-bbb'),
+ ]),
+ ]
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+
+ undesired = addupgradebootentry.collect_undesired_args(livemode_enabled=False)
+
+ assert ('rd.luks.uuid', 'luks-aaa') in undesired
+ assert ('rd.luks.uuid', 'luks-bbb') in undesired
+
+
+def test_collect_undesired_args_no_upgrade_to_remove(monkeypatch):
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+
+ undesired = addupgradebootentry.collect_undesired_args(livemode_enabled=False)
+
+ assert undesired == set()
diff --git a/repos/system_upgrade/common/models/kernelcmdlineargs.py b/repos/system_upgrade/common/models/kernelcmdlineargs.py
index fafd2853..2f163c34 100644
--- a/repos/system_upgrade/common/models/kernelcmdlineargs.py
+++ b/repos/system_upgrade/common/models/kernelcmdlineargs.py
@@ -39,10 +39,14 @@ class LateTargetKernelCmdlineArgTasks(Model):
class UpgradeKernelCmdlineArgTasks(Model):
"""
Modifications of the upgrade kernel cmdline.
+
+ The arguments in to_remove have precedence over argument in to_add. That is, if 'ARG'
+ is in to_remove, it is guaranteed to be removed (even if it is also in to_add).
"""
topic = SystemInfoTopic
to_add = fields.List(fields.Model(KernelCmdlineArg), default=[])
+ to_remove = fields.List(fields.Model(KernelCmdlineArg), default=[])
class KernelCmdline(Model):
--
2.54.0

View File

@ -0,0 +1,81 @@
From bb81f96120a8c8b2ecba4bb9db7ecb86da3867fb Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Thu, 7 May 2026 10:39:12 +0200
Subject: [PATCH 061/105] checkluks: emit messages only once
Currently, the checkluks actor emits messages for target userspace and
initramfs in a loop, i.e., a single message is emitted for every OK luks
partition. This patch changes the behavior, so that messages are emitted
only once, if there are any OK partitions present.
---
.../actors/checkluks/libraries/checkluks.py | 50 +++++++++++--------
1 file changed, 28 insertions(+), 22 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
index f3e45b47..cdf86d08 100644
--- a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
+++ b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
@@ -111,6 +111,7 @@ def check_invalid_luks_devices():
luks1_partitions = []
no_tpm2_partitions = []
+ ok_partitions = []
ceph_vol = _get_ceph_volumes()
for luks_dump in luks_dumps.dumps:
# if the device is managed by ceph, don't inhibit
@@ -126,25 +127,30 @@ def check_invalid_luks_devices():
if luks1_partitions or no_tpm2_partitions:
report_inhibitor(luks1_partitions, no_tpm2_partitions)
else:
- required_crypt_rpms = [
- 'clevis',
- 'clevis-dracut',
- 'clevis-systemd',
- 'clevis-udisks2',
- 'clevis-luks',
- 'cryptsetup',
- 'tpm2-tss',
- 'tpm2-tools',
- 'tpm2-abrmd'
- ]
- api.produce(TargetUserSpaceUpgradeTasks(
- copy_files=[CopyFile(src="/etc/crypttab")],
- install_rpms=required_crypt_rpms)
- )
- api.produce(UpgradeInitramfsTasks(
- include_files=['/etc/crypttab'],
- include_dracut_modules=[
- DracutModule(name='clevis'),
- DracutModule(name='clevis-pin-tpm2')
- ])
- )
+ ok_partitions.append(luks_dump.device_name)
+
+ if luks1_partitions or no_tpm2_partitions:
+ report_inhibitor(luks1_partitions, no_tpm2_partitions)
+ elif ok_partitions:
+ required_crypt_rpms = [
+ 'clevis',
+ 'clevis-dracut',
+ 'clevis-systemd',
+ 'clevis-udisks2',
+ 'clevis-luks',
+ 'cryptsetup',
+ 'tpm2-tss',
+ 'tpm2-tools',
+ 'tpm2-abrmd'
+ ]
+ api.produce(TargetUserSpaceUpgradeTasks(
+ copy_files=[CopyFile(src="/etc/crypttab")],
+ install_rpms=required_crypt_rpms)
+ )
+ api.produce(UpgradeInitramfsTasks(
+ include_files=['/etc/crypttab'],
+ include_dracut_modules=[
+ DracutModule(name='clevis'),
+ DracutModule(name='clevis-pin-tpm2')
+ ])
+ )
--
2.54.0

View File

@ -0,0 +1,256 @@
From 5e4cba2a334b7ff2f3413f0d67f316b86a945fe5 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Thu, 7 May 2026 10:41:20 +0200
Subject: [PATCH 062/105] checkluks: remove rd.luks.uuid from the upgrade entry
Request removal of the rd.luks.uuid cmdline args from the upgrade boot
entry. These args filter which devices in /etc/cryptab can be unlocked
during early stages of the boot process. As we want to mount more
devices (essentially everything in /etc/cryptab), presence of these args
is undesired.
Jira-ref: RHEL-145136
---
.../common/actors/checkluks/actor.py | 21 +++++-
.../actors/checkluks/libraries/checkluks.py | 44 ++++++++++--
.../actors/checkluks/tests/test_checkluks.py | 67 +++++++++++++++++--
3 files changed, 118 insertions(+), 14 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkluks/actor.py b/repos/system_upgrade/common/actors/checkluks/actor.py
index 2ea16985..e382be98 100644
--- a/repos/system_upgrade/common/actors/checkluks/actor.py
+++ b/repos/system_upgrade/common/actors/checkluks/actor.py
@@ -1,6 +1,15 @@
from leapp.actors import Actor
from leapp.libraries.actor.checkluks import check_invalid_luks_devices
-from leapp.models import CephInfo, LuksDumps, StorageInfo, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks
+from leapp.models import (
+ CephInfo,
+ KernelCmdline,
+ LuksDumps,
+ StorageInfo,
+ TargetKernelCmdlineArgTasks,
+ TargetUserSpaceUpgradeTasks,
+ UpgradeInitramfsTasks,
+ UpgradeKernelCmdlineArgTasks
+)
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -15,8 +24,14 @@ class CheckLuks(Actor):
"""
name = 'check_luks'
- consumes = (CephInfo, LuksDumps, StorageInfo)
- produces = (Report, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks)
+ consumes = (CephInfo, KernelCmdline, LuksDumps, StorageInfo)
+ produces = (
+ Report,
+ TargetKernelCmdlineArgTasks,
+ TargetUserSpaceUpgradeTasks,
+ UpgradeInitramfsTasks,
+ UpgradeKernelCmdlineArgTasks
+ )
tags = (ChecksPhaseTag, IPUWorkflowTag)
def process(self):
diff --git a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
index cdf86d08..8db1db19 100644
--- a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
+++ b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
@@ -6,9 +6,12 @@ from leapp.models import (
CephInfo,
CopyFile,
DracutModule,
+ KernelCmdline,
LuksDumps,
+ TargetKernelCmdlineArgTasks,
TargetUserSpaceUpgradeTasks,
- UpgradeInitramfsTasks
+ UpgradeInitramfsTasks,
+ UpgradeKernelCmdlineArgTasks
)
from leapp.reporting import create_report
@@ -103,6 +106,41 @@ def report_inhibitor(luks1_partitions, no_tpm2_partitions):
] + report_hints)
+def _emit_rdluks_undesired_for_upgrade_cmdline():
+ """
+ Identify rd.luks.uuid args on the source kernel cmdline and request their
+ removal from the upgrade boot entry.
+
+ The upgrade initramfs uses clevis-tpm2 to unlock LUKS devices. Dracut's
+ rd.luks.uuid mechanism is not needed and can cause problems if left in.
+ This mechanism limits what devices will be unlocked during boot, i.e., only
+ devices named in the rd.luks.uuid args will be unlocked. However, we want
+ to unlock all LUKS devices (at least those in /etc/crypttab). Therefore, we
+ request that rd.luks.uuid args be removed from the upgrade kernel cmdline.
+ """
+ cmdline = next(api.consume(KernelCmdline), None)
+ if not cmdline:
+ api.current_logger().debug('No KernelCmdline message received, nothing to do.')
+ return
+
+ rdluks_args = [arg for arg in cmdline.parameters if arg.key == 'rd.luks.uuid']
+
+ if not rdluks_args:
+ api.current_logger().debug('No rd.luks.uuid args found on the source kernel cmdline.')
+ return
+
+ api.current_logger().debug(
+ 'Requesting removal of rd.luks.uuid args from the upgrade kernel cmdline: %s',
+ ['{}={}'.format(a.key, a.value) for a in rdluks_args]
+ )
+ api.produce(UpgradeKernelCmdlineArgTasks(to_remove=rdluks_args))
+
+ # When installing the target kernel RPM, the cmdline is copied from the booted system.
+ # As we remove the rd.luks.uuid args, we would accidentally remove them also from the
+ # target entry. Therefore, we add them back in here.
+ api.produce(TargetKernelCmdlineArgTasks(to_add=rdluks_args))
+
+
def check_invalid_luks_devices():
luks_dumps = next(api.consume(LuksDumps), None)
if not luks_dumps:
@@ -123,9 +161,6 @@ def check_invalid_luks_devices():
luks1_partitions.append(luks_dump.device_name)
elif luks_dump.version == 2 and not _at_least_one_tpm_token(luks_dump):
no_tpm2_partitions.append(luks_dump.device_name)
-
- if luks1_partitions or no_tpm2_partitions:
- report_inhibitor(luks1_partitions, no_tpm2_partitions)
else:
ok_partitions.append(luks_dump.device_name)
@@ -154,3 +189,4 @@ def check_invalid_luks_devices():
DracutModule(name='clevis-pin-tpm2')
])
)
+ _emit_rdluks_undesired_for_upgrade_cmdline()
diff --git a/repos/system_upgrade/common/actors/checkluks/tests/test_checkluks.py b/repos/system_upgrade/common/actors/checkluks/tests/test_checkluks.py
index 0147c4f8..0e13e09f 100644
--- a/repos/system_upgrade/common/actors/checkluks/tests/test_checkluks.py
+++ b/repos/system_upgrade/common/actors/checkluks/tests/test_checkluks.py
@@ -1,13 +1,19 @@
+from leapp.libraries.actor import checkluks
from leapp.libraries.common import distro
-from leapp.libraries.common.config import version
+from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
from leapp.models import (
CephInfo,
+ KernelCmdline,
+ KernelCmdlineArg,
LsblkEntry,
LuksDump,
LuksDumps,
LuksToken,
+ TargetKernelCmdlineArgTasks,
TargetUserSpaceUpgradeTasks,
- UpgradeInitramfsTasks
+ UpgradeInitramfsTasks,
+ UpgradeKernelCmdlineArgTasks
)
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
@@ -17,7 +23,7 @@ _REPORT_TITLE_UNSUITABLE = 'Detected LUKS devices unsuitable for in-place upgrad
def test_actor_with_luks1_notpm(monkeypatch, current_actor_context):
- monkeypatch.setattr(version, 'get_source_major_version', lambda: '8')
+ monkeypatch.setattr(checkluks, 'get_source_major_version', lambda: '8')
monkeypatch.setattr(distro, 'get_source_distro_id', lambda: 'rhel')
monkeypatch.setattr(distro, 'get_target_distro_id', lambda: 'rhel')
@@ -41,7 +47,7 @@ def test_actor_with_luks1_notpm(monkeypatch, current_actor_context):
def test_actor_with_luks2_notpm(monkeypatch, current_actor_context):
- monkeypatch.setattr(version, 'get_source_major_version', lambda: '8')
+ monkeypatch.setattr(checkluks, 'get_source_major_version', lambda: '8')
luks_dump = LuksDump(
version=2,
uuid='27b57c75-9adf-4744-ab04-9eb99726a301',
@@ -62,7 +68,7 @@ def test_actor_with_luks2_notpm(monkeypatch, current_actor_context):
def test_actor_with_luks2_invalid_token(monkeypatch, current_actor_context):
- monkeypatch.setattr(version, 'get_source_major_version', lambda: '8')
+ monkeypatch.setattr(checkluks, 'get_source_major_version', lambda: '8')
luks_dump = LuksDump(
version=2,
uuid='dc1dbe37-6644-4094-9839-8fc5dcbec0c6',
@@ -84,7 +90,7 @@ def test_actor_with_luks2_invalid_token(monkeypatch, current_actor_context):
def test_actor_with_luks2_clevis_tpm_token(monkeypatch, current_actor_context):
- monkeypatch.setattr(version, 'get_source_major_version', lambda: '8')
+ monkeypatch.setattr(checkluks, 'get_source_major_version', lambda: '8')
luks_dump = LuksDump(
version=2,
uuid='83050bd9-61c6-4ff0-846f-bfd3ac9bfc67',
@@ -113,7 +119,7 @@ def test_actor_with_luks2_clevis_tpm_token(monkeypatch, current_actor_context):
def test_actor_with_luks2_ceph(monkeypatch, current_actor_context):
- monkeypatch.setattr(version, 'get_source_major_version', lambda: '8')
+ monkeypatch.setattr(checkluks, 'get_source_major_version', lambda: '8')
ceph_volume = ['sda']
current_actor_context.feed(CephInfo(encrypted_volumes=ceph_volume))
luks_dump = LuksDump(
@@ -143,3 +149,50 @@ LSBLK_ENTRY = LsblkEntry(
parent_name="",
parent_path=""
)
+
+
+def test_rdluks_uuid_args_removed_from_upgrade_cmdline(monkeypatch):
+ cmdline = KernelCmdline(parameters=[
+ KernelCmdlineArg(key='root', value='/dev/mapper/rhel-root'),
+ KernelCmdlineArg(key='rd.luks.uuid', value='luks-aaa-bbb'),
+ KernelCmdlineArg(key='rd.luks.uuid', value='luks-ccc-ddd'),
+ KernelCmdlineArg(key='ro', value=None),
+ ])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkluks._emit_rdluks_undesired_for_upgrade_cmdline()
+
+ upgrade_msgs = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert len(upgrade_msgs) == 1
+
+ removed_keys_values = {(arg.key, arg.value) for arg in upgrade_msgs[0].to_remove}
+ assert removed_keys_values == {
+ ('rd.luks.uuid', 'luks-aaa-bbb'),
+ ('rd.luks.uuid', 'luks-ccc-ddd'),
+ }
+
+ target_msgs = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert len(target_msgs) == 1
+
+ readded_keys_values = {(arg.key, arg.value) for arg in target_msgs[0].to_add}
+ assert readded_keys_values == {
+ ('rd.luks.uuid', 'luks-aaa-bbb'),
+ ('rd.luks.uuid', 'luks-ccc-ddd'),
+ }
+
+
+def test_no_rdluks_uuid_no_message(monkeypatch):
+ cmdline = KernelCmdline(parameters=[
+ KernelCmdlineArg(key='root', value='/dev/mapper/rhel-root'),
+ KernelCmdlineArg(key='ro', value=None),
+ ])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ checkluks._emit_rdluks_undesired_for_upgrade_cmdline()
+
+ upgrade_msgs = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert not upgrade_msgs
+ target_msgs = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert not target_msgs
--
2.54.0

View File

@ -0,0 +1,114 @@
From 7d0bf3eed770e3cbaf48bfacbc1c11943f3a63c8 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Mon, 11 May 2026 18:28:32 +0200
Subject: [PATCH 063/105] conftest: exit actor context when switching repos
Prior to this patch, the current_actor_context fixture was always exited
when executing a new actor. Exiting the fixture causes sys.meta_path to
be restored to a state when the fixture was first entered. Therefore, if
we are running tests across multiple repositories, say common and el9toel10,
then when we are executing the first actor from el9toel10 we execute
__exit__ of the last executed actor from the common repository. So the
following would happen:
1) Finish running the last actor named 'X' from the common repository.
2) Load el9toel10 repository, appending to sys.meta_path loaders for
leapp.libraries.common, etc.
3) Discovery of the tests of the first actor from el9toel10.
4) Detect that we are executing a different actor than 'X', so call
cleanup, which restores the state of sys.meta_path. Therefore, this
effectively removes all loaders for el9toel10 repository from
sys.meta_path.
5) Execute tests for the first actor from el9toel10.
This patch ensures that we call current_actor_context.__exit__ whenever
we are about to start loading a new repository, preventing us from
unwanted removal of the loaders in sys.meta_path (added by the loaded
repository).
---
conftest.py | 47 ++++++++++++++++++++++++++++++++++++-----------
1 file changed, 36 insertions(+), 11 deletions(-)
diff --git a/conftest.py b/conftest.py
index 5da5cc5f..542e22cb 100644
--- a/conftest.py
+++ b/conftest.py
@@ -12,40 +12,65 @@ logging.getLogger("parso").setLevel(logging.INFO)
def _load_and_add_repo(manager, repo_path):
- repo = find_and_scan_repositories(
+ manager_with_new_repos = find_and_scan_repositories(
repo_path,
include_locals=True
)
- unloaded = set()
- loaded = {r.repo_id for r in manager.repos}
- if hasattr(repo, 'repos'):
- for repo in repo.repos:
+
+ newly_discovered_repo_ids = set()
+ already_known_repoids = {r.repo_id for r in manager.repos}
+ if hasattr(manager_with_new_repos, 'repos'):
+ for repo in manager_with_new_repos.repos:
if not manager.repo_by_id(repo.repo_id):
manager.add_repo(repo)
- unloaded.add(repo.repo_id)
+ newly_discovered_repo_ids.add(repo.repo_id)
else:
manager.add_repo(repo)
- if not loaded:
+ if not already_known_repoids:
manager.load(skip_actors_discovery=True)
else:
- for repo_id in unloaded:
+ for repo_id in newly_discovered_repo_ids:
manager.repo_by_id(repo_id).load(skip_actors_discovery=True)
+def _cleanup_actor_context_from_session(collector):
+ is_actor_context_attached = hasattr(collector.session, "current_actor_path")
+
+ if not is_actor_context_attached:
+ return # Nothing to clean
+
+ try:
+ collector.session.current_actor_context.__exit__(
+ None, None, None
+ )
+ delattr(collector.session, 'current_actor_path')
+ collector.session.current_actor_context = None
+ collector.session.current_actor = None
+ except AttributeError:
+ pass
+
+
def pytest_collectstart(collector):
if collector.nodeid:
current_repo_basedir = find_repository_basedir(str(collector.fspath))
if not current_repo_basedir:
# This is not a repository
return
+
if not hasattr(collector.session, "leapp_repository"):
collector.session.leapp_repository = RepositoryManager()
collector.session.repo_base_dir = current_repo_basedir
_load_and_add_repo(collector.session.leapp_repository, current_repo_basedir)
else:
- if not collector.session.leapp_repository.repo_by_id(
- get_repository_id(current_repo_basedir)
- ):
+ repo_id = get_repository_id(current_repo_basedir)
+ repo = collector.session.leapp_repository.repo_by_id(repo_id)
+ if not repo:
+ # We are about to load a new repository, which will append module loaders
+ # to sys.meta_path. Cleaning actor context will restore the meta_path used
+ # when the context was entered, so if we did the cleanup later we would
+ # undo appends to the meta_path.
+ _cleanup_actor_context_from_session(collector)
+
_load_and_add_repo(collector.session.leapp_repository, current_repo_basedir)
# we're forcing the actor context switch only when traversing new
--
2.54.0

View File

@ -0,0 +1,52 @@
From 8b5840dd94efca6a30ec1b1ba5ac75bb015e4dac Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 4 May 2026 14:41:41 +0200
Subject: [PATCH 064/105] testutils: logger_mocked: accepts kwargs
For logger calls specifying kw like: exc_info, stack_info, ...
methods in logger_mocked were broken. Allow to specify **kwargs so
the mocking is allowed. If any kw is specified, it's ignored in the
mocked call at this moment as we do not have use for it in tests.
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
repos/system_upgrade/common/libraries/testutils.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/testutils.py b/repos/system_upgrade/common/libraries/testutils.py
index 0a56d698..c9d6337a 100644
--- a/repos/system_upgrade/common/libraries/testutils.py
+++ b/repos/system_upgrade/common/libraries/testutils.py
@@ -45,23 +45,23 @@ class logger_mocked:
self.warnmsg = []
self.errmsg = []
- def debug(self, *args):
+ def debug(self, *args, **kwargs):
self.dbgmsg.extend(args)
- def info(self, *args):
+ def info(self, *args, **kwargs):
self.infomsg.extend(args)
@deprecated(since='2020-09-23', message=(
'The logging.warn method has been deprecated since Python 3.3.'
'Use the warning method instead.'
))
- def warn(self, *args):
+ def warn(self, *args, **kwargs):
self.warnmsg.extend(args)
- def warning(self, *args):
+ def warning(self, *args, **kwargs):
self.warnmsg.extend(args)
- def error(self, *args):
+ def error(self, *args, **kwargs):
self.errmsg.extend(args)
def __call__(self):
--
2.54.0

View File

@ -0,0 +1,52 @@
From d3e20f73aca107a48ae7ee5734bd6a8542dfca96 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 4 May 2026 15:10:04 +0000
Subject: [PATCH 065/105] conftest: Add leapp_tmpdir fixture for standardized
temp directories
Introduces a project-wide pytest fixture that ensures all tests create
temporary directories with the 'leapptest-' prefix in /tmp, maintaining
consistency across the test suite and simplifying cleanup.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
conftest.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/conftest.py b/conftest.py
index 542e22cb..f3d02e74 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,5 +1,9 @@
import logging
import os
+import shutil
+import tempfile
+
+import pytest
from leapp.repository.manager import RepositoryManager
from leapp.repository.scan import find_and_scan_repositories
@@ -121,3 +125,18 @@ def pytest_runtestloop(session):
)
except AttributeError:
pass
+
+
+@pytest.fixture
+def leapp_tmpdir():
+ """
+ Create a temporary directory with 'leapptest-' prefix in /tmp.
+
+ This fixture automatically creates and cleans up a temporary directory
+ that follows the project's naming convention for test directories.
+
+ :returns: Path to the temporary directory
+ :rtype: str
+ """
+ with tempfile.TemporaryDirectory(prefix='leapptest-', dir='/tmp') as tmpdir:
+ yield tmpdir
--
2.54.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,256 @@
From f60f6af718e6d8e1459022374be836b2850add03 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 10 Mar 2026 10:49:38 +0000
Subject: [PATCH 067/105] Update imports to use new dnflibs modules
Update all actors and tests to import DNF libraries from the new
dnflibs package location instead of the deprecated root paths.
Changes:
- Update dnfplugin imports to use dnflibs.dnfplugin
- Update module imports to use dnflibs.dnfmodule
- Affects 9 actors and 3 test files
This completes the migration to the new library structure, ensuring
all internal code uses the new import paths while maintaining backward
compatibility through deprecation shims.
Assisted-by: Claude Sonnet 4.5
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
docs/source/libraries-and-api/deprecations-list.md | 7 +++----
.../common/actors/applytransactionworkarounds/actor.py | 2 +-
.../tests/unit_test_applytransactionworkarounds.py | 2 +-
repos/system_upgrade/common/actors/dnfdryrun/actor.py | 2 +-
.../common/actors/dnfpackagedownload/actor.py | 2 +-
.../common/actors/dnftransactioncheck/actor.py | 2 +-
.../common/actors/dnfupgradetransaction/actor.py | 2 +-
.../common/actors/getenabledmodules/actor.py | 2 +-
.../libraries/upgradeinitramfsgenerator.py | 3 ++-
.../common/actors/rpmscanner/libraries/rpmscanner.py | 4 ++--
.../common/actors/rpmscanner/tests/test_rpmscanner.py | 10 +++++-----
.../targetuserspacecreator/libraries/userspacegen.py | 3 ++-
.../common/libraries/tests/test_dnfplugin.py | 2 +-
13 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/docs/source/libraries-and-api/deprecations-list.md b/docs/source/libraries-and-api/deprecations-list.md
index aa20f874..301de558 100644
--- a/docs/source/libraries-and-api/deprecations-list.md
+++ b/docs/source/libraries-and-api/deprecations-list.md
@@ -19,10 +19,9 @@ Only the versions in which a deprecation has been made are listed.
- Models:
- **`RenamedInterfaces`** - Information provided in this message is not always complete and it's not used since the `net.naming-scheme` kernel command line argument is set during the upgrade.
- Shared libraries
- - DNF-related libraries have been reorganized into the `leapp.libraries.common.dnflibs` package. The following modules have been moved:
- - **`leapp.libraries.common.dnfconfig`** - Moved to `leapp.libraries.common.dnflibs.dnfconfig`. All public functions (`exclude_leapp_rpms`) are deprecated in the old location.
- - **`leapp.libraries.common.dnfplugin`** - Moved to `leapp.libraries.common.dnflibs.dnfplugin`. All public functions (`install`, `build_plugin_data`, `create_config`, `backup_config`, `backup_debug_data`, `apply_workarounds`, `install_initramdisk_requirements`, `perform_transaction_install`, `perform_transaction_check`, `perform_rpm_download`, `perform_dry_run`) and constants (`DNF_PLUGIN_NAME`, `DNF_PLUGIN_PATH`, `DNF_PLUGIN_DATA_NAME`, `DNF_PLUGIN_DATA_PATH`, `DNF_PLUGIN_DATA_LOG_PATH`, `DNF_DEBUG_DATA_PATH`) are available in the new location.
- - **`leapp.libraries.common.module`** - Moved to `leapp.libraries.common.dnflibs.dnfmodule` (renamed for clarity). All public functions (`get_modules`, `get_enabled_modules`, `map_installed_rpms_to_modules`) are deprecated in the old location.
+ - **`leapp.libraries.common.dnfconfig`** - Moved to `leapp.libraries.common.dnflibs.dnfconfig`. Original library is deprecated.
+ - **`leapp.libraries.common.dnfplugin`** - Moved to `leapp.libraries.common.dnflibs.dnfplugin`. Original library is deprecated.
+ - **`leapp.libraries.common.module`** - Replaced by `leapp.libraries.common.dnflibs.dnfmodule` (renamed for clarity). Original library is deprecated.
## v0.24.0 <span style="font-size:0.5em; font-weight:normal">(till September 2026)</span>
diff --git a/repos/system_upgrade/common/actors/applytransactionworkarounds/actor.py b/repos/system_upgrade/common/actors/applytransactionworkarounds/actor.py
index f1514dd1..00fb4c4c 100644
--- a/repos/system_upgrade/common/actors/applytransactionworkarounds/actor.py
+++ b/repos/system_upgrade/common/actors/applytransactionworkarounds/actor.py
@@ -1,5 +1,5 @@
from leapp.actors import Actor
-from leapp.libraries.common import dnfplugin
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.models import DNFWorkaround
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag
diff --git a/repos/system_upgrade/common/actors/applytransactionworkarounds/tests/unit_test_applytransactionworkarounds.py b/repos/system_upgrade/common/actors/applytransactionworkarounds/tests/unit_test_applytransactionworkarounds.py
index 96b8094f..92d316b8 100644
--- a/repos/system_upgrade/common/actors/applytransactionworkarounds/tests/unit_test_applytransactionworkarounds.py
+++ b/repos/system_upgrade/common/actors/applytransactionworkarounds/tests/unit_test_applytransactionworkarounds.py
@@ -1,6 +1,6 @@
import os
-from leapp.libraries.common.dnfplugin import api, apply_workarounds, mounting
+from leapp.libraries.common.dnflibs.dnfplugin import api, apply_workarounds, mounting
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.models import DNFWorkaround
diff --git a/repos/system_upgrade/common/actors/dnfdryrun/actor.py b/repos/system_upgrade/common/actors/dnfdryrun/actor.py
index bc3267b4..1f1d6bd1 100644
--- a/repos/system_upgrade/common/actors/dnfdryrun/actor.py
+++ b/repos/system_upgrade/common/actors/dnfdryrun/actor.py
@@ -1,5 +1,5 @@
from leapp.actors import Actor
-from leapp.libraries.common import dnfplugin
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.models import (
BootContent,
DNFPluginTask,
diff --git a/repos/system_upgrade/common/actors/dnfpackagedownload/actor.py b/repos/system_upgrade/common/actors/dnfpackagedownload/actor.py
index b54f5627..796b21d4 100644
--- a/repos/system_upgrade/common/actors/dnfpackagedownload/actor.py
+++ b/repos/system_upgrade/common/actors/dnfpackagedownload/actor.py
@@ -1,5 +1,5 @@
from leapp.actors import Actor
-from leapp.libraries.common import dnfplugin
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.models import (
DNFPluginTask,
DNFWorkaround,
diff --git a/repos/system_upgrade/common/actors/dnftransactioncheck/actor.py b/repos/system_upgrade/common/actors/dnftransactioncheck/actor.py
index b545d1ce..49f9bfe7 100644
--- a/repos/system_upgrade/common/actors/dnftransactioncheck/actor.py
+++ b/repos/system_upgrade/common/actors/dnftransactioncheck/actor.py
@@ -1,5 +1,5 @@
from leapp.actors import Actor
-from leapp.libraries.common import dnfplugin
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.models import (
DNFPluginTask,
DNFWorkaround,
diff --git a/repos/system_upgrade/common/actors/dnfupgradetransaction/actor.py b/repos/system_upgrade/common/actors/dnfupgradetransaction/actor.py
index 2e069296..7bf8ab23 100644
--- a/repos/system_upgrade/common/actors/dnfupgradetransaction/actor.py
+++ b/repos/system_upgrade/common/actors/dnfupgradetransaction/actor.py
@@ -1,7 +1,7 @@
import shutil
from leapp.actors import Actor
-from leapp.libraries.common import dnfplugin
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.libraries.stdlib import run
from leapp.models import (
DNFPluginTask,
diff --git a/repos/system_upgrade/common/actors/getenabledmodules/actor.py b/repos/system_upgrade/common/actors/getenabledmodules/actor.py
index 71cefef6..38f458ef 100644
--- a/repos/system_upgrade/common/actors/getenabledmodules/actor.py
+++ b/repos/system_upgrade/common/actors/getenabledmodules/actor.py
@@ -1,5 +1,5 @@
from leapp.actors import Actor
-from leapp.libraries.common.module import get_enabled_modules
+from leapp.libraries.common.dnflibs.dnfmodule import get_enabled_modules
from leapp.models import EnabledModules, Module
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
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 1a0dccd8..aec1debf 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
@@ -4,8 +4,9 @@ import shutil
from collections import namedtuple
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common import dnfplugin, mounting
+from leapp.libraries.common import mounting
from leapp.libraries.common.config.version import get_target_major_version
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import RequiredUpgradeInitramPackages # deprecated
from leapp.models import UpgradeDracutModule # deprecated
diff --git a/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py b/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
index 2a2ee03e..ebf17e19 100644
--- a/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
+++ b/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
@@ -1,8 +1,8 @@
import warnings
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common import module as module_lib
from leapp.libraries.common import rpms
+from leapp.libraries.common.dnflibs import dnfmodule
from leapp.libraries.stdlib import api
from leapp.models import InstalledRPM, RPM
@@ -100,7 +100,7 @@ def map_modular_rpms_to_modules():
"""
Map modular packages to the module streams they come from.
"""
- modules = module_lib.get_modules()
+ modules = dnfmodule.get_modules()
# empty on RHEL 7 because of no modules
if not modules:
return {}
diff --git a/repos/system_upgrade/common/actors/rpmscanner/tests/test_rpmscanner.py b/repos/system_upgrade/common/actors/rpmscanner/tests/test_rpmscanner.py
index 151a1b2b..9955aff7 100644
--- a/repos/system_upgrade/common/actors/rpmscanner/tests/test_rpmscanner.py
+++ b/repos/system_upgrade/common/actors/rpmscanner/tests/test_rpmscanner.py
@@ -4,8 +4,8 @@ import pytest
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import rpmscanner
-from leapp.libraries.common import module as module_lib
from leapp.libraries.common import rpms, testutils
+from leapp.libraries.common.dnflibs import dnfmodule
from leapp.libraries.stdlib import api
from leapp.models import InstalledRPM, RPM
from leapp.snactor.fixture import current_actor_context
@@ -103,20 +103,20 @@ MODULES = [
@pytest.mark.skipif(no_yum and no_dnf, reason='yum/dnf is unavailable')
def test_actor_execution(monkeypatch, current_actor_context):
- monkeypatch.setattr(rpmscanner.module_lib, 'get_modules', lambda: [])
+ monkeypatch.setattr(rpmscanner.dnfmodule, 'get_modules', lambda: [])
current_actor_context.run()
assert current_actor_context.consume(InstalledRPM)
assert current_actor_context.consume(InstalledRPM)[0].items
def test_map_modular_rpms_to_modules_empty(monkeypatch):
- monkeypatch.setattr(module_lib, 'get_modules', lambda: [])
+ monkeypatch.setattr(dnfmodule, 'get_modules', lambda: [])
mapping = rpmscanner.map_modular_rpms_to_modules()
assert not mapping
def test_map_modular_rpms_to_modules(monkeypatch):
- monkeypatch.setattr(module_lib, 'get_modules', lambda: MODULES)
+ monkeypatch.setattr(dnfmodule, 'get_modules', lambda: MODULES)
mapping = rpmscanner.map_modular_rpms_to_modules()
assert mapping[
('afterburn', '0', '4.2.0', '1.module_f31+6825+8330d585', 'x86_64')
@@ -154,7 +154,7 @@ PACKAGE_REPOS = {
def test_process(monkeypatch):
- monkeypatch.setattr(module_lib, 'get_modules', lambda: MODULES)
+ monkeypatch.setattr(dnfmodule, 'get_modules', lambda: MODULES)
monkeypatch.setattr(rpmscanner, 'get_package_repository_data', lambda: PACKAGE_REPOS)
monkeypatch.setattr(rpms, 'get_installed_rpms', lambda: INSTALLED_RPMS)
monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
index 2475aad4..a776deac 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
@@ -6,7 +6,7 @@ import shutil
from leapp import reporting
from leapp.exceptions import StopActorExecution, StopActorExecutionError
from leapp.libraries.actor import constants
-from leapp.libraries.common import distro, dnfplugin, mounting, overlaygen, repofileutils, rhsm, utils
+from leapp.libraries.common import distro, mounting, overlaygen, repofileutils, rhsm, utils
from leapp.libraries.common.config import (
get_env,
get_product_type,
@@ -19,6 +19,7 @@ from leapp.libraries.common.config.version import (
get_target_major_version,
get_target_version
)
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.libraries.common.gpg import get_path_to_gpg_certs, is_nogpgcheck_set
from leapp.libraries.stdlib import api, CalledProcessError, config, run
from leapp.models import RequiredTargetUserspacePackages # deprecated
diff --git a/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py b/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
index 1ca95945..2f220785 100644
--- a/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
@@ -3,8 +3,8 @@ from collections import namedtuple
import pytest
import leapp.models
-from leapp.libraries.common import dnfplugin
from leapp.libraries.common.config.version import get_major_version
+from leapp.libraries.common.dnflibs import dnfplugin
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models.fields import Boolean
--
2.54.0

View File

@ -0,0 +1,35 @@
From 1b6beff15bf6e5c23feb74ab5bc16ea4c526597b Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 4 May 2026 14:46:05 +0200
Subject: [PATCH 068/105] dnflibs: introduce DNFError exception
This is generic exception used for all DNF related errors raised
in dnflibs libraries. IOW, all exceptions introduced in these libraries
are based on DNFError, so if wanted, it's possible to catch a DNFError
exception in calling functions instead of listing each possible DNF
exception separately.
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
repos/system_upgrade/common/libraries/dnflibs/__init__.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/repos/system_upgrade/common/libraries/dnflibs/__init__.py b/repos/system_upgrade/common/libraries/dnflibs/__init__.py
index d94e4b4c..1aef3295 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/__init__.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/__init__.py
@@ -6,3 +6,11 @@ This package consolidates DNF functionality previously scattered across:
- leapp.libraries.common.dnfplugin -> dnflibs.dnfplugin
- leapp.libraries.common.module -> dnflibs.dnfmodule
"""
+
+from leapp.exceptions import StopActorExecutionError
+
+
+class DNFError(StopActorExecutionError):
+ """
+ Generic exception inherited by all DNF errors raised in dnflibs libraries.
+ """
--
2.54.0

View File

@ -0,0 +1,48 @@
From 7d576c6d427d748e43c71e9ee031f556810fbb36 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 10 Mar 2026 16:11:47 +0100
Subject: [PATCH 069/105] dnflibs.dnfplugin: Drop dead code:
_handle_transaction_err_msg_old
This function is not used anywhere anymore. Let's just remove it.
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/libraries/dnflibs/dnfplugin.py | 21 -------------------
1 file changed, 21 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
index 32407283..b0e10054 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
@@ -145,27 +145,6 @@ def backup_debug_data(context):
api.current_logger().warning('Failed to copy debugdata. Message: {}'.format(str(e)), exc_info=True)
-def _handle_transaction_err_msg_old(stage, xfs_info, err):
- # NOTE(pstodulk): This is going to be removed in future!
- message = 'DNF execution failed with non zero exit code.'
- details = {'STDOUT': err.stdout, 'STDERR': err.stderr}
-
- if 'more space needed on the' in err.stderr and stage != 'upgrade':
- # Disk Requirements:
- # At least <size> more space needed on the <path> filesystem.
- #
- article_section = 'Generic case'
- if xfs_info.present and xfs_info.without_ftype:
- article_section = 'XFS ftype=0 case'
-
- message = ('There is not enough space on the file system hosting /var/lib/leapp directory '
- 'to extract the packages.')
- details = {'hint': "Please follow the instructions in the '{}' section of the article at: "
- "link: https://access.redhat.com/solutions/5057391".format(article_section)}
-
- raise StopActorExecutionError(message=message, details=details)
-
-
def _handle_transaction_err_msg(err, is_container=False):
NO_SPACE_STR = 'more space needed on the'
if NO_SPACE_STR not in err.stderr:
--
2.54.0

View File

@ -0,0 +1,107 @@
From e84fe5ebffc05e5f44d431ff770c0910131a6343 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 10 Mar 2026 18:24:46 +0100
Subject: [PATCH 070/105] dnflibs.dnfplugin: Drop deprecated DNF_PLUGIN_PATH
This has been deprecated for a long time - dropping it finally to
clean the code a little bit after the moving to a new path.
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/libraries/dnflibs/dnfplugin.py | 49 ++++++-------------
.../common/libraries/dnfplugin.py | 3 +-
2 files changed, 17 insertions(+), 35 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
index b0e10054..43179138 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
@@ -14,54 +14,37 @@ from leapp.libraries.stdlib import api, CalledProcessError, config
from leapp.models import DNFWorkaround
DNF_PLUGIN_NAME = 'rhel_upgrade.py'
-_DEDICATED_URL = 'https://access.redhat.com/solutions/7011704'
-
-
-class _DnfPluginPathStr(str):
- _PATHS = {
- "9": os.path.join('/lib/python3.9/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
- "10": os.path.join('/lib/python3.12/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
- }
+DNF_PLUGIN_DATA_NAME = 'dnf-plugin-data.txt'
+DNF_PLUGIN_DATA_PATH = os.path.join('/var/lib/leapp', DNF_PLUGIN_DATA_NAME)
+DNF_PLUGIN_DATA_LOG_PATH = os.path.join('/var/log/leapp', DNF_PLUGIN_DATA_NAME)
+DNF_DEBUG_DATA_PATH = '/var/log/leapp/dnf-debugdata/'
- def __init__(self): # noqa: W0231; pylint: disable=super-init-not-called
- self.data = ""
+_DEDICATED_URL = 'https://access.redhat.com/solutions/7011704'
+_DNF_PLUGIN_PATHS = {
+ '9': os.path.join('/lib/python3.9/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
+ '10': os.path.join('/lib/python3.12/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
+}
- def _feed(self):
- major = get_target_major_version()
- if major not in _DnfPluginPathStr._PATHS:
- raise KeyError('{} is not a supported target version of RHEL'.format(major))
- self.data = _DnfPluginPathStr._PATHS[major]
- def __str__(self):
- self._feed()
- return str(self.data)
- def __repr__(self):
- self._feed()
- return repr(self.data)
- def lstrip(self, chars=None):
- self._feed()
- return self.data.lstrip(chars)
-# Deprecated
-DNF_PLUGIN_PATH = _DnfPluginPathStr()
-DNF_PLUGIN_DATA_NAME = 'dnf-plugin-data.txt'
-DNF_PLUGIN_DATA_PATH = os.path.join('/var/lib/leapp', DNF_PLUGIN_DATA_NAME)
-DNF_PLUGIN_DATA_LOG_PATH = os.path.join('/var/log/leapp', DNF_PLUGIN_DATA_NAME)
-DNF_DEBUG_DATA_PATH = '/var/log/leapp/dnf-debugdata/'
def install(target_basedir):
"""
Installs our plugin to the DNF plugins.
"""
+ # NOTE(pstodulk): Keeping KeyError unhandled as this can happen only
+ # for the new major upgrade path.
+ tgt_plugin_path = os.path.join(
+ target_basedir,
+ _DNF_PLUGIN_PATHS[get_target_major_version()].lstrip('/')
+ )
try:
- shutil.copy2(
- api.get_file_path(DNF_PLUGIN_NAME),
- os.path.join(target_basedir, DNF_PLUGIN_PATH.lstrip('/')))
+ shutil.copy2(api.get_file_path(DNF_PLUGIN_NAME), tgt_plugin_path)
except EnvironmentError as e:
api.current_logger().debug('Failed to install DNF plugin', exc_info=True)
raise StopActorExecutionError(
diff --git a/repos/system_upgrade/common/libraries/dnfplugin.py b/repos/system_upgrade/common/libraries/dnfplugin.py
index 713cc34b..19e4d4e2 100644
--- a/repos/system_upgrade/common/libraries/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnfplugin.py
@@ -10,9 +10,8 @@ This shim will be removed in a future version. Please update imports to:
from leapp.libraries.common.dnflibs import dnfplugin as _dnfplugin
from leapp.utils.deprecation import deprecated
-# Re-export constants (not deprecated)
+# Re-export constants
DNF_PLUGIN_NAME = _dnfplugin.DNF_PLUGIN_NAME
-DNF_PLUGIN_PATH = _dnfplugin.DNF_PLUGIN_PATH
DNF_PLUGIN_DATA_NAME = _dnfplugin.DNF_PLUGIN_DATA_NAME
DNF_PLUGIN_DATA_PATH = _dnfplugin.DNF_PLUGIN_DATA_PATH
DNF_PLUGIN_DATA_LOG_PATH = _dnfplugin.DNF_PLUGIN_DATA_LOG_PATH
--
2.54.0

View File

@ -0,0 +1,903 @@
From e5f867a809db5ae9e671cb4a5c02eb2b88f3bf39 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 10 Mar 2026 18:27:18 +0100
Subject: [PATCH 071/105] dnflibs.dnfplugin: Introduce new DNF exceptions and
tests
Originally the library raised usually StopActorExecutionError exception
in case of issues. But as we agreed recently, we want to start to
raise rather proper exceptions in libraries and handle this better
in callers (actors).
For the comfort (and sake of safety) the newly created exception
classes are based on the DNFError class, which is based on StopActorExecutionError,
so these exceptions will be properly handled also if they are not caught
correctly in actors. Also, in case of this library we kind of know
that we are the only callers of these functions as they are fundamental
for the upgrade process and majority of them can be executed just once
per the upgrade process.
Docstrings in public functions have been properly updated to reflect
the change. This includes also docstrings of other public functions
in the library.
List of new exceptions:
* DNFPluginInstallError
* DNFExecutionError
* DNFUpgradeTransactionError
* RegisteredWorkaroundApplicationError
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/libraries/dnflibs/dnfplugin.py | 194 +++++++++++-
.../libraries/dnflibs/tests/test_dnfplugin.py | 294 ++++++++++++++++++
.../common/libraries/tests/test_dnfplugin.py | 193 ------------
3 files changed, 475 insertions(+), 206 deletions(-)
create mode 100644 repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfplugin.py
delete mode 100644 repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
index 43179138..cb810a2a 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
@@ -8,7 +8,7 @@ import shutil
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import guards, mounting, overlaygen, rhsm, utils
from leapp.libraries.common.config.version import get_target_major_version, get_target_version
-from leapp.libraries.common.dnflibs import dnfconfig
+from leapp.libraries.common.dnflibs import dnfconfig, DNFError
from leapp.libraries.common.gpg import is_nogpgcheck_set
from leapp.libraries.stdlib import api, CalledProcessError, config
from leapp.models import DNFWorkaround
@@ -26,16 +26,43 @@ _DNF_PLUGIN_PATHS = {
}
+class DNFPluginInstallError(DNFError):
+ """
+ Raised when cannot install the rhel_upgrade DNF plugin.
+ """
+
+
+class DNFExecutionError(DNFError):
+ """
+ Raised when cannot execute DNF.
+ """
+class DNFUpgradeTransactionError(DNFError):
+ """
+ Raised when an error with the DNF upgrade transaction occurs.
+ This covers all stages of the upgrade, from the initial calculation in
+ preupgrade phases to the final execution of the transaction in the
+ upgrade environment.
+ """
+class RegisteredWorkaroundApplicationError(StopActorExecutionError):
+ """
+ Raised when a registered workaround cannot be applied or the application failed.
+ """
def install(target_basedir):
"""
- Installs our plugin to the DNF plugins.
+ Installs the rhel_upgrade plugin to the DNF plugin under specified container path.
+
+ :param target_basedir: The target userspace container base directory
+ :type target_basedir: str
+ :raises DNFPluginInstallError: When cannot install the DNF plugin.
+ :raises KeyError: When the installation DNF plugin path is not defined for
+ the target system (yet).
"""
# NOTE(pstodulk): Keeping KeyError unhandled as this can happen only
# for the new major upgrade path.
@@ -47,7 +74,7 @@ def install(target_basedir):
shutil.copy2(api.get_file_path(DNF_PLUGIN_NAME), tgt_plugin_path)
except EnvironmentError as e:
api.current_logger().debug('Failed to install DNF plugin', exc_info=True)
- raise StopActorExecutionError(
+ raise DNFPluginInstallError(
message='Failed to install DNF plugin. Error: {}'.format(str(e))
)
@@ -55,6 +82,11 @@ def install(target_basedir):
def _rebuild_rpm_db(context, root=None):
"""
Convert rpmdb from BerkeleyDB to Sqlite
+
+ :param context: Execution context
+ :type context: mounting.IsolatedActions
+ :param root: The system root dir for which the rpmdb should be rebuilt
+ :type root: str
"""
base_cmd = ['rpmdb', '--rebuilddb']
cmd = base_cmd if not root else base_cmd + ['-r', root]
@@ -64,6 +96,19 @@ def _rebuild_rpm_db(context, root=None):
def build_plugin_data(target_repoids, debug, test, tasks, on_aws):
"""
Generates a dictionary with the DNF plugin data.
+
+ :param target_repoids: Target repositories to use for the upgrade
+ :type target_repoids: list
+ :param debug: Whether debug data should be produced
+ :type debug: bool
+ :param test: Flag whether the test of the DNF transaction should be performed
+ :type test: bool
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param on_aws: Whether running on AWS infrastructure
+ :type on_aws: bool
+ :returns: Dictionary with data for the DNF plugin
+ :rtype: dict
"""
# get list of repo IDs of target repositories that should be used for upgrade
data = {
@@ -99,6 +144,19 @@ def build_plugin_data(target_repoids, debug, test, tasks, on_aws):
def create_config(context, target_repoids, debug, test, tasks, on_aws=False):
"""
Creates the configuration data file for our DNF plugin.
+
+ :param context: Execution context
+ :type context: mounting.IsolatedActions
+ :param target_repoids: Target repositories to use for the upgrade
+ :type target_repoids: list
+ :param debug: Whether debug data should be produced
+ :type debug: bool
+ :param test: Flag whether the test of the DNF transaction should be performed
+ :type test: bool
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param on_aws: Whether running on AWS infrastructure
+ :type on_aws: bool
"""
context.makedirs(os.path.dirname(DNF_PLUGIN_DATA_PATH), exists_ok=True)
with context.open(DNF_PLUGIN_DATA_PATH, 'w+') as f:
@@ -111,6 +169,9 @@ def create_config(context, target_repoids, debug, test, tasks, on_aws=False):
def backup_config(context):
"""
Backs up the configuration data used for the plugin.
+
+ :param context: Execution context
+ :type context: mounting.IsolatedActions
"""
context.copy_from(DNF_PLUGIN_DATA_PATH, DNF_PLUGIN_DATA_LOG_PATH)
@@ -118,6 +179,9 @@ def backup_config(context):
def backup_debug_data(context):
"""
Performs the backup of DNF debug data
+
+ :param context: Execution context
+ :type context: mounting.IsolatedActions
"""
if config.is_debug():
# The debugdata is a folder generated by dnf when using the --debugsolver dnf option. We switch on the
@@ -155,7 +219,7 @@ def _handle_transaction_err_msg(err, is_container=False):
"for your current system to pass the early phases of the upgrade process."
)
details = {'STDOUT': err.stdout, 'STDERR': err.stderr, 'hint': proxy_hint}
- raise StopActorExecutionError(message=message, details=details)
+ raise DNFUpgradeTransactionError(message=message, details=details)
# Disk Requirements:
# At least <size> more space needed on the <path> filesystem.
@@ -189,7 +253,7 @@ def _handle_transaction_err_msg(err, is_container=False):
)
details = {'hint': hint, 'Disk Requirements': '\n'.join(missing_space)}
- raise StopActorExecutionError(message=message, details=details)
+ raise DNFUpgradeTransactionError(message=message, details=details)
def _transaction(context, stage, target_repoids, tasks, plugin_info, xfs_info,
@@ -198,7 +262,6 @@ def _transaction(context, stage, target_repoids, tasks, plugin_info, xfs_info,
Perform the actual DNF rpm download via our DNF plugin
"""
- # we do not want
if stage not in ['dry-run', 'upgrade']:
create_config(
context=context,
@@ -259,7 +322,7 @@ def _transaction(context, stage, target_repoids, tasks, plugin_info, xfs_info,
)
except OSError as e:
api.current_logger().error('Could not call dnf command: Message: %s', str(e), exc_info=True)
- raise StopActorExecutionError(
+ raise DNFExecutionError(
message='Failed to execute dnf. Reason: {}'.format(str(e))
)
except CalledProcessError as e:
@@ -283,8 +346,17 @@ def _prepare_transaction(used_repos, target_userspace_info, binds=()):
def apply_workarounds(context=None):
"""
Apply registered workarounds in the given context environment
+
+ Note this function consumes DNFWorkaround messages! An actor calling this
+ function must list the DNFWorkaround in the list of consumable messages.
+
+ :param context: Execution context (defaults to NotIsolatedActions)
+ :type context: mounting.IsolatedActions | None
+ :raises RegisteredWorkaroundApplicationError: When a workaround script fails to execute.
"""
context = context or mounting.NotIsolatedActions(base_dir='/')
+ # FIXME(pstodulk): add check that actor is consuming DNFWorkaround, and raise
+ # new error if it is not.
for workaround in api.consume(DNFWorkaround):
try:
api.show_message('Applying transaction workaround - {}'.format(workaround.display_name))
@@ -297,15 +369,28 @@ def apply_workarounds(context=None):
cmd_str = workaround.script_path
context.call(['/bin/bash', '-c', cmd_str])
except (OSError, CalledProcessError) as e:
- raise StopActorExecutionError(
- message=('Failed to execute script to apply transaction workaround {display_name}.'
- ' Message: {error}'.format(error=str(e), display_name=workaround.display_name))
+ raise RegisteredWorkaroundApplicationError(
+ message=f'Failed to execute script to apply transaction workaround {workaround.display_name}.',
+ details={
+ 'error message': str(e),
+ 'workaround name': workaround.display_name
+ }
)
def install_initramdisk_requirements(packages, target_userspace_info, used_repos):
"""
Performs the installation of packages into the initram disk
+
+ :param packages: List of package names to install
+ :type packages: list[str]
+ :param target_userspace_info: Information about the target userspace
+ :type target_userspace_info: TargetUserSpaceInfo
+ :param used_repos: Target repositories to use for the installation
+ :type used_repos: Iterable[UsedTargetRepositories]
+ :raises DNFUpgradeTransactionError: When the DNF transaction fails (insufficient disk space,
+ package conflicts, or repository access issues).
+
"""
mount_binds = ['/:/installroot']
with _prepare_transaction(used_repos=used_repos, target_userspace_info=target_userspace_info,
@@ -346,6 +431,25 @@ def install_initramdisk_requirements(packages, target_userspace_info, used_repos
def perform_transaction_install(target_userspace_info, storage_info, used_repos, tasks, plugin_info, xfs_info):
"""
Performs the actual installation with the DNF rhel-upgrade plugin using the target userspace
+
+ :param target_userspace_info: Information about the target userspace
+ :type target_userspace_info: TargetUserSpaceInfo
+ :param storage_info: Storage and filesystem information
+ :type storage_info: StorageInfo
+ :param used_repos: Target repositories to use for the upgrade
+ :type used_repos: UsedTargetRepositories
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param plugin_info: DNF plugin configuration
+ :type plugin_info: list[DNFPluginTask]
+ :param xfs_info: XFS filesystem information
+ :type xfs_info: XFSPresence
+ :raises DNFExecutionError: When the DNF command fails to execute.
+ :raises DNFUpgradeTransactionError: When the upgrade transaction encounters an error
+ (insufficient disk space, package conflicts, etc.).
+
+ .. seealso::
+ :func:`dnfconfig.exclude_leapp_rpms` - For DNF configuration exceptions
"""
stage = 'upgrade'
@@ -398,7 +502,7 @@ def perform_transaction_install(target_userspace_info, storage_info, used_repos,
if int(get_target_major_version()) >= 9:
_rebuild_rpm_db(context, root='/installroot')
_transaction(
- context=context, stage='upgrade', target_repoids=target_repoids, plugin_info=plugin_info,
+ context=context, stage=stage, target_repoids=target_repoids, plugin_info=plugin_info,
xfs_info=xfs_info, tasks=tasks, cmd_prefix=cmd_prefix
)
@@ -435,6 +539,27 @@ def perform_transaction_check(target_userspace_info,
target_iso=None):
"""
Perform DNF transaction check using our plugin
+
+ :param target_userspace_info: Information about the target userspace
+ :type target_userspace_info: TargetUserSpaceInfo
+ :param storage_info: Storage and filesystem information
+ :type storage_info: StorageInfo
+ :param used_repos: Target repositories to use for the upgrade
+ :type used_repos: UsedTargetRepositories
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param plugin_info: DNF plugin configuration
+ :type plugin_info: list[DNFPluginTask]
+ :param xfs_info: XFS filesystem information
+ :type xfs_info: XFSPresence
+ :param target_iso: Optional path to target ISO image
+ :type target_iso: TargetOSInstallationImage | None
+ :raises DNFExecutionError: When the DNF command fails to execute.
+ :raises DNFUpgradeTransactionError: When the transaction check encounters an error
+ (package dependency conflicts, repository issues, etc.).
+
+ .. seealso::
+ :func:`dnfconfig.exclude_leapp_rpms` - For DNF configuration exceptions
"""
stage = 'check'
@@ -451,7 +576,7 @@ def perform_transaction_check(target_userspace_info,
dnfconfig.exclude_leapp_rpms(context, disable_plugins)
_transaction(
- context=context, stage='check', target_repoids=target_repoids, plugin_info=plugin_info, xfs_info=xfs_info,
+ context=context, stage=stage, target_repoids=target_repoids, plugin_info=plugin_info, xfs_info=xfs_info,
tasks=tasks
)
@@ -466,6 +591,29 @@ def perform_rpm_download(target_userspace_info,
on_aws=False):
"""
Perform RPM download including the transaction test using dnf with our plugin
+
+ :param target_userspace_info: Information about the target userspace
+ :type target_userspace_info: TargetUserSpaceInfo
+ :param storage_info: Storage and filesystem information
+ :type storage_info: StorageInfo
+ :param used_repos: Target repositories to use for the upgrade
+ :type used_repos: UsedTargetRepositories
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param plugin_info: DNF plugin configuration
+ :type plugin_info: list[DNFPluginTask]
+ :param xfs_info: XFS filesystem information
+ :type xfs_info: XFSPresence
+ :param target_iso: Optional path to target ISO image
+ :type target_iso: TargetOSInstallationImage | None
+ :param on_aws: Whether running on AWS infrastructure
+ :type on_aws: bool
+ :raises DNFExecutionError: When the DNF command fails to execute.
+ :raises DNFUpgradeTransactionError: When the RPM download or transaction test fails
+ (insufficient disk space, network issues, repository problems).
+
+ .. seealso::
+ :func:`dnfconfig.exclude_leapp_rpms` - For DNF configuration exceptions
"""
stage = 'download'
@@ -485,7 +633,7 @@ def perform_rpm_download(target_userspace_info,
apply_workarounds(overlay.nspawn())
dnfconfig.exclude_leapp_rpms(context, disable_plugins)
_transaction(
- context=context, stage='download', target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks,
+ context=context, stage=stage, target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks,
test=True, on_aws=on_aws, xfs_info=xfs_info
)
@@ -500,6 +648,26 @@ def perform_dry_run(target_userspace_info,
on_aws=False):
"""
Perform the dnf transaction test / dry-run using only cached data.
+
+ :param target_userspace_info: Information about the target userspace
+ :type target_userspace_info: TargetUserSpaceInfo
+ :param storage_info: Storage and filesystem information
+ :type storage_info: StorageInfo
+ :param used_repos: Target repositories to use for the upgrade
+ :type used_repos: UsedTargetRepositories
+ :param tasks: Explicit RPM transaction tasks for the DNF plugin
+ :type tasks: FilteredRpmTransactionTasks
+ :param plugin_info: DNF plugin configuration
+ :type plugin_info: list[DNFPluginTask]
+ :param xfs_info: XFS filesystem information
+ :type xfs_info: XFSPresence
+ :param target_iso: Optional path to target ISO image
+ :type target_iso: TargetOSInstallationImage | None
+ :param on_aws: Whether running on AWS infrastructure
+ :type on_aws: bool
+ :raises DNFExecutionError: When the DNF command fails to execute.
+ :raises DNFUpgradeTransactionError: When the dry-run transaction test encounters an error
+ (package dependency conflicts, insufficient disk space).
"""
with _prepare_perform(used_repos=used_repos,
target_userspace_info=target_userspace_info,
diff --git a/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfplugin.py b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfplugin.py
new file mode 100644
index 00000000..18908934
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfplugin.py
@@ -0,0 +1,294 @@
+import os
+from unittest.mock import MagicMock
+
+import pytest
+
+from leapp.libraries.common.dnflibs import dnfplugin
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+from leapp.models import DNFWorkaround, Module
+
+
+class MockContext:
+ def __init__(self, should_raise=None):
+ self.should_raise = should_raise
+ self.makedirs_calls = []
+ self.open_calls = []
+ self.copy_from_calls = []
+ self.copytree_from_calls = []
+ self.call_calls = []
+ self._files = {}
+
+ def makedirs(self, path, exists_ok=False):
+ self.makedirs_calls.append((path, exists_ok))
+
+ def open(self, path, mode):
+ self.open_calls.append((path, mode))
+ mock_file = MagicMock()
+ self._files[path] = mock_file
+ return mock_file
+
+ def copy_from(self, src, dst):
+ self.copy_from_calls.append((src, dst))
+
+ def copytree_from(self, src, dst):
+ self.copytree_from_calls.append((src, dst))
+ if self.should_raise:
+ raise self.should_raise
+
+ def call(self, cmd, **kwargs):
+ self.call_calls.append(cmd)
+ if self.should_raise:
+ raise self.should_raise
+
+
+class MockTasks:
+ def __init__(self):
+ self.local_rpms = ['/path/to/rpm1.rpm', '/path/to/rpm2.rpm']
+ self.to_install = ['pkg1', 'pkg2']
+ self.to_remove = ['old-pkg1', 'old-pkg2']
+ self.to_upgrade = ['upgrade-pkg1', 'upgrade-pkg2']
+ self.modules_to_enable = []
+ self.modules_to_reset = []
+
+
+@pytest.mark.parametrize('target_version', ['9', '10'])
+def test_install_success(monkeypatch, leapp_tmpdir, target_version):
+ target_basedir = leapp_tmpdir
+
+ plugin_source = os.path.join(leapp_tmpdir, dnfplugin.DNF_PLUGIN_NAME)
+ target_plugin_dir = os.path.join(
+ target_basedir,
+ dnfplugin._DNF_PLUGIN_PATHS[target_version].lstrip('/')
+ )
+
+ def mocked_copy2(src, dst):
+ assert plugin_source == src
+ assert target_plugin_dir == dst
+
+ def mocked_get_file_path(fpath):
+ assert fpath == dnfplugin.DNF_PLUGIN_NAME
+ return plugin_source
+
+ monkeypatch.setattr(dnfplugin, 'get_target_major_version', lambda: target_version)
+ monkeypatch.setattr(api, 'get_file_path', mocked_get_file_path)
+ monkeypatch.setattr(dnfplugin.shutil, 'copy2', mocked_copy2)
+
+ dnfplugin.install(target_basedir)
+
+
+def test_install_failure(monkeypatch, leapp_tmpdir):
+ target_basedir = leapp_tmpdir
+
+ monkeypatch.setattr(dnfplugin, 'get_target_major_version', lambda: '9')
+ monkeypatch.setattr(api, 'get_file_path', lambda name: '/nonexistent/file')
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ with pytest.raises(dnfplugin.DNFPluginInstallError) as exc_info:
+ dnfplugin.install(target_basedir)
+
+ assert 'Failed to install DNF plugin' in str(exc_info.value)
+
+
+def test_install_unsupported_version(monkeypatch, leapp_tmpdir):
+ target_basedir = leapp_tmpdir
+
+ def mocked_copy2(dummy_src, dummy_dst):
+ assert False
+
+ monkeypatch.setattr(dnfplugin, 'get_target_major_version', lambda: '99')
+ monkeypatch.setattr(dnfplugin.shutil, 'copy2', mocked_copy2)
+
+ with pytest.raises(KeyError):
+ dnfplugin.install(target_basedir)
+
+
+@pytest.mark.parametrize('debug,test,on_aws', [
+ (True, False, False),
+ (False, True, True),
+ (True, True, False),
+])
+def test_build_plugin_data(monkeypatch, debug, test, on_aws):
+ tasks = MockTasks()
+ module1 = Module(name='nodejs', stream='18')
+ module2 = Module(name='postgresql', stream='15')
+ tasks.modules_to_enable = [module1, module2]
+
+ target_repoids = ['rhel-9-baseos', 'rhel-9-appstream']
+
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='9.3'))
+ monkeypatch.setattr(dnfplugin, 'is_nogpgcheck_set', lambda: False)
+
+ data = dnfplugin.build_plugin_data(target_repoids, debug, test, tasks, on_aws)
+
+ assert data['pkgs_info']['local_rpms'] == ['/installroot/path/to/rpm1.rpm', '/installroot/path/to/rpm2.rpm']
+ assert data['pkgs_info']['to_install'] == ['pkg1', 'pkg2']
+ assert data['pkgs_info']['to_remove'] == ['old-pkg1', 'old-pkg2']
+ assert data['pkgs_info']['to_upgrade'] == ['upgrade-pkg1', 'upgrade-pkg2']
+ assert 'nodejs:18' in data['pkgs_info']['modules_to_enable']
+ assert 'postgresql:15' in data['pkgs_info']['modules_to_enable']
+
+ assert data['dnf_conf']['debugsolver'] is debug
+ assert data['dnf_conf']['enable_repos'] == target_repoids
+ assert data['dnf_conf']['platform_id'] == 'platform:el9'
+ assert data['dnf_conf']['releasever'] == '9.3'
+ assert data['dnf_conf']['test_flag'] is test
+ assert data['dnf_conf']['gpgcheck'] is True
+
+ assert data['rhui']['aws']['on_aws'] is on_aws
+
+
+def test_build_plugin_data_with_nogpgcheck(monkeypatch):
+ tasks = MockTasks()
+
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='9.3'))
+ monkeypatch.setattr(dnfplugin, 'is_nogpgcheck_set', lambda: True)
+
+ data = dnfplugin.build_plugin_data([], False, False, tasks, False)
+
+ assert data['dnf_conf']['gpgcheck'] is False
+
+
+def test_create_config(monkeypatch):
+ context = MockContext()
+ tasks = MockTasks()
+ target_repoids = ['repo1', 'repo2']
+
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='9.3'))
+ monkeypatch.setattr(dnfplugin, 'is_nogpgcheck_set', lambda: False)
+
+ dnfplugin.create_config(context, target_repoids, debug=True, test=False, tasks=tasks, on_aws=False)
+
+ assert len(context.makedirs_calls) == 1
+ assert context.makedirs_calls[0][0] == os.path.dirname(dnfplugin.DNF_PLUGIN_DATA_PATH)
+
+ assert len(context.open_calls) == 1
+ assert context.open_calls[0][0] == dnfplugin.DNF_PLUGIN_DATA_PATH
+ assert context.open_calls[0][1] == 'w+'
+
+
+def test_backup_config():
+ context = MockContext()
+
+ dnfplugin.backup_config(context)
+
+ assert len(context.copy_from_calls) == 1
+ assert context.copy_from_calls[0] == (dnfplugin.DNF_PLUGIN_DATA_PATH, dnfplugin.DNF_PLUGIN_DATA_LOG_PATH)
+
+
+def test_backup_debug_data_with_debug(monkeypatch):
+ context = MockContext()
+
+ monkeypatch.setattr(dnfplugin.config, 'is_debug', lambda: True)
+
+ dnfplugin.backup_debug_data(context)
+
+ assert len(context.copytree_from_calls) == 1
+ assert context.copytree_from_calls[0][0] == '/debugdata'
+
+
+def test_backup_debug_data_without_debug(monkeypatch):
+ context = MockContext()
+
+ monkeypatch.setattr(dnfplugin.config, 'is_debug', lambda: False)
+
+ dnfplugin.backup_debug_data(context)
+
+ assert len(context.copytree_from_calls) == 0
+
+
+def test_backup_debug_data_oserror(monkeypatch):
+ context = MockContext(should_raise=OSError('Permission denied'))
+
+ monkeypatch.setattr(dnfplugin.config, 'is_debug', lambda: True)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ dnfplugin.backup_debug_data(context)
+
+
+@pytest.mark.parametrize('stderr,is_container,expected_in_message', [
+ ('At least 500MB more space needed on the / filesystem.', True, '500MB'),
+ ('At least 500MB more space needed on the /usr filesystem.', False, '/usr'),
+ ('At least 200MB more space needed on the /var filesystem.', False, '/var'),
+])
+def test_handle_transaction_err_msg_no_space(stderr, is_container, expected_in_message):
+ err = CalledProcessError(
+ 'dnf failed',
+ ['dnf', 'upgrade'],
+ {
+ 'stdout': 'stdout',
+ 'stderr': 'Error: Transaction test error:\n Disk Requirements:\n {}'.format(stderr),
+ 'exit_code': 1
+ }
+ )
+
+ with pytest.raises(dnfplugin.DNFUpgradeTransactionError) as exc_info:
+ dnfplugin._handle_transaction_err_msg(err, is_container=is_container)
+
+ assert 'not enough space' in str(exc_info.value).lower()
+ hint = str(exc_info.value.details.get('hint', ''))
+ disk_reqs = str(exc_info.value.details.get('Disk Requirements', ''))
+ assert expected_in_message in (hint + disk_reqs)
+
+
+def test_handle_transaction_err_msg_generic_error():
+ err = CalledProcessError(
+ 'dnf failed',
+ ['dnf', 'upgrade'],
+ {'stdout': 'stdout output', 'stderr': 'Some other DNF error', 'exit_code': 1}
+ )
+
+ with pytest.raises(dnfplugin.DNFUpgradeTransactionError) as exc_info:
+ dnfplugin._handle_transaction_err_msg(err, is_container=False)
+
+ assert 'DNF execution failed' in str(exc_info.value)
+ assert exc_info.value.details['STDOUT'] == 'stdout output'
+ assert exc_info.value.details['STDERR'] == 'Some other DNF error'
+ assert 'proxy' in exc_info.value.details['hint'].lower()
+
+
+def test_apply_workarounds_success(monkeypatch):
+ workaround1 = DNFWorkaround(
+ display_name='Workaround 1',
+ script_path='/path/to/script1.sh',
+ script_args=[]
+ )
+ workaround2 = DNFWorkaround(
+ display_name='Workaround 2',
+ script_path='/path/to/script2.sh',
+ script_args=['arg1', 'arg2']
+ )
+
+ context = MockContext()
+
+ monkeypatch.setattr(api, 'consume', lambda model: [workaround1, workaround2])
+ monkeypatch.setattr(api, 'show_message', lambda msg: None)
+
+ dnfplugin.apply_workarounds(context)
+
+ assert len(context.call_calls) == 2
+ assert context.call_calls[0] == ['/bin/bash', '-c', '/path/to/script1.sh']
+ assert context.call_calls[1] == ['/bin/bash', '-c', '/path/to/script2.sh arg1 arg2']
+
+
+def test_apply_workarounds_script_fails(monkeypatch):
+ workaround = DNFWorkaround(
+ display_name='Failing Workaround',
+ script_path='/path/to/failing.sh',
+ script_args=[]
+ )
+
+ context = MockContext(should_raise=CalledProcessError(
+ 'Script failed',
+ ['/bin/bash', '-c', '/path/to/failing.sh'],
+ {'exit_code': 1, 'stdout': '', 'stderr': 'Script error'}
+ ))
+
+ monkeypatch.setattr(api, 'consume', lambda model: [workaround])
+ monkeypatch.setattr(api, 'show_message', lambda msg: None)
+
+ with pytest.raises(dnfplugin.RegisteredWorkaroundApplicationError) as exc_info:
+ dnfplugin.apply_workarounds(context)
+
+ assert 'Failed to execute script' in str(exc_info.value)
+ assert 'Failing Workaround' in exc_info.value.details['workaround name']
diff --git a/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py b/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
deleted file mode 100644
index 2f220785..00000000
--- a/repos/system_upgrade/common/libraries/tests/test_dnfplugin.py
+++ /dev/null
@@ -1,193 +0,0 @@
-from collections import namedtuple
-
-import pytest
-
-import leapp.models
-from leapp.libraries.common.config.version import get_major_version
-from leapp.libraries.common.dnflibs import dnfplugin
-from leapp.libraries.common.testutils import CurrentActorMocked
-from leapp.libraries.stdlib import api
-from leapp.models.fields import Boolean
-from leapp.topics import Topic
-
-
-class DATADnfPluginDataTopic(Topic):
- name = 'data_dnf_plugin_data'
-
-
-fields = leapp.models.fields
-
-TaskData = namedtuple('TaskData', 'expected initdata')
-
-TEST_INSTALL_PACKAGES = TaskData(
- expected=('install1', 'install2'),
- initdata=('install1', 'install2')
-)
-TEST_REMOVE_PACKAGES = TaskData(
- expected=('remove1', 'remove2'),
- initdata=('remove1', 'remove2'),
-)
-TEST_UPGRADE_PACKAGES = TaskData(
- expected=('upgrade1', 'upgrade2'),
- initdata=('upgrade1', 'upgrade2'),
-)
-TEST_ENABLE_MODULES = TaskData(
- expected=('enable1:stream1', 'enable2:stream2'),
- initdata=(
- leapp.models.Module(name='enable1', stream='stream1'),
- leapp.models.Module(name='enable2', stream='stream2'),
- )
-)
-
-
-class DATADnfPluginDataPkgsInfo(leapp.models.Model):
- topic = DATADnfPluginDataTopic
- local_rpms = fields.List(fields.String())
- to_install = fields.List(fields.StringEnum(choices=TEST_INSTALL_PACKAGES.expected))
- to_remove = fields.List(fields.StringEnum(choices=TEST_REMOVE_PACKAGES.expected))
- to_upgrade = fields.List(fields.StringEnum(choices=TEST_UPGRADE_PACKAGES.expected))
- modules_to_enable = fields.List(fields.StringEnum(choices=TEST_ENABLE_MODULES.expected))
-
-
-TEST_ENABLE_REPOS_CHOICES = ('enabled_repo', 'BASEOS', 'APPSTREAM')
-
-
-class BooleanEnum(fields.EnumMixin, Boolean):
- pass
-
-
-class DATADnfPluginDataDnfConf(leapp.models.Model):
- topic = DATADnfPluginDataTopic
- allow_erasing = BooleanEnum(choices=[True])
- best = BooleanEnum(choices=[True])
- debugsolver = fields.Boolean()
- disable_repos = BooleanEnum(choices=[True])
- enable_repos = fields.List(fields.StringEnum(choices=TEST_ENABLE_REPOS_CHOICES))
- gpgcheck = fields.Boolean()
- platform_id = fields.StringEnum(choices=['platform:el8', 'platform:el9'])
- releasever = fields.String()
- installroot = fields.StringEnum(choices=['/installroot'])
- test_flag = fields.Boolean()
-
-
-class DATADnfPluginDataRHUIAWS(leapp.models.Model):
- topic = DATADnfPluginDataTopic
- on_aws = fields.Boolean()
- region = fields.Nullable(fields.String())
-
-
-class DATADnfPluginDataRHUI(leapp.models.Model):
- topic = DATADnfPluginDataTopic
- aws = fields.Model(DATADnfPluginDataRHUIAWS)
-
-
-class DATADnfPluginData(leapp.models.Model):
- topic = DATADnfPluginDataTopic
- pkgs_info = fields.Model(DATADnfPluginDataPkgsInfo)
- dnf_conf = fields.Model(DATADnfPluginDataDnfConf)
- rhui = fields.Model(DATADnfPluginDataRHUI)
-
-
-# Delete those models from leapp.models to 'unpolute' the module
-del leapp.models.DATADnfPluginDataPkgsInfo
-del leapp.models.DATADnfPluginDataDnfConf
-del leapp.models.DATADnfPluginDataRHUI
-del leapp.models.DATADnfPluginDataRHUIAWS
-del leapp.models.DATADnfPluginData
-
-
-_CONFIG_BUILD_TEST_DEFINITION = (
- # Parameter, Input Data, Expected Fields with data
- ('debug', False, ('dnf_conf', 'debugsolver'), False),
- ('debug', True, ('dnf_conf', 'debugsolver'), True),
- ('target_repoids', TEST_ENABLE_REPOS_CHOICES, ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES)),
- ('target_repoids', TEST_ENABLE_REPOS_CHOICES[0:1],
- ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[0:1])),
- ('target_repoids', TEST_ENABLE_REPOS_CHOICES[1:],
- ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[1:])),
- ('target_repoids', TEST_ENABLE_REPOS_CHOICES[2:],
- ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[2:])),
- ('test', False, ('dnf_conf', 'test_flag'), False),
- ('test', True, ('dnf_conf', 'test_flag'), True),
-)
-
-
-@pytest.mark.parametrize('used_target_version', ['8.4', '8.5', '9.0', '9.1'])
-@pytest.mark.parametrize('parameter,input_value,test_path,expected_value', _CONFIG_BUILD_TEST_DEFINITION)
-def test_build_plugin_data_variations(
- monkeypatch,
- used_target_version,
- parameter,
- input_value,
- test_path,
- expected_value,
-):
- used_target_major_version = get_major_version(used_target_version)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver=used_target_version))
- inputs = {
- 'target_repoids': ['BASEOS', 'APPSTREAM'],
- 'debug': True,
- 'test': True,
- 'on_aws': False,
- 'tasks': leapp.models.FilteredRpmTransactionTasks(
- to_install=TEST_INSTALL_PACKAGES.initdata,
- to_remove=TEST_REMOVE_PACKAGES.initdata,
- to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
- modules_to_enable=TEST_ENABLE_MODULES.initdata
- )
- }
- inputs[parameter] = input_value
- created = DATADnfPluginData.create(
- dnfplugin.build_plugin_data(
- **inputs
- )
- )
- assert created.dnf_conf.platform_id == 'platform:el{}'.format(used_target_major_version)
- assert created.dnf_conf.releasever == used_target_version
- value = created
- for path in test_path:
- value = getattr(value, path)
- assert value == expected_value
-
-
-def test_build_plugin_data(monkeypatch):
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.4'))
- # Use leapp to validate format and data
- created = DATADnfPluginData.create(
- dnfplugin.build_plugin_data(
- target_repoids=['BASEOS', 'APPSTREAM'],
- debug=True,
- test=True,
- on_aws=False,
- tasks=leapp.models.FilteredRpmTransactionTasks(
- to_install=TEST_INSTALL_PACKAGES.initdata,
- to_remove=TEST_REMOVE_PACKAGES.initdata,
- to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
- modules_to_enable=TEST_ENABLE_MODULES.initdata
- )
- )
- )
- assert created.dnf_conf.debugsolver is True
- assert created.dnf_conf.test_flag is True
- assert created.rhui.aws.on_aws is False
-
- with pytest.raises(fields.ModelViolationError):
- DATADnfPluginData.create(
- dnfplugin.build_plugin_data(
- target_repoids=['BASEOS', 'APPSTREAM'],
- debug=True,
- test=True,
- on_aws=False,
- tasks=leapp.models.FilteredRpmTransactionTasks(
- to_install=TEST_INSTALL_PACKAGES.initdata,
- to_remove=TEST_REMOVE_PACKAGES.initdata,
- to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
- # Enforcing the failure
- modules_to_enable=(
- leapp.models.Module(
- name='broken', stream=None
- ),
- ),
- )
- )
- )
--
2.54.0

View File

@ -0,0 +1,637 @@
From a48283cb02eeca31847b5501698d259979efc78b Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Wed, 11 Mar 2026 18:42:48 +0100
Subject: [PATCH 072/105] dnflibs/dnfmodule: Move init of dnf.Base to dnflibs
and add tests
Introduce `create__dnf_base()` function in the `dnflibs` package.
The function replaces original private function
`dnflibs.dnfmodule._create_or_get_dnf_base`.
The correct creation and initialisation of the dnf.Base object is
non-trivial. To cover the initialisation correctly for various setups
(including also systems using RHUI which requires additional transparent
DNF plugin) we make this function public so it can be used in actors.
The sack in dnf.Base object is filled and prepared for use.
The function can raise `DNFRepoError` exception, based on
`DNFError`, if a repository cannot be loaded properly,
providing additional details about the problem.
NOTE that included tests provides just trivial coverate as there
is not a good way to cover this library by unit-tests without full
mock of DNF and hawkey libraries - if we want to keep test independent
on systems where they are executed. Keeping the valid testing on CI
pipelines with integration tests.
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/libraries/dnflibs/__init__.py | 67 +++++
.../common/libraries/dnflibs/dnfmodule.py | 71 ++---
.../libraries/dnflibs/tests/test_dnflibs.py | 118 ++++++++
.../libraries/dnflibs/tests/test_dnfmodule.py | 270 ++++++++++++++++++
4 files changed, 483 insertions(+), 43 deletions(-)
create mode 100644 repos/system_upgrade/common/libraries/dnflibs/tests/test_dnflibs.py
create mode 100644 repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfmodule.py
diff --git a/repos/system_upgrade/common/libraries/dnflibs/__init__.py b/repos/system_upgrade/common/libraries/dnflibs/__init__.py
index 1aef3295..787119a7 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/__init__.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/__init__.py
@@ -7,10 +7,77 @@ This package consolidates DNF functionality previously scattered across:
- leapp.libraries.common.module -> dnflibs.dnfmodule
"""
+import warnings
+
from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.common.config.version import get_source_major_version
+
+try:
+ import dnf
+except ImportError:
+ dnf = None
+ warnings.warn('Could not import the `dnf` python module.', ImportWarning)
class DNFError(StopActorExecutionError):
"""
Generic exception inherited by all DNF errors raised in dnflibs libraries.
"""
+
+
+class DNFRepoError(DNFError):
+ """
+ Used when DNF fails to load repositories.
+ """
+
+
+def create_dnf_base():
+ """
+ Create properly initialized dnf.Base with filled sack.
+
+ The proper initialisation of dnf.Base object is non-trivial and order of
+ operations matters - we made plenty of mistakes already before the function
+ got to this state, covering various setups and systems. So use it instead
+ of trying to do so manually.
+
+ :returns: Initialized dnf.Base object with filled sack
+ :rtype: dnf.Base
+ :raises DNFRepoError: When a repository cannot be loaded
+ """
+ # The DNF command reads /etc/dnf/vars/releasever, but the DNF library does not. It parses redhat-release
+ # package to retrieve system's major version which it then uses as $releasever. However, some systems might
+ # have repositories only for the exact system version (including the minor number). In a case when
+ # /etc/dnf/vars/releasever is present, read its contents so that we can access repositores on such systems.
+ conf = dnf.conf.Conf()
+
+ # preload releasever from what we know, this will be our fallback
+ conf.substitutions['releasever'] = get_source_major_version()
+
+ # load all substitutions from etc
+ conf.substitutions.update_from_etc('/')
+
+ base = dnf.Base(conf=conf)
+ base.conf.read()
+ base.init_plugins()
+ base.read_all_repos()
+
+ # configure plugins after the repositories are loaded
+ # e.g. the amazon-id plugin requires loaded repositories
+ # for the proper configuration.
+ base.configure_plugins()
+
+ try:
+ base.fill_sack()
+ except dnf.exceptions.RepoError as e:
+ err_msg = str(e)
+ repoid = err_msg.split('repo:')[-1].strip() if 'repo:' in err_msg else 'unknown repo'
+ repoid = repoid.strip('"').strip("'").replace('\\"', '')
+ raise DNFRepoError(
+ message='DNF failed to load repositories: {}'.format(str(e)),
+ details={
+ 'hint': 'Ensure the {} repository definition is correct or remove it '
+ 'if the repository is not needed anymore.'.format(repoid)
+ }
+ )
+
+ return base
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfmodule.py b/repos/system_upgrade/common/libraries/dnflibs/dnfmodule.py
index d85e4659..b274d917 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfmodule.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/dnfmodule.py
@@ -1,7 +1,6 @@
import warnings
-from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common.config.version import get_source_major_version
+from leapp.libraries.common.dnflibs import create_dnf_base
try:
import dnf
@@ -16,55 +15,26 @@ except ImportError:
warnings.warn('Could not import the `hawkey` python module.', ImportWarning)
-def _create_or_get_dnf_base(base=None):
- if not base:
- # The DNF command reads /etc/yum/vars/releasever, but the DNF library does not. It parses redhat-release
- # package to retrieve system's major version which it then uses as $releasever. However, some systems might
- # have repositories only for the exact system version (including the minor number). In a case when
- # /etc/yum/vars/releasever is present, read its contents so that we can access repositores on such systems.
- conf = dnf.conf.Conf()
-
- # preload releasever from what we know, this will be our fallback
- conf.substitutions['releasever'] = get_source_major_version()
-
- # load all substitutions from etc
- conf.substitutions.update_from_etc('/')
-
- base = dnf.Base(conf=conf)
- base.conf.read()
- base.init_plugins()
- base.read_all_repos()
- # configure plugins after the repositories are loaded
- # e.g. the amazon-id plugin requires loaded repositories
- # for the proper configuration.
- base.configure_plugins()
-
- try:
- base.fill_sack()
- except dnf.exceptions.RepoError as e:
- err_msg = str(e)
- repoid = err_msg.split('repo:')[-1].strip() if 'repo:' in err_msg else 'unknown repo'
- repoid = repoid.strip('"').strip("'").replace('\\"', '')
- raise StopActorExecutionError(
- message='DNF failed to load repositories: {}'.format(str(e)),
- details={
- 'hint': 'Ensure the {} repository definition is correct or remove it '
- 'if the repository is not needed anymore.'.format(repoid)
- }
- )
- return base
-
-
def get_modules(base=None):
"""
Return info about all module streams as a list of libdnf.module.ModulePackage objects.
+
+ The function return an empty list if the DNF python module is not present.
+
+ :param base: If it is set, use it instead of creating a new one.
+ :type base: dnf.Base | None
+
+ .. seealso::
+ :func:`create_dnf_base` for exceptions raised when creating dnf.Base
"""
if not dnf:
return []
- base = _create_or_get_dnf_base(base)
+ if not base:
+ base = create_dnf_base()
module_base = dnf.module.module_base.ModuleBase(base)
# this method is absent on RHEL 7, in which case there are no modules anyway
+ # note the method could be removed in future as well - so keep the check
if not hasattr(module_base, 'get_modules'):
return []
return module_base.get_modules('*')[0]
@@ -73,11 +43,20 @@ def get_modules(base=None):
def get_enabled_modules():
"""
Return currently enabled module streams as a list of libdnf.module.ModulePackage objects.
+
+ The function return an empty list if the DNF python module is not present.
+
+ :returns: Currently enabled module streams
+ :rtype: List[libdnf.module.ModulePackage]
+
+ .. seealso::
+ :func:`get_modules` for exceptions raised during module discovery.
+ :func:`create_dnf_base` for exceptions raised when creating dnf.Base
"""
if not dnf:
return []
- base = _create_or_get_dnf_base()
+ base = create_dnf_base()
modules = get_modules(base)
# if modules are not supported (RHEL 7), base.sack._moduleContainer won't exist
@@ -88,6 +67,12 @@ def get_enabled_modules():
def map_installed_rpms_to_modules():
"""
Map installed modular packages to the module streams they come from.
+
+ :returns: Mapping of RPM NVRA tuples to (module_name, stream) tuples
+ :rtype: dict
+
+ .. seealso::
+ :func:`get_modules` for exceptions raised during module discovery.
"""
modules = get_modules()
# empty on RHEL 7 because of no modules
diff --git a/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnflibs.py b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnflibs.py
new file mode 100644
index 00000000..daf9c125
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnflibs.py
@@ -0,0 +1,118 @@
+import pytest
+
+from leapp.libraries.common import dnflibs
+from leapp.libraries.common.testutils import CurrentActorMocked
+from leapp.libraries.stdlib import api
+
+
+class MockSubstitutions(dict):
+ def update_from_etc(self, path):
+ pass
+
+
+class MockDNFConf:
+ def __init__(self):
+ self.substitutions = MockSubstitutions({'releasever': '8'})
+
+ def read(self):
+ pass
+
+
+class MockDNFBase:
+ def __init__(self, conf=None):
+ self.conf = conf or MockDNFConf()
+ self._fill_sack_called = False
+
+ def read(self):
+ pass
+
+ def init_plugins(self):
+ pass
+
+ def read_all_repos(self):
+ pass
+
+ def configure_plugins(self):
+ pass
+
+ def fill_sack(self):
+ self._fill_sack_called = True
+
+
+class MockDNF:
+ """Mock dnf module"""
+ class conf:
+ Conf = MockDNFConf
+
+ Base = MockDNFBase
+
+ class exceptions:
+ RepoError = Exception
+
+
+def test_create_dnf_base_success(monkeypatch):
+ """
+ Test successful creation and initialization of dnf.Base
+ """
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='8.10'))
+ monkeypatch.setattr(dnflibs, 'dnf', MockDNF)
+
+ base = dnflibs.create_dnf_base()
+
+ assert base is not None
+ assert isinstance(base, MockDNFBase)
+ assert base._fill_sack_called
+
+
+@pytest.mark.parametrize('error_message,expected_repoid', [
+ ('Failed to download metadata for repo: test-repo', 'test-repo'),
+ ('Error with repo: "my-repo"', 'my-repo'),
+ ("Failed for repo: 'quoted-repo'", 'quoted-repo'),
+ ('Generic error without repo information', 'unknown repo'),
+])
+def test_create_dnf_base_repo_error(monkeypatch, error_message, expected_repoid):
+ class RepoError(Exception):
+ pass
+
+ class MockExceptions:
+ pass
+
+ MockExceptions.RepoError = RepoError
+
+ class FailingMockDNFBase:
+ _repo_error = RepoError
+ _error_message = error_message
+
+ def __init__(self, conf=None):
+ self.conf = conf or MockDNFConf()
+
+ def read(self):
+ pass
+
+ def init_plugins(self):
+ pass
+
+ def read_all_repos(self):
+ pass
+
+ def configure_plugins(self):
+ pass
+
+ def fill_sack(self):
+ raise self._repo_error(self._error_message)
+
+ class FailingMockDNF:
+ class conf:
+ Conf = MockDNFConf
+
+ Base = FailingMockDNFBase
+ exceptions = MockExceptions
+
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='8.10'))
+ monkeypatch.setattr(dnflibs, 'dnf', FailingMockDNF)
+
+ with pytest.raises(dnflibs.DNFRepoError) as exc_info:
+ dnflibs.create_dnf_base()
+
+ assert 'DNF failed to load repositories' in str(exc_info.value)
+ assert expected_repoid in str(exc_info.value.details['hint'])
diff --git a/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfmodule.py b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfmodule.py
new file mode 100644
index 00000000..82ce62c7
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfmodule.py
@@ -0,0 +1,270 @@
+"""
+Trivial unit testing of dnfmodule library => rely on integration tests.
+
+There is actually not a good coverage we could do for this library via
+unit-tests. These tests seem to have a low value as basically DNF & hawkey
+libraries are completely mocked - and there is not a good way to write unit-tests
+without this full mocking. The library is covered by CI tests.
+"""
+
+from leapp.libraries.common.dnflibs import dnfmodule
+
+
+class MockModulePackage:
+ def __init__(self, name, stream, artifacts=None):
+ self._name = name
+ self._stream = stream
+ self._artifacts = artifacts or []
+
+ def getName(self):
+ return self._name
+
+ def getStream(self):
+ return self._stream
+
+ def getArtifacts(self):
+ return self._artifacts
+
+
+class MockNEVRA:
+ def __init__(self, name, version, release, arch):
+ self.name = name
+ self.version = version
+ self.release = release
+ self.arch = arch
+
+
+class MockModuleBase:
+ def __init__(self, base):
+ self.base = base
+ self._modules = []
+
+ def get_modules(self, pattern):
+ return [self._modules, None]
+
+
+class MockModuleContainer:
+ def __init__(self):
+ self._enabled = set()
+
+ def isEnabled(self, module):
+ return module.getName() in self._enabled
+
+ def enable(self, module_name):
+ self._enabled.add(module_name)
+
+
+class MockSack:
+ def __init__(self):
+ self._moduleContainer = MockModuleContainer()
+
+
+class MockDNFBase:
+ def __init__(self):
+ self.sack = MockSack()
+
+
+def _split_nevra(rpm_str):
+ """Simple NEVRA parser for testing"""
+ parts = rpm_str.rsplit('.', 1)
+ arch = parts[-1] if len(parts) > 1 else 'noarch'
+ rest = parts[0] if len(parts) > 1 else rpm_str
+
+ parts = rest.rsplit('-', 2)
+ if len(parts) >= 3:
+ name, version, release = parts[0], parts[1], parts[2]
+ elif len(parts) == 2:
+ name, version, release = parts[0], parts[1], '0'
+ else:
+ name, version, release = parts[0], '0', '0'
+
+ return MockNEVRA(name, version, release, arch)
+
+
+def test_get_modules_with_base(monkeypatch):
+ module1 = MockModulePackage('nodejs', '18')
+ module2 = MockModulePackage('postgresql', '15')
+
+ base = MockDNFBase()
+
+ class ModuleBaseWithModules(MockModuleBase):
+ def __init__(self, base):
+ super().__init__(base)
+ self._modules = [module1, module2]
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = ModuleBaseWithModules
+
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+
+ modules = dnfmodule.get_modules(base)
+
+ assert len(modules) == 2
+ assert modules[0].getName() == 'nodejs'
+ assert modules[1].getName() == 'postgresql'
+
+
+def test_get_modules_without_base(monkeypatch):
+ module1 = MockModulePackage('nodejs', '18')
+
+ def mock_create_dnf_base():
+ return MockDNFBase()
+
+ class ModuleBaseWithModules(MockModuleBase):
+ def __init__(self, base):
+ super().__init__(base)
+ self._modules = [module1]
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = ModuleBaseWithModules
+
+ monkeypatch.setattr(dnfmodule, 'create_dnf_base', mock_create_dnf_base)
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+
+ modules = dnfmodule.get_modules()
+
+ assert len(modules) == 1
+ assert modules[0].getName() == 'nodejs'
+
+
+def test_get_modules_no_get_modules_method(monkeypatch):
+ """
+ Test the compatibility for DNF versions without module stream functionality.
+
+ This is a known case for RHEL 7, which we do not support in upstream anymore.
+ However, it's possible that future versions of DNF will drop module stream
+ functionality as well. So keeping this test for now, but the functionality
+ can be "broken" in different ways from the one we test at this moment.
+ """
+ base = MockDNFBase()
+
+ class LimitedModuleBase:
+ def __init__(self, base):
+ self.base = base
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = LimitedModuleBase
+
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+
+ modules = dnfmodule.get_modules(base)
+
+ assert modules == []
+
+
+def test_get_enabled_modules(monkeypatch):
+ module1 = MockModulePackage('nodejs', '18')
+ module2 = MockModulePackage('postgresql', '15')
+ module3 = MockModulePackage('ruby', '3.0')
+
+ def mock_create_dnf_base():
+ base = MockDNFBase()
+ base.sack._moduleContainer.enable('nodejs')
+ base.sack._moduleContainer.enable('ruby')
+ return base
+
+ class ModuleBaseWithModules(MockModuleBase):
+ def __init__(self, base):
+ super().__init__(base)
+ self._modules = [module1, module2, module3]
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = ModuleBaseWithModules
+
+ monkeypatch.setattr(dnfmodule, 'create_dnf_base', mock_create_dnf_base)
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+
+ enabled = dnfmodule.get_enabled_modules()
+
+ assert len(enabled) == 2
+ enabled_names = [m.getName() for m in enabled]
+ assert 'nodejs' in enabled_names
+ assert 'ruby' in enabled_names
+ assert 'postgresql' not in enabled_names
+
+
+def test_get_enabled_modules_no_dnf(monkeypatch):
+ monkeypatch.setattr(dnfmodule, 'dnf', None)
+
+ enabled = dnfmodule.get_enabled_modules()
+
+ assert enabled == []
+
+
+def test_map_installed_rpms_to_modules(monkeypatch):
+ module1 = MockModulePackage('nodejs', '18', artifacts=[
+ 'nodejs-18.0.0-1.x86_64',
+ 'npm-8.0.0-1.x86_64'
+ ])
+ module2 = MockModulePackage('postgresql', '15', artifacts=[
+ 'postgresql-15.0-1.x86_64',
+ 'postgresql-server-15.0-1.x86_64'
+ ])
+
+ class ModuleBaseWithModules(MockModuleBase):
+ def __init__(self, base):
+ super().__init__(base)
+ self._modules = [module1, module2]
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = ModuleBaseWithModules
+
+ class MockHawkey:
+ split_nevra = staticmethod(_split_nevra)
+
+ def mock_create_dnf_base():
+ return MockDNFBase()
+
+ monkeypatch.setattr(dnfmodule, 'create_dnf_base', mock_create_dnf_base)
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+ monkeypatch.setattr(dnfmodule, 'hawkey', MockHawkey)
+
+ rpm_map = dnfmodule.map_installed_rpms_to_modules()
+
+ assert ('nodejs', '18.0.0', '1', 'x86_64') in rpm_map
+ assert rpm_map[('nodejs', '18.0.0', '1', 'x86_64')] == ('nodejs', '18')
+
+ assert ('npm', '8.0.0', '1', 'x86_64') in rpm_map
+ assert rpm_map[('npm', '8.0.0', '1', 'x86_64')] == ('nodejs', '18')
+
+ assert ('postgresql', '15.0', '1', 'x86_64') in rpm_map
+ assert rpm_map[('postgresql', '15.0', '1', 'x86_64')] == ('postgresql', '15')
+
+ assert ('postgresql-server', '15.0', '1', 'x86_64') in rpm_map
+ assert rpm_map[('postgresql-server', '15.0', '1', 'x86_64')] == ('postgresql', '15')
+
+
+def test_map_installed_rpms_to_modules_empty(monkeypatch):
+ class EmptyModuleBase(MockModuleBase):
+ def __init__(self, base):
+ super().__init__(base)
+ self._modules = []
+
+ class MockDNF:
+ class module:
+ class module_base:
+ ModuleBase = EmptyModuleBase
+
+ class MockHawkey:
+ split_nevra = staticmethod(_split_nevra)
+
+ def mock_create_dnf_base():
+ return MockDNFBase()
+
+ monkeypatch.setattr(dnfmodule, 'create_dnf_base', mock_create_dnf_base)
+ monkeypatch.setattr(dnfmodule, 'dnf', MockDNF)
+ monkeypatch.setattr(dnfmodule, 'hawkey', MockHawkey)
+
+ rpm_map = dnfmodule.map_installed_rpms_to_modules()
+
+ assert rpm_map == {}
--
2.54.0

View File

@ -0,0 +1,363 @@
From bcc445b165cfd63ae659f699209fff10e6aa8c42 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Fri, 24 Apr 2026 15:52:07 +0200
Subject: [PATCH 073/105] dnflibs.dnfconfig: Raise proper exceptions and add
tests
Originally the library raised StopActorExecutionError or
CalledProcessError exceptions in case of problems. This change
introduces new exceptions to make clear what problem actually happens.
New exceptions are based on DNFError (which is based on
StopActorExecutionError, to ensure that errors are caught and handled
properly).
List of new exceptions:
* InvalidDNFConfig
* CannotObtainDNFConfig
* CannotUpdateDNFConfig (replacing CalledProcessError)
Assisted-by: Claude Sonnet 4.5
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/libraries/dnflibs/dnfconfig.py | 46 +++-
.../libraries/dnflibs/tests/test_dnfconfig.py | 228 ++++++++++++++++++
2 files changed, 266 insertions(+), 8 deletions(-)
create mode 100644 repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfconfig.py
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfconfig.py b/repos/system_upgrade/common/libraries/dnflibs/dnfconfig.py
index 9f1902b6..8872e592 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfconfig.py
+++ b/repos/system_upgrade/common/libraries/dnflibs/dnfconfig.py
@@ -1,8 +1,26 @@
-from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.common.dnflibs import DNFError
from leapp.libraries.common.rpms import get_leapp_packages
from leapp.libraries.stdlib import api, CalledProcessError
+class InvalidDNFConfig(DNFError):
+ """
+ Raised when DNF configuration is invalid.
+ """
+
+
+class CannotObtainDNFConfig(DNFError):
+ """
+ Raised when cannot obtain DNF configuration.
+ """
+
+
+class CannotUpdateDNFConfig(DNFError):
+ """
+ Raised when the DNF configuration could not be updated.
+ """
+
+
def _strip_split(data, sep, maxsplit=-1):
"""
Just like str.split(), but remove ambient whitespaces from all items
@@ -14,7 +32,7 @@ def _get_main_dump(context, disable_plugins):
"""
Return the dnf configuration dump of main options for the given context.
- Returns the list of lines after the line with "[main]" section
+ :rtype: dict
"""
cmd = ['dnf', 'config-manager', '--dump']
@@ -27,7 +45,7 @@ def _get_main_dump(context, disable_plugins):
data = context.call(cmd, split=True)['stdout']
except CalledProcessError as e:
api.current_logger().error('Cannot obtain the dnf configuration')
- raise StopActorExecutionError(
+ raise CannotObtainDNFConfig(
message='Cannot obtain data about the DNF configuration',
details={'stdout': e.stdout, 'stderr': e.stderr}
)
@@ -36,7 +54,7 @@ def _get_main_dump(context, disable_plugins):
# return index of the first item in the main section
main_start = data.index('[main]') + 1
except ValueError:
- raise StopActorExecutionError(
+ raise InvalidDNFConfig(
message='Invalid DNF configuration data (missing [main])',
details=data,
)
@@ -76,7 +94,7 @@ def _set_excluded_pkgs(context, pkglist, disable_plugins):
"""
Configure DNF to exclude packages in the given list
- Raise the CalledProcessError on error.
+ :raises CannotUpdateDNFConfig: When an attempt to update DNF configuration fails
"""
exclude = 'exclude={}'.format(','.join(pkglist))
cmd = ['dnf', 'config-manager', '--save', '--setopt', exclude]
@@ -87,9 +105,12 @@ def _set_excluded_pkgs(context, pkglist, disable_plugins):
try:
context.call(cmd)
- except CalledProcessError:
- api.current_logger().error('Cannot set the dnf configuration')
- raise
+ except CalledProcessError as e:
+ api.current_logger().error('Cannot update the dnf configuration')
+ raise CannotUpdateDNFConfig(
+ message='Cannot update the DNF configuration to exclude RPMs of the upgrade tooling.',
+ details={'stdout': e.stdout, 'stderr': e.stderr}
+ )
api.current_logger().debug('The DNF configuration has been updated to exclude leapp packages.')
@@ -103,6 +124,15 @@ def exclude_leapp_rpms(context, disable_plugins):
- on the host system
So user will have to drop these packages from the exclude after the
upgrade.
+
+ :param context: The execution context
+ :type context: mounting.IsolatedActions
+ :param disable_plugins: List of plugins supposed to be disabled during DNF execution
+ :type disable_plugins: list
+ :raises InvalidDNFConfig: When the discovered DNF configuratino is invalid.
+ E.g. the [main] section is missing.
+ :raises CannotObtainDNFConfig: The DNF call `dnf config-manager --dump` failed.
+ :raises CannotUpdateDNFConfig: The DNF call to update its configuration failed.
"""
to_exclude = list(set(_get_excluded_pkgs(context, disable_plugins) + get_leapp_packages()))
_set_excluded_pkgs(context, to_exclude, disable_plugins)
diff --git a/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfconfig.py b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfconfig.py
new file mode 100644
index 00000000..8cfa75ba
--- /dev/null
+++ b/repos/system_upgrade/common/libraries/dnflibs/tests/test_dnfconfig.py
@@ -0,0 +1,228 @@
+import pytest
+
+from leapp.libraries.common.dnflibs import dnfconfig
+from leapp.libraries.common.testutils import logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+
+
+class MockContext:
+ def __init__(self, stdout=None, should_raise=None):
+ self.stdout = stdout or []
+ self.should_raise = should_raise
+ self.commands = []
+
+ @property
+ def last_cmd(self):
+ return self.commands[-1] if self.commands else None
+
+ def call(self, cmd, split=False):
+ self.commands.append(cmd)
+ if self.should_raise:
+ raise self.should_raise
+ if split:
+ return {'stdout': self.stdout}
+ # TODO(pstodulk): this part is not used now, but if we want to make
+ # this in future more generic, we should update it properly
+ return None
+
+
+_SAMPLE_DNF_DUMP = [
+ '[main]',
+ 'gpgcheck = 1',
+ 'installonly_limit = 3',
+ 'clean_requirements_on_remove = True',
+ 'exclude = pkg1,pkg2',
+]
+
+
+@pytest.mark.parametrize('data,sep,maxsplit,expected', [
+ ('key = value', '=', -1, ['key', 'value']),
+ (' key = value ', '=', -1, ['key', 'value']),
+ ('a,b,c,d', ',', 2, ['a', 'b', 'c,d']),
+ ('pkg1, pkg2, pkg3', ',', -1, ['pkg1', 'pkg2', 'pkg3']),
+])
+def test_strip_split(data, sep, maxsplit, expected):
+ result = dnfconfig._strip_split(data, sep, maxsplit)
+ assert result == expected
+
+
+def test_get_main_dump_success(monkeypatch):
+ context = MockContext(stdout=_SAMPLE_DNF_DUMP)
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ result = dnfconfig._get_main_dump(context, disable_plugins=None)
+
+ assert 'gpgcheck' in result
+ assert result['gpgcheck'] == '1'
+ assert result['installonly_limit'] == '3'
+ assert result['exclude'] == 'pkg1,pkg2'
+
+
+def test_get_main_dump_with_disabled_plugins():
+ context = MockContext(stdout=_SAMPLE_DNF_DUMP)
+
+ dnfconfig._get_main_dump(context, disable_plugins=['plugin1', 'plugin2'])
+
+ expected_cmd = [
+ 'dnf', 'config-manager', '--dump',
+ '--disableplugin', 'plugin1',
+ '--disableplugin', 'plugin2'
+ ]
+ assert context.last_cmd == expected_cmd
+
+
+def test_get_main_dump_command_fails(monkeypatch):
+ err = CalledProcessError(
+ 'Command failed',
+ ['dnf', 'config-manager', '--dump'],
+ {'stdout': 'stdout output', 'stderr': 'stderr output', 'exit_code': 1}
+ )
+ context = MockContext(should_raise=err)
+
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ with pytest.raises(dnfconfig.CannotObtainDNFConfig) as exc_info:
+ dnfconfig._get_main_dump(context, disable_plugins=None)
+
+ assert 'Cannot obtain data about the DNF configuration' in str(exc_info.value)
+ assert exc_info.value.details['stdout'] == 'stdout output'
+ assert exc_info.value.details['stderr'] == 'stderr output'
+
+
+def test_get_main_dump_missing_main_section():
+ stdout = [
+ '[repo1]',
+ 'name = Repository 1',
+ 'enabled = 1'
+ ]
+ context = MockContext(stdout=stdout)
+
+ with pytest.raises(dnfconfig.InvalidDNFConfig) as exc_info:
+ dnfconfig._get_main_dump(context, disable_plugins=None)
+
+ assert 'Invalid DNF configuration data (missing [main])' in str(exc_info.value)
+
+
+def test_get_main_dump_malformed_line(monkeypatch):
+ stdout = [
+ '[main]',
+ 'gpgcheck = 1',
+ 'malformed line without equals',
+ 'exclude = pkg1'
+ ]
+ context = MockContext(stdout=stdout)
+
+ mocked_logger = logger_mocked()
+ monkeypatch.setattr(api, 'current_logger', mocked_logger)
+
+ result = dnfconfig._get_main_dump(context, disable_plugins=None)
+
+ assert result['gpgcheck'] == '1'
+ assert result['exclude'] == 'pkg1'
+ assert len(mocked_logger.warnmsg) > 0
+ assert 'malformed line without equals' in mocked_logger.warnmsg[0]
+
+
+@pytest.mark.parametrize('exclude_value,expected', [
+ ('pkg1, pkg2, pkg3', ['pkg1', 'pkg2', 'pkg3']),
+ ('pkg1,pkg2, pkg3', ['pkg1', 'pkg2', 'pkg3']),
+ ('', []),
+ ('pkg1, , pkg2, ', ['pkg1', 'pkg2']),
+])
+def test_get_excluded_pkgs(exclude_value, expected):
+ stdout = ['[main]', 'exclude = {}'.format(exclude_value)] if exclude_value else ['[main]', 'gpgcheck = 1']
+ context = MockContext(stdout=stdout)
+
+ result = dnfconfig._get_excluded_pkgs(context, disable_plugins=None)
+
+ assert result == expected
+
+
+def test_set_excluded_pkgs_success():
+ context = MockContext()
+ pkglist = ['pkg1', 'pkg2', 'pkg3']
+
+ dnfconfig._set_excluded_pkgs(context, pkglist, disable_plugins=None)
+
+ expected_cmd = [
+ 'dnf', 'config-manager', '--save',
+ '--setopt', 'exclude=pkg1,pkg2,pkg3'
+ ]
+ assert context.last_cmd == expected_cmd
+
+
+def test_set_excluded_pkgs_with_disabled_plugins():
+ context = MockContext()
+
+ dnfconfig._set_excluded_pkgs(context, ['pkg1'], disable_plugins=['plugin1', 'plugin2'])
+
+ expected_cmd = [
+ 'dnf', 'config-manager', '--save',
+ '--setopt', 'exclude=pkg1',
+ '--disableplugin', 'plugin1',
+ '--disableplugin', 'plugin2'
+ ]
+ assert context.last_cmd == expected_cmd
+
+
+def test_set_excluded_pkgs_fails(monkeypatch):
+ err = CalledProcessError(
+ 'Command failed',
+ ['dnf', 'config-manager', '--save'],
+ {'stdout': 'stdout output', 'stderr': 'stderr output', 'exit_code': 1}
+ )
+ context = MockContext(should_raise=err)
+
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ with pytest.raises(dnfconfig.CannotUpdateDNFConfig) as exc_info:
+ dnfconfig._set_excluded_pkgs(context, ['pkg1'], disable_plugins=None)
+
+ assert 'Cannot update the DNF configuration' in str(exc_info.value)
+ assert exc_info.value.details['stdout'] == 'stdout output'
+ assert exc_info.value.details['stderr'] == 'stderr output'
+
+
+def test_exclude_leapp_rpms(monkeypatch):
+ """
+ Test that exclude_leapp_rpms merges existing exclusions with leapp packages
+ """
+ context = MockContext(stdout=[
+ '[main]',
+ 'exclude = existing-pkg1, existing-pkg2'
+ ])
+ monkeypatch.setattr(dnfconfig, 'get_leapp_packages', lambda: ['leapp', 'leapp-upgrade-el8toel9'])
+
+ dnfconfig.exclude_leapp_rpms(context, disable_plugins=None)
+
+ setopt_cmd = None
+ for cmd in context.commands:
+ if '--setopt' in cmd:
+ setopt_cmd = cmd
+ break
+
+ assert setopt_cmd is not None
+ exclude_arg = set([arg for arg in setopt_cmd if arg.startswith('exclude=')][0].split('=')[1].split(','))
+ expected_arg = set(['existing-pkg1', 'existing-pkg2', 'leapp', 'leapp-upgrade-el8toel9'])
+ assert expected_arg == exclude_arg
+
+
+def test_exclude_leapp_rpms_no_duplicates(monkeypatch):
+ context = MockContext(stdout=[
+ '[main]',
+ 'exclude = leapp'
+ ])
+ monkeypatch.setattr(dnfconfig, 'get_leapp_packages', lambda: ['leapp', 'leapp-upgrade-el8toel9'])
+
+ dnfconfig.exclude_leapp_rpms(context, disable_plugins=None)
+
+ setopt_cmd = None
+ for cmd in context.commands:
+ if '--setopt' in cmd:
+ setopt_cmd = cmd
+ break
+
+ assert setopt_cmd is not None
+ exclude_arg = [arg for arg in setopt_cmd if arg.startswith('exclude=')][0]
+ packages = exclude_arg.replace('exclude=', '').split(',')
+ assert packages.count('leapp') == 1
--
2.54.0

View File

@ -0,0 +1,57 @@
From cf1a6cf5d890573a888a067196bc69fec3b23d4a Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 27 Apr 2026 20:45:31 +0000
Subject: [PATCH 074/105] DOCSTRINGS: Document DNF related exception
propagation
Update docstrings in actors that use dnflibs libraries to make
make clear what exceptions can be potentially raised:
* rpmscanner
* upgradeinitramfsgenerator
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/upgradeinitramfsgenerator.py | 8 ++++++++
.../common/actors/rpmscanner/libraries/rpmscanner.py | 6 ++++++
2 files changed, 14 insertions(+)
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 aec1debf..24f35847 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
@@ -169,6 +169,14 @@ def _get_dracut_modules():
def _install_initram_deps(packages):
+ """
+ Install initramfs dependencies into the target userspace.
+
+ :param packages: List of package names to install
+
+ .. seealso::
+ :func:`leapp.libraries.common.dnflibs.dnfplugin.install_initramdisk_requirements`
+ """
used_repos = api.consume(UsedTargetRepositories)
target_userspace_info = next(api.consume(TargetUserSpaceInfo), None)
diff --git a/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py b/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
index ebf17e19..100fb32c 100644
--- a/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
+++ b/repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py
@@ -99,6 +99,12 @@ def get_package_repository_data():
def map_modular_rpms_to_modules():
"""
Map modular packages to the module streams they come from.
+
+ :returns: Mapping of RPM NEVRA tuples to (module_name, stream) tuples
+ :rtype: dict
+
+ .. seealso::
+ :func:`leapp.libraries.common.dnflibs.dnfmodule.get_modules`
"""
modules = dnfmodule.get_modules()
# empty on RHEL 7 because of no modules
--
2.54.0

View File

@ -0,0 +1,87 @@
From 545f15a053b6dc588f41a5326964a9423d4a7857 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jozef=20Ba=C4=8D=C3=ADk?= <jobacik@redhat.com>
Date: Mon, 11 May 2026 12:21:31 +0200
Subject: [PATCH 075/105] pulseaudiocheck: fix for users with "None" home
directory
If the user has single entry in /etc/passwd without any colons, the reported
pw_dir is NoneType. This makes the os.path.join() crash as NoneType is
unexpected.
While technically, entry without the colons is considered invalid and not POSIX
compliant, it has been seen that some customers are using such entries and leapp
is already taking this possibility into account.
This commit modifies the behaviour of the scanpulseaudio actor, so such entries
are logged and skipped.
Co-authored-by: Matej Matuska <mmatuska@redhat.com>
---
.../scanpulseaudio/libraries/scanpulseaudio.py | 4 ++++
.../tests/test_scanpulseaudio.py | 18 ++++++++++++++----
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
index e5c2be53..28cde062 100644
--- a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
@@ -2,6 +2,7 @@ import os
import pwd
from leapp.libraries.common.rpms import check_file_modification
+from leapp.libraries.stdlib import api
from leapp.models import PulseAudioConfiguration
# System-wide PulseAudio configuration directory
@@ -54,6 +55,9 @@ def _get_user_config_dirs():
"""
found = []
for user in pwd.getpwall():
+ if not user.pw_dir:
+ api.current_logger().debug('User "{}" has no valid home entry, skipping.'.format(user.pw_name))
+ continue
pulse_dir = os.path.join(user.pw_dir, _USER_CONFIG_SUBDIR)
if os.path.isdir(pulse_dir) and os.listdir(pulse_dir):
found.append(pulse_dir)
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py
index f499aafe..e6b63a4c 100644
--- a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py
@@ -1,5 +1,7 @@
import os
+import pytest
+
from leapp.libraries.actor import scanpulseaudio
from leapp.libraries.actor.scanpulseaudio import _get_dropin_dirs_with_content, _get_user_config_dirs, scan_pulseaudio
@@ -36,16 +38,24 @@ class TestGetUserConfigDirs:
monkeypatch.setattr(os.path, 'isdir', lambda _: False)
assert _get_user_config_dirs() == []
- def test_user_config_found(self, monkeypatch):
+ @pytest.mark.parametrize(
+ 'home_dir, expect',
+ [
+ ('/home/testuser', ['/home/testuser/.config/pulse']),
+ ('', []),
+ ],
+ )
+ def test_user_config_found(self, monkeypatch, home_dir, expect):
monkeypatch.setattr(os.path, 'isdir', lambda path: path == '/home/testuser/.config/pulse')
monkeypatch.setattr(os, 'listdir', lambda _: ['default.pa'])
class FakeUser:
- pw_dir = '/home/testuser'
+ pw_name = 'testuser'
+ pw_dir = home_dir
- monkeypatch.setattr(scanpulseaudio.pwd, 'getpwall', lambda: [FakeUser()])
+ monkeypatch.setattr(scanpulseaudio.pwd, 'getpwall', lambda: [FakeUser])
result = _get_user_config_dirs()
- assert result == ['/home/testuser/.config/pulse']
+ assert result == expect
class TestScanPulseaudio:
--
2.54.0

View File

@ -0,0 +1,68 @@
From 03e67c0d42914b40cac1bff390bd27b9c7b91d04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20He=C4=8Dko?= <michal.sk.com@gmail.com>
Date: Wed, 13 May 2026 11:51:48 +0200
Subject: [PATCH 076/105] fix(target_initramfs): always regenerate initramfs
(#1536)
Remove conditions that would skip target initramfs regeneration if there
are no dracut or kernel modules to be included. Instead, regenerate the
target initramfs always, as there might be new configs written by us
after the upgrade RPM transaction installs the target kernel and
generates a corresponding initramfs. The initramfs generated during RPM
transaction, therefore lacks or contains invalid config files.
Jira: RHEL-151509
---
.../libraries/targetinitramfsgenerator.py | 12 +++++++-----
.../tests/test_targetinitramfsgenerator.py | 5 ++---
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/libraries/targetinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/libraries/targetinitramfsgenerator.py
index edfb42ce..e3b9352e 100644
--- a/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/libraries/targetinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/libraries/targetinitramfsgenerator.py
@@ -97,14 +97,16 @@ def _get_modules():
def process():
+ """
+ Regenerate target system initramfs.
+
+ The initramfs is regenerated unconditionally always as there might be new configs
+ necessary for boot which are written by us after the RPM transaction installs
+ the target kernel (and generates the target initramfs for the first time).
+ """
files = _get_files()
modules = _get_modules()
- if not files and not modules['kernel'] and not modules['dracut']:
- api.current_logger().debug(
- 'No additional files or modules required to add into the target initramfs.')
- return
-
target_kernel_info = next(api.consume(InstalledTargetKernelInfo), None)
if not target_kernel_info:
raise StopActorExecutionError(
diff --git a/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/tests/test_targetinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/tests/test_targetinitramfsgenerator.py
index 4df9a485..d21cdce8 100644
--- a/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/tests/test_targetinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/tests/test_targetinitramfsgenerator.py
@@ -71,13 +71,12 @@ def gen_InitrdIncludes(files):
def test_no_includes(monkeypatch):
run_mocked = RunMocked()
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[mk_kernel_info(KERNEL_VERSION)]))
monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(targetinitramfsgenerator, 'run', run_mocked)
targetinitramfsgenerator.process()
- assert NO_INCLUDE_MSG in api.current_logger.dbgmsg
- assert not run_mocked.called
+ assert run_mocked.called
TEST_CASES = [
--
2.54.0

View File

@ -0,0 +1,393 @@
From af876b2deb797606089ed309c551b674e2ba0345 Mon Sep 17 00:00:00 2001
From: karolinku <kkula@redhat.com>
Date: Mon, 11 May 2026 09:56:27 +0200
Subject: [PATCH 077/105] Add AI assistant harness with AGENTS.md and task
skills
Introduce an AI assistant harness to guide agents and human
developers when working with coding tools with this repo.
AGENTS.md serves as a compact always-loaded entrypoint with project
boundaries, architecture, phases, general rules and a definition of
done. Detailed procedure guidance is split into skill files to
minimize token consumption.
Skills added:
- leapp-test-runner: container-based testing, linting, failure triage
- leapp-unit-tests: test patterns, testutils API, mocking conventions
- leapp-actor-dev: actor structure, phase selection, placement rules
---
AGENTS.md | 81 ++++++++++++++++++++++++++
skills/leapp-actor-dev/SKILL.md | 95 +++++++++++++++++++++++++++++++
skills/leapp-test-runner/SKILL.md | 68 ++++++++++++++++++++++
skills/leapp-unit-tests/SKILL.md | 94 ++++++++++++++++++++++++++++++
4 files changed, 338 insertions(+)
create mode 100644 AGENTS.md
create mode 100644 skills/leapp-actor-dev/SKILL.md
create mode 100644 skills/leapp-test-runner/SKILL.md
create mode 100644 skills/leapp-unit-tests/SKILL.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..5de224bc
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,81 @@
+# AGENTS.md
+
+## Project objective
+
+[Leapp-repository](https://github.com/oamg/leapp-repository) contains the actors, models, and libraries for RHEL in-place upgrades (IPU). It runs on top of the [Leapp framework](https://github.com/oamg/leapp), which provides the actor runtime, workflow execution, messaging, and core CLI behavior. It's important to notice, that framework-level mechanics belong to leapp, while upgrade-path logic belong to leapp-repository.
+
+## Architecture
+
+Leapp IPU content is actor-based and message-driven:
+- Actors exchange typed messages (`consumes` / `produces`) through workflow phases.
+- Messages only flow forward: a producer must run before its consumer. Within the same phase, the framework resolves ordering automatically.
+- Prefer scanner/checker design: collect facts in one actor, evaluate in another.
+- Keep actor `process()` thin; place implementation logic in actor libraries for testability.
+
+### Phase workflow (always apply)
+- `FactsCollectionPhase`: collect system facts and produce messages
+- `ChecksPhase`: consume previously collected facts, report/inhibit compatibility issues
+- If modification is required, do it in later phases (`ApplicationsPhase` or `ThirdPartyApplicationsPhase`).
+- `FinalPhase`: to perform actions after the upgraded system is booted.
+
+For full phase details if ambiguous :
+- [Phases of the Upgrade Workflow](https://leapp-repository.readthedocs.io/latest/upgrade-architecture-and-workflow/phases-overview.html#phases-overview)
+
+### Repository Layout
+
+```
+├── system_upgrade/
+ ├── common/ # common repository/content used and accessible for all system upgrades
+ │ ├── actors/<name>/
+ │ │ ├── actor.py # Actor class definition
+ │ │ ├── libraries/ # Actor-private logic
+ │ │ └── tests/ # Actor tests (unit_test_*.py, test_*.py)
+ │ ├── models/ # Data models (consumed/produced by actors)
+ │ ├── topics/ # Message topics
+ │ └── libraries/ # Shared libraries (importable via leapp.libraries.common)
+ ├── el8toel9/ # content specific only for RHEL 8 -> 9 upgrades
+ └── el9toel10/ # content specific only for RHEL 9 -> 10 upgrades
+```
+
+
+## General rules (always apply)
+
+- Follow leapp specific guidelines: docs/source/contributing/coding-guidelines.md and project conventions first.
+- Follow Python coding guidelines: https://leapp.readthedocs.io/en/stable/contributing.html
+- Analyze and plan before introducing code.
+- Ask, don't assume. Be verbose when user input is unclear or can be interpreted in many ways.
+- Explain your decisions briefly; expand detail only when requested.
+- If multiple approaches or solution exists, pick simpler one.
+- Be ready to undo your changes.
+- Before introducing new code, check if it's not already solved in codebase. Avoid code duplication.
+- Try to write as simple code as possible. New code should easy to read and understand even for non-experienced developers.
+- Don't change code, which is not directly involved in given task. Don't try to improve unrelated code.
+- Don't perform git commit, git push, or create PRs without explicit user request.
+
+
+
+## Common Commands
+
+Quick reference (details in skills below):
+
+```bash
+make lint # pylint + flake8 + isort
+TEST_CONTAINER=el9 make test_container # full CI-equivalent test
+ACTOR=<name> make dev_test_no_lint # single-actor fast test
+```
+
+## Skills
+
+Load the relevant skill for the task at hand:
+
+- **Run tests / lint** → `skills/leapp-test-runner/`
+- **Write or fix unit tests** → `skills/leapp-unit-tests/`
+- **Develop or modify actors** → `skills/leapp-actor-dev/`
+
+
+## Definition of Done
+
+- Scope is clear and limited to the requested change.
+- New or changed code is covered by unit tests.
+- Relevant checks were run (`make lint` and/or targeted tests) and results are reported.
+- Risks, assumptions, and follow-up work are explicitly called out
diff --git a/skills/leapp-actor-dev/SKILL.md b/skills/leapp-actor-dev/SKILL.md
new file mode 100644
index 00000000..124f3e99
--- /dev/null
+++ b/skills/leapp-actor-dev/SKILL.md
@@ -0,0 +1,95 @@
+# Developing Leapp Actors
+
+## Actor structure
+
+Every actor follows layout:
+
+```
+repos/system_upgrade/<leapp_repo_dir>/actors/<actor_name>/
+├── actor.py # Actor class (thin wrapper)
+├── libraries/
+│ └── <actor_name>.py # Implementation logic
+└── tests/
+ └── test_<actor_name>.py
+```
+
+Keep `actor.py` minimal — delegate the real implementation to the library for testability:
+
+```python
+from leapp.actors import Actor
+from leapp.libraries.actor import <actor_name>
+from leapp.models import <ConsumedModel>, <ProducedModel>
+from leapp.tags import <PhaseTag>, IPUWorkflowTag
+
+
+class MyActor(Actor):
+ """Brief description of what this actor does."""
+
+ name = '<actor_name>'
+ consumes = (<ConsumedModel>,)
+ produces = (<ProducedModel>,)
+ tags = (IPUWorkflowTag, <PhaseTag>)
+
+ def process(self):
+ <actor_name>.process()
+```
+
+## Phase selection
+
+| Intent | Phase tag | Rules |
+|--------|-----------|-------|
+| Collect system facts | `FactsPhaseTag` | Produce messages only, no decisions |
+| Check compatibility / inhibit | `ChecksPhaseTag` | Consume facts, no direct system interaction |
+| Modify application config | `ApplicationsPhaseTag` | Only after RPM upgrade is complete |
+| Modify third-party config | `ThirdPartyApplicationsPhaseTag` | Same as above, for non-Red Hat apps |
+
+### Common multi-actor patterns
+
+**Inhibiting an incompatible system** (2 actors):
+1. Scanner in `FactsCollectionPhase` — produces a facts message.
+2. Checker in `ChecksPhase` — consumes facts, creates report / inhibits.
+
+**Modifying incompatible config** (3 actors):
+1. Scanner in `FactsCollectionPhase` — collects info.
+2. Checker in `ChecksPhase` — decides and reports planned change.
+3. Modifier in `ApplicationsPhase` — performs the change.
+
+## Placement rules
+
+- Reusable across upgrade paths → `repos/system_upgrade/common/`
+- Specific to one path → `repos/system_upgrade/el8toel9/` or `el9toel10/`
+- Same rule applies to models and shared libraries.
+
+
+## Before writing new code
+
+- Search existing actors/models/libraries — reuse before creating.
+- Check if a model already provides the message you need (search model class names in `repos/`).
+- Discover `repos/system_upgrade/common/libraries/` for shared utilities.
+- Prefer using stdlib functions over shell commands.
+- If introduce envars, use LEAPP and LEAPP_DEVEL.
+- Write unit-testable code (see `skills/leapp-unit-tests/`).
+
+
+## Key constraints
+
+- Avoid running code on a module level to avoid slow downs during this load phase
+- Never use `subprocess` — use `leapp.libraries.stdlib.run()` instead.
+- Never interact with the system during `ChecksPhase`.
+- Do not introduce new dependencies, unless it's strongly justified.
+
+
+## Python compatibility
+
+| Repository | Required Python versions |
+|------------|--------------------------|
+| `common` | 3.6, 3.9, 3.12 |
+| `el8toel9` | 3.6, 3.9 |
+| `el9toel10`| 3.9, 3.12 |
+
+## Reference
+
+- [How to write an Actor for Leapp Upgrade](https://leapp-repository.readthedocs.io/latest/tutorials/howto-first-actor-upgrade.html)
+- [Phases of the Upgrade Workflow](https://leapp-repository.readthedocs.io/latest/upgrade-architecture-and-workflow/phases-overview.html)
+- [Coding guidelines](https://leapp-repository.readthedocs.io/latest/contributing/coding-guidelines.html)
+- [Best Practices for actor development](https://leapp.readthedocs.io/en/stable/best-practices.html)
diff --git a/skills/leapp-test-runner/SKILL.md b/skills/leapp-test-runner/SKILL.md
new file mode 100644
index 00000000..579aeb5d
--- /dev/null
+++ b/skills/leapp-test-runner/SKILL.md
@@ -0,0 +1,68 @@
+# Running Tests and Linters
+
+All non-container commands (`make lint`, `make test_no_lint`, `make fast_lint`, `make dev_test_no_lint`) require a local virtualenv and `REPOSITORIES` envar (e.g. `REPOSITORIES=common,el9toel10`). Create the virtualenv with `make install-deps` (RHEL/CentOS) or `make install-deps-fedora` (Fedora) — the Makefile handles activation internally, don't source the venv manually. Set `PYTHON_VENV=pythonX.X` to choose the Python version (default: `python3.6`). Container commands are self-contained and recommended as the default.
+
+## Container-based testing (CI-equivalent, no venv needed)
+
+Three container options, each matching a target Python version:
+
+| Container | Python | Used for |
+|-----------|--------|----------|
+| `el8` | 3.6 | `common`, `el8toel9` |
+| `el9` | 3.9 | `common`, `el8toel9`, `el9toel10` |
+| `f42` | 3.13 | lint-only (latest tooling) |
+
+Pick container(s) based on which repositories your change touches:
+- `common` actors: test on `el8` **and** `el9` (both Python versions must pass).
+- `el8toel9` only: `el8` is sufficient, `el9` is a bonus.
+- `el9toel10` only: `el9`.
+
+```bash
+# Full test (lint + unit tests) in container — recommended
+TEST_CONTAINER=el9 make test_container
+
+# Tests only, skip lint — faster iteration
+TEST_CONTAINER=el9 make test_container_no_lint
+
+# All containers at once
+make test_container_all
+```
+
+## Single-actor testing (fastest feedback, requires venv)
+
+Use during development when iterating on one actor:
+
+```bash
+ACTOR=checkmemory make dev_test_no_lint
+```
+
+## Linting
+
+```bash
+# Lint in container (recommended, no venv needed)
+make lint_container
+
+# Full lint locally (requires venv): pylint + flake8 + isort
+make lint
+
+# Lint only local changes relative to main branch (requires venv)
+make fast_lint
+
+# Auto-fix isort violations locally (requires venv)
+make lint_fix
+```
+
+## Choosing what to run
+
+| Situation | Command |
+|-----------|---------|
+| Quick check during development | `ACTOR=<name> make dev_test_no_lint` |
+| Before pushing | `TEST_CONTAINER=elX make test_container` |
+| Full CI confidence | `make test_container_all` |
+| Only lint issues | `make fast_lint` or `TEST_CONTAINER=elX make lint_container` |
+
+## Interpreting failures
+
+- **pylint / flake8**: fix the reported file and line.
+- **isort**: run `make lint_fix` to auto-sort imports, then verify.
+- **pytest**: read the traceback; check if the test uses `monkeypatch.setattr` correctly and if mocked models match current schema.
\ No newline at end of file
diff --git a/skills/leapp-unit-tests/SKILL.md b/skills/leapp-unit-tests/SKILL.md
new file mode 100644
index 00000000..894d4271
--- /dev/null
+++ b/skills/leapp-unit-tests/SKILL.md
@@ -0,0 +1,94 @@
+# Writing Unit Tests
+
+
+## File placement
+
+### Tests for actors
+
+Tests live in the tests directory under an actor:
+
+```
+repos/system_upgrade/<leapp_repo_dir>/actors/<actor_name>/
+├── actor.py
+├── libraries/<actor_name>.py
+└── tests/test_<actor_name>.py # <-- tests go here
+```
+
+### Tests for shared libraries
+
+Tests for shared libraries are in the `tests` sub-directory next to the shared library, like:
+
+```
+repos/system_upgrade/<leapp_repo_dir>/
+├── <libA>.py
+├── tests/test_<libA>.py # <-- tests for libA go here
+├── <pathB>/<libB>.py
+└── <pathB>/tests/test_<libB>.py # <-- tests for libB go here
+```
+
+## Key testing utilities
+
+As mocked objects, use functions and classes present in `leapp.libraries.common.testutils` - especially:
+
+| Utility | Purpose |
+| ------------------------- | ---------------------------------------------------------- |
+| `CurrentActorMocked(...)` | Mock `api.current_actor` with configuration, version, etc. |
+| `produce_mocked()` | Capture `api.produce(...)` calls for assertions |
+| `create_report_mocked()` | Capture `reporting.create_report(...)` calls |
+| `logger_mocked()` | Capture log output |
+
+
+## Standard test pattern
+
+Test the **library**, not the actor class directly:
+
+```python
+from leapp.libraries.actor import <actor_lib>
+from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import <ConsumedModel>, <ProducedModel>
+
+
+def test_<scenario>(monkeypatch):
+ # 1. Mock current_actor context
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ # 2. Mock consumed messages
+ monkeypatch.setattr(api, 'consume', lambda x: iter([<ConsumedModel>(...)]))
+
+ # 3. Mock produce
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ # 4. Call library function
+ <actor_lib>.process()
+
+ # 5. Assert on produced messages
+ assert api.produce.called
+ assert isinstance(api.produce.model_instances[0], <ProducedModel>)
+```
+
+## Rules
+
+
+- Test the library, not `actor.py` — the `process()` method should be a thin wrapper.
+- Pass consumed messages via `CurrentActorMocked(msgs=[...])`.
+- Mock file I/O instead of creating temp files when possible. Use the `leapp_tmpdir` fixture when tempfiles are needed.
+- Use `monkeypatch.setattr` over `unittest.mock.patch` — monkeypatch is the project convention.
+- Cover at least: happy path, edge/error cases, and inhibitor conditions if applicable.
+- If you intentionally skip a test scenario, add a comment explaining why.
+- Use consistent string quoting within a file.
+- Use pytest.mark.parametrize where possible.
+- Never use type() when creating mock classes.
+- Use module-level constants with "_" prefix.
+
+- Name test functions clearly after the behavior: `test_inhibits_when_fips_enabled` not `test_fips_1`.
+- Look at existing tests in `repos/system_upgrade/common/actors/` for project conventions before writing new ones.
+
+## Running tests
+
+See `skills/leapp-test-runner/` for full commands. Quick single-actor run:
+
+```bash
+ACTOR=<actor_name> make dev_test_no_lint
+```
+
--
2.54.0

View File

@ -0,0 +1,230 @@
From 3268f9a5cc41755bb2fecdbeb81f73dd788f74b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= <ihuguet@redhat.com>
Date: Thu, 23 Apr 2026 09:25:39 +0200
Subject: [PATCH 078/105] el8toel9: networkdeprecations: warn about ifcfg
devices not controlled by NM
Network devices currently configured via ifcfg have to be configured by
NetworkManager in EL9. If NetworkManager is disabled, connectivity will
be lost. Generate a report with HIGH severity in that case.
If devices are configured with NM_CONTROLLED=no, then ignore it, as they
are being explicitly excluded from NetworkManager control.
Assisted-by: Claude 4.6 Opus
Jira: https://redhat.atlassian.net/browse/RHEL-133966
---
.../actors/networkdeprecations/actor.py | 4 +-
.../libraries/networkdeprecations.py | 53 +++++++++++-
.../tests/unit_test_networkdeprecations.py | 82 ++++++++++++++++++-
3 files changed, 135 insertions(+), 4 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/networkdeprecations/actor.py b/repos/system_upgrade/el8toel9/actors/networkdeprecations/actor.py
index 3074a3c7..8515ec69 100644
--- a/repos/system_upgrade/el8toel9/actors/networkdeprecations/actor.py
+++ b/repos/system_upgrade/el8toel9/actors/networkdeprecations/actor.py
@@ -1,6 +1,6 @@
from leapp.actors import Actor
from leapp.libraries.actor import networkdeprecations
-from leapp.models import IfCfg, NetworkManagerConnection, Report
+from leapp.models import IfCfg, NetworkManagerConnection, Report, SystemdServicesInfoSource
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -16,7 +16,7 @@ class CheckNetworkDeprecations(Actor):
"""
name = "network_deprecations"
- consumes = (IfCfg, NetworkManagerConnection,)
+ consumes = (IfCfg, NetworkManagerConnection, SystemdServicesInfoSource,)
produces = (Report,)
tags = (ChecksPhaseTag, IPUWorkflowTag,)
diff --git a/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py b/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
index 92dfc51d..d4f2106b 100644
--- a/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
+++ b/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
@@ -1,12 +1,13 @@
from leapp import reporting
from leapp.libraries.stdlib import api
-from leapp.models import IfCfg, NetworkManagerConnection
+from leapp.models import IfCfg, NetworkManagerConnection, SystemdServicesInfoSource
FMT_LIST_SEPARATOR = '\n - '
def process():
wep_files = []
+ nm_controlled_files = []
# Scan NetworkManager native keyfile connections
for nmconn in api.consume(NetworkManagerConnection):
@@ -26,6 +27,8 @@ def process():
if ifcfg.secrets is not None:
props = props + ifcfg.secrets
+ nm_controlled = True
+
for prop in props:
name = prop.name
value = prop.value
@@ -40,6 +43,14 @@ def process():
wep_files.append(ifcfg.filename)
continue
+ # NM_CONTROLLED
+ if name == 'NM_CONTROLLED' and value.lower() in ("no", "false", "f", "n", "0"):
+ nm_controlled = False
+ continue
+
+ if nm_controlled:
+ nm_controlled_files.append(ifcfg.filename)
+
if wep_files:
title = 'Wireless networks using unsupported WEP encryption detected'
summary = ('The Wired Equivalent Privacy (WEP) algorithm used for'
@@ -65,3 +76,43 @@ def process():
reporting.RelatedResource('file', fname)
for fname in wep_files
])
+
+ if nm_controlled_files and _is_nm_service_disabled():
+ title = 'ifcfg files expect NetworkManager but the service is disabled'
+ summary = (
+ 'NetworkManager is disabled on the system, but there are ifcfg'
+ ' configuration files that do not set NM_CONTROLLED=no. After the'
+ ' upgrade, legacy network scripts will no longer be available and'
+ ' NetworkManager will be the only way to manage networking. These'
+ ' connections will not be active unless NetworkManager is enabled.'
+ ' Files with the problematic configuration:{}'
+ ).format(
+ ''.join(['{}{}'.format(FMT_LIST_SEPARATOR, f) for f in nm_controlled_files])
+ )
+ remediation = (
+ 'Either enable the NetworkManager service before upgrading, or add'
+ ' NM_CONTROLLED=no to the ifcfg files that should not be managed'
+ ' by NetworkManager.'
+ )
+ reporting.create_report([
+ reporting.Title(title),
+ reporting.Summary(summary),
+ reporting.Remediation(hint=remediation),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.NETWORK, reporting.Groups.SERVICES]),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.RelatedResource('package', 'NetworkManager'),
+ ] + [
+ reporting.RelatedResource('file', fname)
+ for fname in nm_controlled_files
+ ])
+
+
+def _is_nm_service_disabled():
+ services_info = next(api.consume(SystemdServicesInfoSource), None)
+ if not services_info:
+ return True
+ for svc in services_info.service_files:
+ if svc.name == 'NetworkManager.service':
+ return svc.state == 'disabled'
+ return True
diff --git a/repos/system_upgrade/el8toel9/actors/networkdeprecations/tests/unit_test_networkdeprecations.py b/repos/system_upgrade/el8toel9/actors/networkdeprecations/tests/unit_test_networkdeprecations.py
index 659ab993..e9fa69ac 100644
--- a/repos/system_upgrade/el8toel9/actors/networkdeprecations/tests/unit_test_networkdeprecations.py
+++ b/repos/system_upgrade/el8toel9/actors/networkdeprecations/tests/unit_test_networkdeprecations.py
@@ -3,7 +3,9 @@ from leapp.models import (
IfCfgProperty,
NetworkManagerConnection,
NetworkManagerConnectionProperty,
- NetworkManagerConnectionSetting
+ NetworkManagerConnectionSetting,
+ SystemdServiceFile,
+ SystemdServicesInfoSource
)
from leapp.reporting import Report
from leapp.utils.report import is_inhibitor
@@ -122,3 +124,81 @@ def test_ifcfg_dynamic_wep(current_actor_context):
current_actor_context.run()
report_fields = current_actor_context.consume(Report)[0].report
assert is_inhibitor(report_fields)
+
+
+def _nm_service_info(state):
+ return SystemdServicesInfoSource(service_files=[
+ SystemdServiceFile(name='NetworkManager.service', state=state),
+ ])
+
+
+def test_nm_disabled_ifcfg_no_nm_controlled(current_actor_context):
+ """
+ Report if NM is disabled and ifcfg files don't have NM_CONTROLLED=no.
+ """
+
+ current_actor_context.feed(_nm_service_info('disabled'))
+ current_actor_context.feed(IfCfg(filename='/NM/ifcfg-eth0', properties=(
+ IfCfgProperty(name='TYPE', value='Ethernet'),
+ )))
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert len(reports) == 1
+ assert 'ifcfg files expect NetworkManager' in reports[0].report['title']
+
+
+def test_nm_disabled_ifcfg_nm_controlled_no(current_actor_context):
+ """
+ No report if NM is disabled but all ifcfg files have NM_CONTROLLED=no.
+ """
+
+ current_actor_context.feed(_nm_service_info('disabled'))
+ current_actor_context.feed(IfCfg(filename='/NM/ifcfg-eth0', properties=(
+ IfCfgProperty(name='TYPE', value='Ethernet'),
+ IfCfgProperty(name='NM_CONTROLLED', value='no'),
+ )))
+ current_actor_context.run()
+ assert not current_actor_context.consume(Report)
+
+
+def test_nm_enabled_ifcfg_no_nm_controlled(current_actor_context):
+ """
+ No report if NM is enabled, even if ifcfg files lack NM_CONTROLLED=no.
+ """
+
+ current_actor_context.feed(_nm_service_info('enabled'))
+ current_actor_context.feed(IfCfg(filename='/NM/ifcfg-eth0', properties=(
+ IfCfgProperty(name='TYPE', value='Ethernet'),
+ )))
+ current_actor_context.run()
+ assert not current_actor_context.consume(Report)
+
+
+def test_nm_disabled_mixed_ifcfg(current_actor_context):
+ """
+ Report only the ifcfg files that lack NM_CONTROLLED=no when NM is disabled.
+ """
+
+ current_actor_context.feed(_nm_service_info('disabled'))
+ current_actor_context.feed(IfCfg(filename='/NM/ifcfg-eth0', properties=(
+ IfCfgProperty(name='TYPE', value='Ethernet'),
+ )))
+ current_actor_context.feed(IfCfg(filename='/NM/ifcfg-eth1', properties=(
+ IfCfgProperty(name='TYPE', value='Ethernet'),
+ IfCfgProperty(name='NM_CONTROLLED', value='no'),
+ )))
+ current_actor_context.run()
+ reports = current_actor_context.consume(Report)
+ assert len(reports) == 1
+ assert '/NM/ifcfg-eth0' in reports[0].report['summary']
+ assert '/NM/ifcfg-eth1' not in reports[0].report['summary']
+
+
+def test_nm_disabled_no_ifcfg(current_actor_context):
+ """
+ No report if NM is disabled but there are no ifcfg files at all.
+ """
+
+ current_actor_context.feed(_nm_service_info('disabled'))
+ current_actor_context.run()
+ assert not current_actor_context.consume(Report)
--
2.54.0

View File

@ -0,0 +1,441 @@
From 924276d11589cefba16901a30d7269062a06eb3b Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Mon, 4 May 2026 14:07:53 +0530
Subject: [PATCH 079/105] Remove enforcing=1 from upgrade and target kernel
boot entries during IPU
Detect enforcing=1 on the running kernel (KernelCmdline) or in any bootloader
entry. Bootloader detection is collected by systemfacts via `grubby --info ALL`
and exposed as SELinuxFacts.enforcing_via_any_cmdline.
When enforcing=1 is present, checkselinux (Checks phase) emits
UpgradeKernelCmdlineArgTasks to remove enforcing=1 from the interim upgrade
boot entry, and TargetKernelCmdlineArgTasks to remove it from the target kernel
entry and update the default kernel command line during finalization.
An INFO report with remediation guidance to restore enforcing=1 after upgrade is also
produced. Original boot entries for existing kernels are not modified.
JIRA: RHEL-162192
---
.../common/actors/checkselinux/actor.py | 15 +++-
.../checkselinux/libraries/checkselinux.py | 80 ++++++++++++++++++-
.../checkselinux/tests/test_checkselinux.py | 72 ++++++++++++++---
.../systemfacts/libraries/systemfacts.py | 20 +++++
.../tests/test_systemfacts_selinux.py | 61 +++++++++++++-
.../common/models/selinuxfacts.py | 4 +
6 files changed, 236 insertions(+), 16 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkselinux/actor.py b/repos/system_upgrade/common/actors/checkselinux/actor.py
index 4e8fa7df..d618b5d5 100644
--- a/repos/system_upgrade/common/actors/checkselinux/actor.py
+++ b/repos/system_upgrade/common/actors/checkselinux/actor.py
@@ -1,6 +1,15 @@
from leapp.actors import Actor
from leapp.libraries.actor import checkselinux
-from leapp.models import KernelCmdlineArg, Report, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision
+from leapp.models import (
+ KernelCmdline,
+ KernelCmdlineArg,
+ Report,
+ SELinuxFacts,
+ SelinuxPermissiveDecision,
+ SelinuxRelabelDecision,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -13,12 +22,14 @@ class CheckSelinux(Actor):
"""
name = 'check_se_linux'
- consumes = (SELinuxFacts,)
+ consumes = (SELinuxFacts, KernelCmdline)
produces = (
KernelCmdlineArg,
Report,
SelinuxPermissiveDecision,
SelinuxRelabelDecision,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks,
)
tags = (ChecksPhaseTag, IPUWorkflowTag)
diff --git a/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py b/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
index dbd79adf..4f892777 100644
--- a/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
+++ b/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
@@ -1,11 +1,85 @@
from leapp import reporting
+from leapp.libraries.common.config import architecture
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
-from leapp.models import KernelCmdlineArg, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision
+from leapp.models import (
+ KernelCmdline,
+ KernelCmdlineArg,
+ SELinuxFacts,
+ SelinuxPermissiveDecision,
+ SelinuxRelabelDecision,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
DOC_URL = 'https://red.ht/rhel9-disabling-selinux'
+_ENFORCING_ONE_ARG = KernelCmdlineArg(key='enforcing', value='1')
+
+
+def _proc_cmdline_has_enforcing_one(kernel_cmdline):
+ """
+ Checks if the kernel command line contains enforcing=1.
+ """
+ if not kernel_cmdline:
+ return False
+ for param in kernel_cmdline.parameters:
+ if param.key == 'enforcing' and param.value == '1':
+ return True
+ return False
+
+
+def _request_removal_of_enforcing_one():
+ """
+ Schedule stripping of enforcing=1 from the upgrade and target boot entries.
+
+ The upgrade workflow runs with SELinux permissive, but if enforcing=1 is present on
+ the running kernel or in any bootloader entry it can override that. We produce:
+
+ * ``UpgradeKernelCmdlineArgTasks`` so the interim upgrade boot entry drops enforcing=1
+ (consumed by ``addupgradebootentry``).
+ * ``TargetKernelCmdlineArgTasks`` so the target kernel entry and the default kernel
+ command line configuration are updated during finalization (consumed by
+ ``kernelcmdlineconfig``), ensuring that future kernels also omit enforcing=1.
+
+ Original boot entries for existing (non-target) kernels are left untouched.
+
+ ``SELinuxFacts.enforcing_via_any_cmdline`` is populated earlier by the
+ ``systemfacts`` actor.
+ """
+ arch = api.current_actor().configuration.architecture
+ arch_msg = ' Then make sure to run zipl to update the boot menu.' if arch == architecture.ARCH_S390X else ''
+ doc_url = 'https://red.ht/rhel-{}-configure-kernel-cmdline'.format(get_target_major_version())
+ hint = (
+ 'After the upgrade, add the `enforcing=1` kernel command line argument back if it is wanted.'
+ ' Run "grubby --update-kernel=ALL --args=enforcing=1" to enable enforcing on all boot entries'
+ f' and set it as default.{arch_msg}'
+ ' Follow the documentation in the attached link for more details.'
+ )
+
+ api.produce(UpgradeKernelCmdlineArgTasks(to_remove=[_ENFORCING_ONE_ARG]))
+ api.produce(TargetKernelCmdlineArgTasks(to_remove=[_ENFORCING_ONE_ARG]))
+ reporting.create_report([
+ reporting.Title('Kernel boot parameter enforcing=1 will be removed'),
+ reporting.Summary(
+ 'The kernel parameter enforcing=1 forces SELinux enforcing mode at boot and overrides '
+ 'the permissive configuration used during the upgrade. Leapp will remove enforcing=1 from '
+ 'the upgrade and target kernel boot entries and update the default kernel command line '
+ 'configuration so it does not persist after the upgrade. Existing boot entries for other '
+ 'kernels will not be modified.'
+ ),
+ reporting.Remediation(hint=hint),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([
+ reporting.Groups.SELINUX,
+ reporting.Groups.BOOT,
+ reporting.Groups.KERNEL,
+ reporting.Groups.POST,
+ ]),
+ reporting.ExternalLink(url=doc_url, title='Configuring kernel command line parameters'),
+ ])
+
def process():
facts = next(api.consume(SELinuxFacts), None)
@@ -49,6 +123,10 @@ def process():
])
return
+ kernel_cmdline = next(api.consume(KernelCmdline), None)
+ if _proc_cmdline_has_enforcing_one(kernel_cmdline) or facts.enforcing_via_any_cmdline:
+ _request_removal_of_enforcing_one()
+
if conf_status in ('enforcing', 'permissive'):
api.produce(SelinuxRelabelDecision(set_relabel=True))
reporting.create_report([
diff --git a/repos/system_upgrade/common/actors/checkselinux/tests/test_checkselinux.py b/repos/system_upgrade/common/actors/checkselinux/tests/test_checkselinux.py
index 2a42b3ef..a0cd2ec9 100644
--- a/repos/system_upgrade/common/actors/checkselinux/tests/test_checkselinux.py
+++ b/repos/system_upgrade/common/actors/checkselinux/tests/test_checkselinux.py
@@ -4,11 +4,24 @@ from leapp import reporting
from leapp.libraries.actor import checkselinux
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
-from leapp.models import Report, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision
-from leapp.snactor.fixture import current_actor_context
-
-
-def create_selinuxfacts(static_mode, enabled, policy='targeted', mls_enabled=True):
+from leapp.models import (
+ KernelCmdline,
+ KernelCmdlineArg,
+ SELinuxFacts,
+ SelinuxPermissiveDecision,
+ SelinuxRelabelDecision,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
+
+
+def create_selinuxfacts(
+ static_mode,
+ enabled,
+ policy='targeted',
+ mls_enabled=True,
+ enforcing_via_any_cmdline=False,
+):
runtime_mode = static_mode if static_mode != 'disabled' else None
return SELinuxFacts(
@@ -16,7 +29,8 @@ def create_selinuxfacts(static_mode, enabled, policy='targeted', mls_enabled=Tru
static_mode=static_mode,
enabled=enabled,
policy=policy,
- mls_enabled=mls_enabled
+ mls_enabled=mls_enabled,
+ enforcing_via_any_cmdline=enforcing_via_any_cmdline,
)
@@ -25,7 +39,7 @@ def test_actor_schedule_relabelling(monkeypatch, mode):
fact = create_selinuxfacts(static_mode=mode, enabled=True)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, KernelCmdline(parameters=[])]))
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -38,7 +52,7 @@ def test_actor_schedule_relabelling(monkeypatch, mode):
def test_actor_set_permissive(monkeypatch):
relabel = create_selinuxfacts(static_mode='enforcing', enabled=True)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[relabel]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[relabel, KernelCmdline(parameters=[])]))
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -56,7 +70,9 @@ def test_actor_selinux_disabled(monkeypatch, el8_to_el9):
target_ver = '8' if not el8_to_el9 else '9'
monkeypatch.setattr(checkselinux, 'get_target_major_version', lambda: target_ver)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[disabled]))
+ monkeypatch.setattr(
+ api, 'current_actor', CurrentActorMocked(msgs=[disabled, KernelCmdline(parameters=[])])
+ )
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -67,3 +83,41 @@ def test_actor_selinux_disabled(monkeypatch, el8_to_el9):
else:
assert not api.produce.model_instances
assert reporting.create_report.called
+
+
+def test_enforcing_one_in_proc_cmdline_schedules_removal(monkeypatch):
+ fact = create_selinuxfacts(static_mode='permissive', enabled=True)
+ kcmd = KernelCmdline(parameters=[KernelCmdlineArg(key='enforcing', value='1')])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, kcmd]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ checkselinux.process()
+
+ upgrade_produced = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert upgrade_produced
+ assert upgrade_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+ target_produced = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert target_produced
+ assert target_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+
+def test_enforcing_one_only_in_bootloader_schedules_removal(monkeypatch):
+ fact = create_selinuxfacts(
+ static_mode='permissive', enabled=True, enforcing_via_any_cmdline=True
+ )
+ kcmd = KernelCmdline(parameters=[KernelCmdlineArg(key='ro')])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, kcmd]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ checkselinux.process()
+
+ upgrade_produced = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert upgrade_produced
+ assert upgrade_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+ target_produced = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert target_produced
+ assert target_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
diff --git a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
index ba7bdb82..578f96cb 100644
--- a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
+++ b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
@@ -230,6 +230,24 @@ def get_repositories_status():
})
+def _bootloader_entries_contain_enforcing_one():
+ """
+ True if grubby reports enforcing=1 in any boot loader entry's kernel arguments.
+ """
+ try:
+ out = run(['/usr/sbin/grubby', '--info', 'ALL'], split=False)['stdout']
+ except (OSError, CalledProcessError):
+ api.current_logger().debug(
+ 'grubby --info ALL failed; assuming no bootloader enforcing=1', exc_info=True
+ )
+ return False
+
+ for line in out.splitlines():
+ if line.startswith('args=') and 'enforcing=1' in line[len('args='):].strip('"').split():
+ return True
+ return False
+
+
def get_selinux_status():
""" Get SELinux status information """
# will be None if something went wrong or contain SELinuxFacts otherwise
@@ -259,6 +277,8 @@ def get_selinux_status():
outdata['static_mode'] = 'disabled'
outdata['policy'] = 'targeted'
+ outdata['enforcing_via_any_cmdline'] = _bootloader_entries_contain_enforcing_one()
+
res = SELinuxFacts(**outdata)
return res
diff --git a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
index d36900bd..85a3ff3d 100644
--- a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
+++ b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
@@ -2,6 +2,7 @@ import warnings
import pytest
+from leapp.libraries.actor import systemfacts
from leapp.libraries.actor.systemfacts import get_selinux_status
from leapp.models import SELinuxFacts
@@ -20,6 +21,23 @@ reason_to_skip_msg = "Selinux is not available"
# FIXME: create valid tests...
+def _run_result(stdout):
+ return {'stdout': stdout, 'stderr': '', 'signal': None, 'exit_code': 0, 'pid': 1}
+
+
+@pytest.fixture(autouse=True)
+def stub_grubby_info_all_without_enforcing(monkeypatch):
+ """
+ Avoid invoking real grubby from get_selinux_status(); default: no enforcing=1 in boot entries.
+ """
+
+ def mocked_run(cmd, split=False, **dummy_kwargs):
+ assert cmd == ['/usr/sbin/grubby', '--info', 'ALL'], f"Unexpected grubby cmd: {cmd}"
+ return _run_result('index=0\nkernel="/boot/vmlinuz-1"\nargs="ro rhgb quiet"\nroot="UUID=xxx"\n')
+
+ monkeypatch.setattr(systemfacts, 'run', mocked_run)
+
+
@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_enabled_enforcing(monkeypatch):
"""
@@ -34,7 +52,8 @@ def test_selinux_enabled_enforcing(monkeypatch):
'mls_enabled': True,
'enabled': True,
'runtime_mode': 'enforcing',
- 'static_mode': 'enforcing'}
+ 'static_mode': 'enforcing',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -52,7 +71,8 @@ def test_selinux_enabled_permissive(monkeypatch):
'mls_enabled': True,
'enabled': True,
'runtime_mode': 'permissive',
- 'static_mode': 'permissive'}
+ 'static_mode': 'permissive',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -70,7 +90,8 @@ def test_selinux_disabled(monkeypatch):
'mls_enabled': False,
'enabled': False,
'runtime_mode': 'permissive',
- 'static_mode': 'permissive'}
+ 'static_mode': 'permissive',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -93,6 +114,38 @@ def test_selinux_disabled_no_config_file(monkeypatch):
'mls_enabled': False,
'enabled': False,
'runtime_mode': 'permissive',
- 'static_mode': 'disabled'}
+ 'static_mode': 'disabled',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
+
+
+@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
+@pytest.mark.parametrize('grubby_stdout,expected', [
+ ('index=0\nargs="ro enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=1 rhgb quiet"\n', True),
+ ('index=0\nargs="enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=1"\nindex=1\nargs="ro rhgb quiet"\n', True),
+ ('index=0\nargs="ro rhgb quiet"\nindex=1\nargs="ro enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=0"\n', False),
+ ('index=0\nargs="ro rhgb quiet"\n', False),
+ ('index=0\nargs="ro fooenforcing=1"\n', False),
+ ('index=0\nargs="ro enforcing=1bar"\n', False),
+ ('index=0\nargs="ro"\n', False),
+ ('index=0\nargs=""\n', False),
+])
+def test_bootloader_enforcing_one_detected(monkeypatch, grubby_stdout, expected):
+ monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 1)
+ monkeypatch.setattr(selinux, 'security_getenforce', lambda: 1)
+ monkeypatch.setattr(selinux, 'selinux_getenforcemode', lambda: [0, 1])
+ monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 1)
+ monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
+
+ def mocked_run(cmd, split=False, **dummy_kwargs):
+ assert cmd == ['/usr/sbin/grubby', '--info', 'ALL'], f"Unexpected grubby cmd: {cmd}"
+ return _run_result(grubby_stdout)
+
+ monkeypatch.setattr(systemfacts, 'run', mocked_run)
+
+ fact = get_selinux_status()
+ assert fact.enforcing_via_any_cmdline is expected
diff --git a/repos/system_upgrade/common/models/selinuxfacts.py b/repos/system_upgrade/common/models/selinuxfacts.py
index 12b0cf8a..9376e4b1 100644
--- a/repos/system_upgrade/common/models/selinuxfacts.py
+++ b/repos/system_upgrade/common/models/selinuxfacts.py
@@ -10,3 +10,7 @@ class SELinuxFacts(Model):
enabled = fields.Boolean()
policy = fields.StringEnum(['targeted', 'minimum', 'mls'])
mls_enabled = fields.Boolean()
+ enforcing_via_any_cmdline = fields.Boolean(default=False)
+ """
+ True when any bootloader entries have set enforcing=1.
+ """
--
2.54.0

View File

@ -0,0 +1,71 @@
From fdc5e80760f08bbef095713d7d592758f551d6b8 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Thu, 21 May 2026 13:23:02 +0200
Subject: [PATCH 080/105] Fix utils/actor_path.py crash on invalid repo paths
When a scan of a path which does not contain a valid leapp repository is
attempted in utils/actor.py, it crashes. For example, run with
REPOSITORIES=common,invalid:
$ ACTOR=mysql_check REPOSITORIES=common,invalid make test
ERROR: Unknown error: 'name'
ERROR: Possibly you need newer version of the leapp framework
ERROR: e.g.: rm -rf .tut && make install-deps-fedora
Traceback (most recent call last):
File "utils/actor_path.py", line 50, in <module>
main()
File "utils/actor_path.py", line 33, in main
manager.add_repo(scan_repo(os.path.join(SYSUPG_REPO, repo)))
File "/home/user/leapp-repository/tut/lib/python3.6/site-packages/leapp/repository/scan.py", line 70, in scan_repo
return scan(Repository(path), path)
File "/home/user/leapp-repository/tut/lib/python3.6/site-packages/leapp/repository/__init__.py", line 40, in __init__
self.name = get_repository_name(directory)
File "/home/user/leapp-repository/tut/lib/python3.6/site-packages/leapp/utils/repository.py", line 96, in get_repository_name
return get_repository_metadata(path)['name']
KeyError: 'name'
With this patch, there is an error with suggestion:
$ ACTOR=mysql_check REPOSITORIES=common,invalid make test
ERROR: Failed to scan repository at repos/system_upgrade/invalid, make sure there is a valid leapp repository at the path.
. tut/bin/activate; \
echo "--- Linting ... ---" && \
SEARCH_PATH="" && \
echo "Using search path '${SEARCH_PATH}'" && \
echo "--- Running pylint ---" && \
bash -c "[[ ! -z '${SEARCH_PATH}' ]] && find ${SEARCH_PATH} -name '*.py' | sort -u | xargs pylint -j0 " && \
echo "--- Running flake8 ---" && \
bash -c "[[ ! -z '${SEARCH_PATH}' ]] && flake8 ${SEARCH_PATH} "
--- Linting ... ---
Using search path ''
--- Running pylint ---
make: *** [Makefile:343: lint] Error 1
---
utils/actor_path.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/utils/actor_path.py b/utils/actor_path.py
index 3c61ce79..68b70da6 100755
--- a/utils/actor_path.py
+++ b/utils/actor_path.py
@@ -30,7 +30,17 @@ def main():
# the scanning and resolving done below
manager = RepositoryManager()
for repo in repos:
- manager.add_repo(scan_repo(os.path.join(SYSUPG_REPO, repo)))
+ repo_path = os.path.join(SYSUPG_REPO, repo)
+ try:
+ repo_obj = scan_repo(repo_path)
+ except KeyError: # this is not a nice API, should handle this better in the framework
+ sys.stderr.write(
+ "ERROR: Failed to scan repository at {}, make sure there"
+ " is a valid leapp repository at the path.\n".format(repo_path)
+ )
+ return
+
+ manager.add_repo(repo_obj)
_resolve_repository_links(manager=manager, include_locals=True)
manager.load()
else:
--
2.54.0

View File

@ -0,0 +1,119 @@
From 1797c35db27e5af2d54598a05ded4fd55aed3765 Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Thu, 14 May 2026 13:41:34 +0530
Subject: [PATCH 081/105] fix(breadcrumbs): use distro-aware OS names for
source and target
The original code hardcoded "Red Hat Enterprise Linux" for both
source_os and target_os in /etc/migration-results breadcrumbs.
With multiple distros now supported (CentOS Stream, AlmaLinux, etc.),
this produced incorrect records for non-RHEL upgrades.
- Add DISTRO_NAMES mapping in command_utils.py for CLI-layer
use, with a sync note referencing distro_id_to_pretty_name() in
the repo library (which cannot be imported from the CLI layer).
- Replace inline "Red Hat Enterprise Linux" formatting with a single
_get_os_name(ipu_cfg, direction) helper that resolves the display
name from IPUConfig distro/version metadata.
- Return 'N/A' when distro or version data is missing instead of
silently assuming RHEL.
JIRA: RHEL-156580
---
commands/command_utils.py | 14 ++++++++++++
commands/upgrade/breadcrumbs.py | 22 +++++++++++++++----
.../system_upgrade/common/libraries/distro.py | 4 ++++
3 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/commands/command_utils.py b/commands/command_utils.py
index 735144f8..20ae1b48 100644
--- a/commands/command_utils.py
+++ b/commands/command_utils.py
@@ -39,6 +39,20 @@ class DistroIDs(str, Enum):
ALMALINUX = 'almalinux'
+DISTRO_NAMES = {
+ DistroIDs.RHEL: 'Red Hat Enterprise Linux',
+ DistroIDs.CENTOS: 'CentOS Stream',
+ DistroIDs.ALMALINUX: 'AlmaLinux',
+}
+"""
+Maps distro IDs to their user-facing display names (matching NAME in /etc/os-release).
+
+NOTE: keep in sync with distro_id_to_pretty_name() in
+repos/system_upgrade/common/libraries/distro.py the repo library
+is not importable from the CLI layer.
+"""
+
+
_DISTRO_VERSION_FORMATS = {
DistroIDs.RHEL: VersionFormats.MAJOR_MINOR,
DistroIDs.CENTOS: VersionFormats.MAJOR_ONLY,
diff --git a/commands/upgrade/breadcrumbs.py b/commands/upgrade/breadcrumbs.py
index 95a551c3..f5ca92e9 100644
--- a/commands/upgrade/breadcrumbs.py
+++ b/commands/upgrade/breadcrumbs.py
@@ -6,6 +6,7 @@ from functools import wraps
from itertools import chain
from leapp import FULL_VERSION
+from leapp.cli.commands.command_utils import DISTRO_NAMES
from leapp.libraries.stdlib.call import _call
from leapp.utils.audit import get_messages
@@ -15,6 +16,20 @@ except ImportError:
JSONDecodeError = ValueError
+def _get_os_name(ipu_cfg, direction):
+ """
+ Determine the OS display name from IPUConfig data.
+
+ :param direction: 'source' or 'target'
+ """
+ distro = (ipu_cfg.get('distro') or {}).get(direction, '')
+ version = (ipu_cfg.get('version') or {}).get(direction, '')
+ if not distro:
+ return 'N/A'
+ distro_name = DISTRO_NAMES.get(distro, 'unknown')
+ return '{} {}'.format(distro_name, version or 'N/A')
+
+
def runs_in_container():
"""
Check if the current process is running inside a container
@@ -95,10 +110,9 @@ class _BreadCrumbs:
self._crumbs['run_id'] = os.environ.get('LEAPP_EXECUTION_ID', 'N/A')
self._crumbs['leapp_file_changes'].extend(self._verify_leapp_pkgs())
messages = get_messages(('IPUConfig',), self._crumbs['run_id'])
- versions = json.loads((messages or [{}])[0].get('message', {}).get(
- 'data', '{}')).get('version', {'target': 'N/A', 'source': 'N/A'})
- self._crumbs['target_os'] = 'Red Hat Enterprise Linux {target}'.format(**versions)
- self._crumbs['source_os'] = 'Red Hat Enterprise Linux {source}'.format(**versions)
+ ipu_cfg = json.loads((messages or [{}])[0].get('message', {}).get('data', '{}'))
+ self._crumbs['source_os'] = _get_os_name(ipu_cfg, 'source')
+ self._crumbs['target_os'] = _get_os_name(ipu_cfg, 'target')
self._crumbs['activity_ended'] = datetime.datetime.utcnow().isoformat() + 'Z'
self._crumbs['env'] = {k: v for k, v in os.environ.items() if k.startswith('LEAPP_')}
try:
diff --git a/repos/system_upgrade/common/libraries/distro.py b/repos/system_upgrade/common/libraries/distro.py
index 1a5e3923..78ac79ca 100644
--- a/repos/system_upgrade/common/libraries/distro.py
+++ b/repos/system_upgrade/common/libraries/distro.py
@@ -227,6 +227,10 @@ def distro_id_to_pretty_name(distro_id):
Get pretty name for the given distro id.
The pretty name is what is found in the NAME field of /etc/os-release.
+
+ NOTE: a parallel mapping exists in commands/command_utils.py
+ (DISTRO_NAMES) for use in the CLI layer which cannot import
+ repo libraries. Keep both in sync when adding a new distro.
"""
return {
"rhel": "Red Hat Enterprise Linux",
--
2.54.0

View File

@ -0,0 +1,58 @@
From 09846327822b5f3ac6f021daed866990bde5f92b Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Fri, 27 Mar 2026 18:12:03 +0100
Subject: [PATCH 082/105] leapp_resume.service: use install_exec_t SELinux
context
The original fix to deal with AVC during execution of leapp when
booting to the upgraded system appeared to leave still a space to
generate new AVC messages during some operations. From our side,
the only way how we could be sure that we do not hit any additional
error on top of what root user would see, is to set an unconfined
context.
However, after discussion with SELinux guys, we decided to not do that
yet. It can be actually valuable when discovering potential problems
in applications - and there is no harm to the upgrade process as
the system is in the permissive mode.
Currently dealing with the RH Insights client, it has been
discovered that bug is in new SELinux policies for the client and not
in leapp-repository. But after the discussion, we are going to switch
the SELinux type context to `install_exec_t`
which should fit better to the upgrade process than the original one.
We will monitor the situation around AVC messages and make possible
further changes based on the feedback.
Jira: RHEL-161684
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../createresumeservice/files/leapp_resume.service | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
index 749b4b84..e31b5eb5 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
+++ b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
@@ -8,8 +8,15 @@ Wants=network-online.target
[Service]
Type=oneshot
-# FIXME: this is temporary workaround for Python3
-ExecStart=/bin/sh -c "/root/tmp_leapp_py3/leapp3 upgrade --resume"
+# NOTE(pstodulk): After discussion with the SELinux team, we try to stay
+# out of unconfined mode for now.
+#SELinuxContext=unconfined_u:unconfined_r:unconfined_t:s0
+
+# NOTE(pstodulk): Correction of the selinux type context. This one should fit to leapp
+# Ignore the exit code as this cmd is expected to fail if selinux is disabled
+ExecStartPre=-chcon -t install_exec_t /root/tmp_leapp_py3/leapp3
+ExecStart=/root/tmp_leapp_py3/leapp3 upgrade --resume
StandardOutput=journal+console
-# FIXME: this shouldn't be needed, but Satellite upgrade runs installer, and that's slow
+
+# NOTE(pstodulk): some actions can take a lot of time (e.g. satellite installer)
TimeoutStartSec=infinity
--
2.54.0

View File

@ -0,0 +1,380 @@
From b70ff330e9c6b9e570f24c63b0a28760c5e900b6 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Tue, 12 May 2026 19:11:26 +0200
Subject: [PATCH 083/105] config(rhui): Add config for setting rpm gpg keys to
remove
Add a config option obsolete_gpg_keys to allow users to specify obsolete
rpm gpg keys to be removed during the upgrade. The removeobsoletegpgkeys
then checks the config and produces the appropriate DNFWorkaround
messages to remove them, similar to distro keys.
This can be prefilled in the config shipped by leapp-rhui-* packages to
remove RPM GPG keys for cloud vendor keys, such as those which are used
to sign the RHUI client rpm.
Jira: RHEL-121979
---
.../actors/removeobsoletegpgkeys/actor.py | 5 +
.../libraries/removeobsoleterpmgpgkeys.py | 56 +++++++-
.../tests/test_removeobsoleterpmgpgkeys.py | 130 ++++++++++++++++--
repos/system_upgrade/common/configs/rhui.py | 27 +++-
4 files changed, 204 insertions(+), 14 deletions(-)
diff --git a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/actor.py b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/actor.py
index 58b15a84..f3694776 100644
--- a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/actor.py
+++ b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/actor.py
@@ -1,4 +1,5 @@
from leapp.actors import Actor
+from leapp.configs.common.rhui import all_rhui_cfg
from leapp.libraries.actor import removeobsoleterpmgpgkeys
from leapp.models import DNFWorkaround, InstalledRPM
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
@@ -17,10 +18,14 @@ class RemoveObsoleteGpgKeys(Actor):
- If converting, the obsolete keys are all of the keys provided by the
vendor of the source distribution.
+ Additionally, if RHUI configuration is active (use_config=True), GPG keys
+ specified in the RHUI obsolete_gpg_keys configuration will also be removed.
+
A DNFWorkaround is registered to actually remove the keys.
"""
name = "remove_obsolete_gpg_keys"
+ config_schemas = all_rhui_cfg
consumes = (InstalledRPM,)
produces = (DNFWorkaround,)
tags = (FactsPhaseTag, IPUWorkflowTag)
diff --git a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/libraries/removeobsoleterpmgpgkeys.py b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/libraries/removeobsoleterpmgpgkeys.py
index 7d047395..1db6a2ed 100644
--- a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/libraries/removeobsoleterpmgpgkeys.py
+++ b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/libraries/removeobsoleterpmgpgkeys.py
@@ -1,5 +1,8 @@
import itertools
+import re
+from leapp.configs.common.rhui import RHUI_CONFIG_SECTION, RhuiObsoleteGpgKeys, RhuiUseConfig
+from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import get_source_distro_id, get_target_distro_id
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.common.distro import get_distribution_data
@@ -7,6 +10,15 @@ from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import DNFWorkaround, InstalledRPM
+_GPG_PUBKEY_NVR_FORMAT = re.compile(r"^gpg-pubkey-[0-9a-f]{8}-[0-9a-f]{8}$")
+
+
+def _is_valid_pubkey_nvr(name):
+ """
+ Validate if a string is a valid gpg key RPM NVR
+ """
+ return _GPG_PUBKEY_NVR_FORMAT.match(name) is not None
+
def _is_key_installed(key):
"""
@@ -50,17 +62,51 @@ def _get_source_distro_keys():
]
+def _get_rhui_configured_keys():
+ """
+ Get obsolete cloud vendor keys specified in RHUI configuration.
+
+ Returns keys only when RHUI use_config is True, ensuring this is opt-in behavior.
+ Only returns keys that are actually installed on the system.
+ """
+ rhui_config = api.current_actor().config[RHUI_CONFIG_SECTION]
+
+ # Only apply RHUI keys when the leapp RHUI configuration is set
+ if not rhui_config[RhuiUseConfig.name]:
+ api.current_logger().debug("RHUI config disabled, ignoring obsolete RPM GPG keys")
+ return []
+
+ configured_keys = rhui_config[RhuiObsoleteGpgKeys.name]
+
+ for key in configured_keys:
+ if not _is_valid_pubkey_nvr(key):
+ raise StopActorExecutionError(
+ "Provided RHUI config contains invalid value for setting {}".format(
+ RhuiObsoleteGpgKeys.name
+ ),
+ details={
+ "details": "Entry {} is not a in valid RPM GPG name".format(key),
+ "hint": (
+ "The expected format is: gpg-pubkey-<version>-<release>,"
+ " for example: gpg-pubkey-d4082792-5b32db75"
+ ),
+ },
+ )
+
+ return [key for key in configured_keys if _is_key_installed(key)]
+
+
def register_dnfworkaround(keys):
api.produce(
DNFWorkaround(
display_name="remove obsolete RPM GPG keys from RPM DB",
script_path=api.current_actor().get_common_tool_path("removerpmgpgkeys"),
- script_args=keys,
+ script_args=list(keys),
)
)
-def process():
+def _get_all_obsolete_keys():
if get_source_distro_id() == get_target_distro_id():
# only upgrading - remove keys obsoleted in previous versions
keys = _get_obsolete_keys()
@@ -68,5 +114,11 @@ def process():
# also converting - we need to remove all keys from the source distro
keys = _get_source_distro_keys()
+ keys.extend(_get_rhui_configured_keys())
+ return set(keys)
+
+
+def process():
+ keys = _get_all_obsolete_keys()
if keys:
register_dnfworkaround(keys)
diff --git a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/tests/test_removeobsoleterpmgpgkeys.py b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/tests/test_removeobsoleterpmgpgkeys.py
index 8b9b842b..ff6b77ba 100644
--- a/repos/system_upgrade/common/actors/removeobsoletegpgkeys/tests/test_removeobsoleterpmgpgkeys.py
+++ b/repos/system_upgrade/common/actors/removeobsoletegpgkeys/tests/test_removeobsoleterpmgpgkeys.py
@@ -3,10 +3,12 @@ import unittest.mock as mock
import pytest
+from leapp.configs.common.rhui import RhuiObsoleteGpgKeys, RhuiUseConfig
+from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import removeobsoleterpmgpgkeys
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
-from leapp.models import InstalledRPM, RPM
+from leapp.models import DNFWorkaround, InstalledRPM, RPM
_CUR_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -15,6 +17,40 @@ def common_folder_path_mocked(folder):
return os.path.join(_CUR_DIR, "../../../files/", folder)
+@pytest.mark.parametrize(
+ "key, is_valid",
+ [
+ # too short
+ ("gpg-pubkey-10-10", False),
+ ("gpg-pubkey-888-abc", False),
+ ("gpg-d4082792-5b32db75j", False),
+ # too long ver
+ ("gpg-pubkey-1234456789-b32db75j8", False),
+ # end bound
+ ("gpg-pubkey-12345678-123456789", False),
+ # start bound
+ ("aaagpg-pubkey-12345678-12345678", False),
+ # invalid format
+ ("gpg-12345678-12345678", False),
+ ("pubkey-12345678-12345678", False),
+ ("gpg-pubkey-5b32db75j", False),
+ ("gpg-pubkey-d40827925b32db75j", False),
+ # non hex
+ ("gpg-pubkey-abcdefgh-aaaaaaaa", False),
+ ("gpg-pubkey-12345678-hhhhhhhh", False),
+ # uppercase
+ ("gpg-pubkey-DEADBEEF-12345678", False),
+ ("gpg-pubkey-12345678-DEADBEEF", False),
+ # Ok
+ ("gpg-pubkey-12345678-deadbeef", True),
+ ("gpg-pubkey-d4082792-5b32db75", True),
+ ("gpg-pubkey-2fa658e0-45700c69", True),
+ ],
+)
+def test_is_valid_pubkey_nvr(key, is_valid):
+ assert removeobsoleterpmgpgkeys._is_valid_pubkey_nvr(key) == is_valid
+
+
def test_is_key_installed(monkeypatch):
installed_rpms = InstalledRPM(
items=[
@@ -170,27 +206,47 @@ def test_get_source_distro_keys(monkeypatch, distro, expected):
)
def test_workaround_should_register(monkeypatch, keys, should_register):
monkeypatch.setattr(
- removeobsoleterpmgpgkeys, "_get_obsolete_keys", lambda: keys
+ removeobsoleterpmgpgkeys, "_get_all_obsolete_keys", lambda: keys
)
monkeypatch.setattr(api, "produce", produce_mocked())
monkeypatch.setattr(api, "current_actor", CurrentActorMocked())
removeobsoleterpmgpgkeys.process()
assert api.produce.called == should_register
+ if should_register:
+ inst = api.produce.model_instances[0]
+ assert isinstance(inst, DNFWorkaround)
+ assert inst.script_args == keys
-def test_process(monkeypatch):
+@pytest.mark.parametrize(
+ "obsolete_keys, src_distro_keys, rhui_keys",
+ [
+ (
+ ["gpg-pubkey-12345678-abcdefgh"],
+ ["gpg-pubkey-87654321-hgfedcba"],
+ ["gpg-pubkey-12344321-abecedaa"],
+ ),
+ # test if the keys are deduplicated
+ (
+ ["gpg-pubkey-12345678-abcdefgh"],
+ ["gpg-pubkey-12345678-abcdefgh"],
+ ["gpg-pubkey-12345678-abcdefgh"],
+ ),
+ ],
+)
+def test_process(monkeypatch, obsolete_keys, src_distro_keys, rhui_keys):
"""
Test that the correct path is taken depending on whether also converting
"""
- obsolete = ["gpg-pubkey-12345678-abcdefgh"]
- source_distro = ["gpg-pubkey-87654321-hgfedcba"]
-
monkeypatch.setattr(
- removeobsoleterpmgpgkeys, "_get_obsolete_keys", lambda: obsolete
+ removeobsoleterpmgpgkeys, "_get_obsolete_keys", lambda: obsolete_keys
)
monkeypatch.setattr(
- removeobsoleterpmgpgkeys, "_get_source_distro_keys", lambda: source_distro,
+ removeobsoleterpmgpgkeys, "_get_source_distro_keys", lambda: src_distro_keys,
+ )
+ monkeypatch.setattr(
+ removeobsoleterpmgpgkeys, "_get_rhui_configured_keys", lambda: rhui_keys
)
# upgrade only path
@@ -202,7 +258,7 @@ def test_process(monkeypatch):
):
removeobsoleterpmgpgkeys.process()
removeobsoleterpmgpgkeys.register_dnfworkaround.assert_called_once_with(
- obsolete
+ set(obsolete_keys + rhui_keys)
)
# upgrade + conversion paths
@@ -214,7 +270,7 @@ def test_process(monkeypatch):
):
removeobsoleterpmgpgkeys.process()
removeobsoleterpmgpgkeys.register_dnfworkaround.assert_called_once_with(
- source_distro
+ set(src_distro_keys + rhui_keys)
)
monkeypatch.setattr(
@@ -225,5 +281,57 @@ def test_process(monkeypatch):
):
removeobsoleterpmgpgkeys.process()
removeobsoleterpmgpgkeys.register_dnfworkaround.assert_called_once_with(
- source_distro
+ set(src_distro_keys + rhui_keys)
)
+
+
+@pytest.mark.parametrize(
+ "use_config, configured_keys, expected_in_result",
+ [
+ # RHUI config enabled with keys
+ (True, ["gpg-pubkey-aaaaaaaa-11111111", "gpg-pubkey-bbbbbbbb-22222222"], True),
+ # RHUI config disabled (keys should NOT be included)
+ (False, ["gpg-pubkey-12345678-abcdefgh"], False),
+ # Empty key list
+ (True, [], False),
+ ]
+)
+def test_get_rhui_configured_keys(monkeypatch, use_config, configured_keys, expected_in_result):
+ """Test that RHUI configured keys are returned only when use_config=True"""
+ rhui_config = {}
+ if use_config is not None:
+ rhui_config[RhuiUseConfig.name] = use_config
+ if configured_keys is not None:
+ rhui_config[RhuiObsoleteGpgKeys.name] = configured_keys
+
+ all_config = {'rhui': rhui_config}
+
+ monkeypatch.setattr(
+ api, "current_actor", CurrentActorMocked(config=all_config)
+ )
+ monkeypatch.setattr(
+ removeobsoleterpmgpgkeys, "_is_key_installed", lambda key: key in configured_keys
+ )
+
+ result = removeobsoleterpmgpgkeys._get_rhui_configured_keys()
+
+ if expected_in_result:
+ assert set(result) == set(configured_keys)
+ else:
+ assert result == []
+
+
+def test_get_rhui_configured_keys_raises_on_invalid(monkeypatch):
+ """Test that RHUI configured keys are returned only when use_config=True"""
+ rhui_config = {
+ RhuiUseConfig.name: True,
+ RhuiObsoleteGpgKeys.name: ["gpg-pubkey-10-10"],
+ }
+ all_config = {'rhui': rhui_config}
+
+ monkeypatch.setattr(
+ api, "current_actor", CurrentActorMocked(config=all_config)
+ )
+
+ with pytest.raises(StopActorExecutionError):
+ removeobsoleterpmgpgkeys._get_rhui_configured_keys()
diff --git a/repos/system_upgrade/common/configs/rhui.py b/repos/system_upgrade/common/configs/rhui.py
index ade9bab9..4972effe 100644
--- a/repos/system_upgrade/common/configs/rhui.py
+++ b/repos/system_upgrade/common/configs/rhui.py
@@ -116,6 +116,30 @@ class RhuiTargetRepositoriesToUse(Config):
default = list()
+class RhuiObsoleteGpgKeys(Config):
+ section = RHUI_CONFIG_SECTION
+ name = "obsolete_gpg_keys"
+ type_ = fields.List(fields.String())
+ description = """
+ List of obsolete cloud vendor GPG keys to remove from the RPM database during the upgrade.
+
+ These are GPG keys used by cloud providers to sign RHUI client packages. These are NOT
+ Red Hat distribution keys.
+
+ Each key should be specified in the format of an NVR of the RPM metapackage for the given key after
+ it's imported to rpmdb:
+ gpg-pubkey-<version>-<release>
+ For example: "gpg-pubkey-d4082792-5b32db75"
+
+ These keys will be removed in addition to any keys marked as obsolete in the
+ distribution-specific configuration. Only keys that are actually installed
+ will be removed.
+
+ Note: This setting only takes effect when use_config is set to True.
+ """
+ default = list()
+
+
all_rhui_cfg = (
RhuiTargetPkgs,
RhuiUpgradeFiles,
@@ -123,5 +147,6 @@ all_rhui_cfg = (
RhuiCloudProvider,
RhuiCloudVariant,
RhuiSourcePkgs,
- RhuiUseConfig
+ RhuiUseConfig,
+ RhuiObsoleteGpgKeys
)
--
2.54.0

View File

@ -0,0 +1,274 @@
From 078a619c24ed1250f0d5f13c96412a051884a289 Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Wed, 20 May 2026 15:45:13 +0530
Subject: [PATCH 084/105] Add inhibitor for invalid fstab entries on
pseudo-filesystem mountpoints
Detect /etc/fstab entries that incorrectly define pseudo-filesystem
mountpoints (/proc, /sys, /dev/shm, /run, etc.) with block devices or
wrong filesystem types. Such configurations have been invalid since
RHEL 7 but are typically ignored during normal boot. During the upgrade
the system applies these entries as configured, leading to failures.
Use an allowlist of valid virtual filesystem sources and types so
entries referencing UUID=, LABEL=, PARTUUID=, or /dev/* paths are
correctly detected and inhibited.
JIRA: RHEL-17842
---
.../actors/checkfstabapifsoverride/actor.py | 26 ++++
.../libraries/checkfstabapifsoverride.py | 80 ++++++++++++
.../tests/test_checkfstabapifsoverride.py | 121 ++++++++++++++++++
3 files changed, 227 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/checkfstabapifsoverride/actor.py
create mode 100644 repos/system_upgrade/common/actors/checkfstabapifsoverride/libraries/checkfstabapifsoverride.py
create mode 100644 repos/system_upgrade/common/actors/checkfstabapifsoverride/tests/test_checkfstabapifsoverride.py
diff --git a/repos/system_upgrade/common/actors/checkfstabapifsoverride/actor.py b/repos/system_upgrade/common/actors/checkfstabapifsoverride/actor.py
new file mode 100644
index 00000000..5747f0fb
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkfstabapifsoverride/actor.py
@@ -0,0 +1,26 @@
+from leapp.actors import Actor
+from leapp.libraries.actor.checkfstabapifsoverride import check_fstab_api_fs_override
+from leapp.models import StorageInfo
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckFstabApiFsOverride(Actor):
+ """
+ Inhibit upgrade if /etc/fstab contains invalid entries for pseudo-filesystem mountpoints.
+
+ Paths like /proc, /sys, /dev/shm, /run are managed by the kernel and
+ systemd and have specific requirements for their fstab entries. Defining
+ them with block devices (via /dev/*, UUID=, LABEL=, etc.) or wrong
+ filesystem types has been invalid since RHEL 7, but such configurations
+ are typically ignored during normal boot. During the upgrade, these entries
+ are applied as configured, which may lead to failures. This actor ensures
+ users correct these entries before proceeding.
+ """
+ name = 'check_fstab_api_fs_override'
+ consumes = (StorageInfo,)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ check_fstab_api_fs_override()
diff --git a/repos/system_upgrade/common/actors/checkfstabapifsoverride/libraries/checkfstabapifsoverride.py b/repos/system_upgrade/common/actors/checkfstabapifsoverride/libraries/checkfstabapifsoverride.py
new file mode 100644
index 00000000..8fce90fd
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkfstabapifsoverride/libraries/checkfstabapifsoverride.py
@@ -0,0 +1,80 @@
+from leapp import reporting
+from leapp.libraries.stdlib import api, format_list
+from leapp.models import StorageInfo
+
+API_FS_EXPECTED_TYPES = {
+ '/dev/shm': {'tmpfs'},
+ '/dev/pts': {'devpts'},
+ '/dev/mqueue': {'mqueue'},
+ '/dev/hugepages': {'hugetlbfs'},
+ '/proc': {'proc'},
+ '/sys': {'sysfs'},
+ '/sys/fs/cgroup': {'cgroup', 'cgroup2', 'tmpfs'},
+ '/sys/fs/selinux': {'selinuxfs'},
+ '/sys/kernel/debug': {'debugfs'},
+ '/sys/kernel/security': {'securityfs'},
+ '/run': {'tmpfs'},
+ '/run/lock': {'tmpfs'},
+}
+
+
+def check_fstab_api_fs_override():
+ """
+ Check fstab entries targeting pseudo-filesystem mountpoints for invalid sources or types.
+
+ An entry is flagged if its mount point is a known pseudo-filesystem path and
+ either the filesystem type or the source device does not match the expected
+ virtual filesystem set. The 'none' pseudo-source is also accepted.
+ """
+ storage_info = next(api.consume(StorageInfo), None)
+ if not storage_info:
+ return
+
+ bad_entries = []
+ for entry in storage_info.fstab:
+ mount_point = entry.fs_file
+ if mount_point != '/':
+ mount_point = mount_point.rstrip('/')
+
+ expected = API_FS_EXPECTED_TYPES.get(mount_point)
+ if not expected:
+ continue
+
+ if entry.fs_vfstype not in expected or entry.fs_spec not in (expected | {'none'}):
+ bad_entries.append(entry)
+
+ if not bad_entries:
+ return
+
+ entries_text = format_list(
+ [e.fs_file for e in bad_entries],
+ )
+
+ reporting.create_report([
+ reporting.Title(
+ 'Detected invalid entries in /etc/fstab for pseudo-filesystem mountpoints'
+ ),
+ reporting.Summary(
+ 'Detected /etc/fstab entries that incorrectly define pseudo-filesystem '
+ 'mountpoints. These mountpoints (e.g. /proc, /sys, /dev/shm, /run) are '
+ 'managed by the kernel and systemd, and their fstab entries must use the '
+ 'correct virtual filesystem type and source. Defining them with a block '
+ 'device (via /dev/*, UUID=, LABEL=, etc.) or a wrong filesystem type is '
+ 'invalid. Such configurations were common on RHEL 6 and earlier and are '
+ 'typically ignored during normal boot. However, during the upgrade the '
+ 'system applies fstab entries as configured, which may lead to failures.\n'
+ 'Problematic entries: {}'.format(entries_text)
+ ),
+ reporting.Remediation(
+ hint=(
+ 'Remove or correct the problematic entries in /etc/fstab. If an entry '
+ 'is required (e.g. to set specific mount options for compliance), ensure '
+ 'both the source and the filesystem type match the expected virtual '
+ "filesystem (e.g. 'tmpfs /dev/shm tmpfs defaults 0 0').\n"
+ 'After editing, run "mount -a" to verify.'
+ )
+ ),
+ reporting.RelatedResource('file', '/etc/fstab'),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.FILESYSTEM, reporting.Groups.INHIBITOR]),
+ ])
diff --git a/repos/system_upgrade/common/actors/checkfstabapifsoverride/tests/test_checkfstabapifsoverride.py b/repos/system_upgrade/common/actors/checkfstabapifsoverride/tests/test_checkfstabapifsoverride.py
new file mode 100644
index 00000000..1616021f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkfstabapifsoverride/tests/test_checkfstabapifsoverride.py
@@ -0,0 +1,121 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor.checkfstabapifsoverride import check_fstab_api_fs_override
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import FstabEntry, StorageInfo
+
+
+def _fstab(fs_spec, fs_file, fs_vfstype, fs_mntops='defaults'):
+ return FstabEntry(
+ fs_spec=fs_spec, fs_file=fs_file, fs_vfstype=fs_vfstype,
+ fs_mntops=fs_mntops, fs_freq='0', fs_passno='0',
+ )
+
+
+# --- Entries that SHOULD inhibit (invalid API fs overrides) ---
+_SHM_LVM = _fstab('/dev/mapper/vg00-shm', '/dev/shm', 'xfs')
+_SHM_TMPFS_LABEL = _fstab('LABEL=mydata', '/dev/shm', 'tmpfs')
+_SHM_LVM_TRAILING = _fstab('/dev/mapper/vg00-shm', '/dev/shm/', 'xfs')
+_PROC_EXT4 = _fstab('/dev/sda5', '/proc', 'ext4')
+_SYS_LVM = _fstab('/dev/mapper/vg00-sys', '/sys', 'xfs')
+_SYS_LVM_SYSFS = _fstab('/dev/mapper/vg00-sys', '/sys', 'sysfs')
+_SYS_UUID = _fstab('UUID=023e51f5-b590-4de5-ab21-cf33ecasasas', '/sys', 'xfs')
+_SYS_UUID_SYSFS = _fstab('UUID=023e51f5-b590-4de5-ab21-cf33ecasasas', '/sys', 'sysfs')
+_RUN_PARTUUID = _fstab('PARTUUID=abcd-1234', '/run', 'ext4')
+_RUN_UUID = _fstab('UUID=aabbccdd-1234-5678-9012-aabbccddeeff', '/run', 'ext4')
+_SHM_UUID = _fstab('UUID=12345678-abcd-ef01-2345-6789abcdef01', '/dev/shm', 'xfs')
+_PROC_BIND = _fstab('/some/dir', '/proc', 'none', fs_mntops='bind')
+
+# --- Entries that should NOT inhibit ---
+_SHM_TMPFS = _fstab('tmpfs', '/dev/shm', 'tmpfs')
+_PROC_PROC = _fstab('proc', '/proc', 'proc')
+_SYS_SYSFS = _fstab('sysfs', '/sys', 'sysfs')
+_PROC_NONE = _fstab('none', '/proc', 'proc')
+_RUN_TMPFS = _fstab('tmpfs', '/run', 'tmpfs')
+_HOME_EXT4 = _fstab('/dev/mapper/fedora-home', '/home', 'ext4')
+_ROOT_XFS = _fstab('/dev/mapper/fedora-root', '/', 'xfs')
+_VAR_UUID = _fstab('UUID=aabbccdd-1122-3344-5566-778899aabbcc', '/var', 'xfs')
+_OPT_LABEL = _fstab('LABEL=optdata', '/opt', 'ext4')
+_DATA_PARTUUID = _fstab('PARTUUID=abcd-5678', '/data', 'xfs')
+_BOOT_UUID = _fstab('UUID=01234567-89ab-cdef-0123-456789abcdef', '/boot', 'ext2')
+_MNT_LABEL = _fstab('LABEL=backup', '/mnt/backup', 'ext4')
+_SRV_LVM = _fstab('/dev/mapper/vg00-srv', '/srv', 'xfs')
+_HOME_BIND = _fstab('/exports/home', '/home', 'none', fs_mntops='bind')
+_MNT_BIND = _fstab('/data/shared', '/mnt/shared', 'none', fs_mntops='bind')
+
+
+@pytest.mark.parametrize(
+ ('fstab_entries', 'should_inhibit'),
+ [
+ # Invalid overrides of pseudo-filesystem mountpoints
+ ([_SHM_LVM], True),
+ ([_PROC_EXT4], True),
+ ([_SHM_LVM_TRAILING], True),
+ ([_SYS_LVM], True),
+ ([_SYS_LVM_SYSFS], True),
+ ([_SYS_UUID], True),
+ ([_SYS_UUID_SYSFS], True),
+ ([_SHM_TMPFS_LABEL], True),
+ ([_RUN_PARTUUID], True),
+ ([_RUN_UUID], True),
+ ([_SHM_UUID], True),
+ ([_PROC_BIND], True),
+ # Valid pseudo-filesystem entries
+ ([_SHM_TMPFS, _PROC_PROC, _SYS_SYSFS], False),
+ ([_PROC_NONE], False),
+ ([_RUN_TMPFS], False),
+ # Normal mounts (not pseudo-fs paths) with various source types
+ ([_HOME_EXT4, _ROOT_XFS], False),
+ ([_VAR_UUID], False),
+ ([_OPT_LABEL], False),
+ ([_DATA_PARTUUID], False),
+ ([_BOOT_UUID], False),
+ ([_MNT_LABEL], False),
+ ([_SRV_LVM], False),
+ # Bind mounts on normal paths
+ ([_HOME_BIND], False),
+ ([_MNT_BIND], False),
+ # Mixed valid entries
+ ([_HOME_EXT4, _VAR_UUID, _BOOT_UUID, _SHM_TMPFS], False),
+ # Empty fstab
+ ([], False),
+ ],
+)
+def test_check_fstab_api_fs_override(monkeypatch, fstab_entries, should_inhibit):
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ msgs=[StorageInfo(fstab=fstab_entries)]
+ ))
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ check_fstab_api_fs_override()
+
+ assert bool(created_reports.called) == should_inhibit
+
+
+def test_report_lists_only_problematic_entries(monkeypatch):
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ msgs=[StorageInfo(fstab=[_SHM_LVM, _PROC_EXT4, _HOME_EXT4])]
+ ))
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ check_fstab_api_fs_override()
+
+ assert created_reports.called == 1
+ summary = created_reports.reports[0]['summary']
+ assert '/dev/shm' in summary
+ assert '/proc' in summary
+ assert '/home' not in summary
+
+
+def test_no_storage_info(monkeypatch):
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ check_fstab_api_fs_override()
+
+ assert not created_reports.called
--
2.54.0

View File

@ -0,0 +1,81 @@
From 83a3b58313331ff906e6a5823d23d4ac83779699 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Fri, 29 May 2026 13:32:34 +0200
Subject: [PATCH 085/105] lib/version: Remove deprecated is_rhel_realtime
function
The function is deprecated long enough and also importing version lib in
kernel lib would cause unnecessarty circular imports.
---
.../libraries/config/tests/test_version.py | 13 -----------
.../common/libraries/config/version.py | 23 -------------------
2 files changed, 36 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/config/tests/test_version.py b/repos/system_upgrade/common/libraries/config/tests/test_version.py
index f36dbc5f..c7054893 100644
--- a/repos/system_upgrade/common/libraries/config/tests/test_version.py
+++ b/repos/system_upgrade/common/libraries/config/tests/test_version.py
@@ -132,19 +132,6 @@ def test_is_supported_version(monkeypatch, result, src_ver, is_saphana):
assert version.is_supported_version() == result
-@pytest.mark.parametrize('result,kernel,release_id', [
- (False, '3.10.0-790.el7.x86_64', 'rhel'),
- (False, '3.10.0-790.el7.x86_64', 'fedora'),
- (False, '3.10.0-790.35.2.rt666.1133.el7.x86_64', 'fedora'),
- (True, '3.10.0-790.35.2.rt666.1133.el7.x86_64', 'rhel'),
-])
-@suppress_deprecation(version.is_rhel_realtime)
-def test_is_rhel_realtime(monkeypatch, result, kernel, release_id):
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', kernel=kernel,
- release_id=release_id))
- assert version.is_rhel_realtime() == result
-
-
@pytest.mark.parametrize('result,sys_version', [
('7', '7.1.0'),
('7', '7.9'),
diff --git a/repos/system_upgrade/common/libraries/config/version.py b/repos/system_upgrade/common/libraries/config/version.py
index 5c1e30c6..8eb02ef1 100644
--- a/repos/system_upgrade/common/libraries/config/version.py
+++ b/repos/system_upgrade/common/libraries/config/version.py
@@ -3,7 +3,6 @@ import re
import six
-from leapp.libraries.common import kernel as kernel_lib
from leapp.libraries.stdlib import api
from leapp.utils.deprecation import deprecated
@@ -335,28 +334,6 @@ def is_rhel_alt():
return conf.os_release.release_id == 'rhel' and conf.kernel[0] == '4'
-@deprecated(since='2023-08-15', message='This information is now provided by KernelInfo message.')
-def is_rhel_realtime():
- """
- Check whether the original system is RHEL Real Time.
-
- Currently the check is based on the release of the original booted kernel.
- In case of RHEL, we are sure the release contains the ".rt" string and
- non-realtime kernels don't. Let's use this minimalistic check for now.
- In future, we could detect whether the system is preemptive or not based
- on properties of the kernel (e.g. uname -a tells that information).
-
- :return: `True` if the orig system is RHEL RT and `False` otherwise.
- :rtype: bool
- """
- conf = api.current_actor().configuration
- if conf.os_release.release_id != 'rhel':
- return False
-
- kernel_type = kernel_lib.determine_kernel_type_from_uname(get_source_version(), conf.kernel)
- return kernel_type == kernel_lib.KernelType.REALTIME
-
-
def is_supported_version():
"""
Verify if the current system version is supported for the upgrade.
--
2.54.0

View File

@ -0,0 +1,142 @@
From 68e286f5d36fc76ce06e8feff2fe418c96f3a9f5 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Fri, 29 May 2026 13:33:23 +0200
Subject: [PATCH 086/105] lib/kernel: Fix rt kernel detection on centos
The kernel detection function determine_kernel_type_from_uname() didn't
take major-only versions as used for CentOS Stream into account and
therefore it misidentified rt kernels as "ordinary" on CentOS Stream
systems.
The function is modified to use matches_version() from the version lib
which already handles the versions comparison for CentOS Stream as.
Jira: RHEL-161560
---
.../common/libraries/config/version.py | 13 ++++++
.../system_upgrade/common/libraries/kernel.py | 10 ++---
.../common/libraries/tests/test_kernel_lib.py | 43 ++++++++++++++-----
3 files changed, 49 insertions(+), 17 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/config/version.py b/repos/system_upgrade/common/libraries/config/version.py
index 8eb02ef1..faba3e68 100644
--- a/repos/system_upgrade/common/libraries/config/version.py
+++ b/repos/system_upgrade/common/libraries/config/version.py
@@ -171,6 +171,19 @@ def matches_version(match_list, detected):
"""
Check if the `detected` version meets the criteria specified in `match_list`.
+ For CentOS Stream (which natively uses only major versions), the match list
+ and the detected version are automatically adjusted to ensure intuitive evaluation:
+
+ - If `match_list` contains only major versions, the comparison is restricted
+ to the major version component:
+ version.matches_version(['> 8', '<= 9'], '9.5') # True
+
+ - If `match_list` contains major.minor versions but `detected` is a major-only
+ version, the function resolves the input to its defined "virtual version"
+ before evaluation:
+ # e.g., if the virtual version for '9' is defined as '9.5'
+ version.matches_version(['> 8.10', '<= 9.7'], '9') # True
+
:param match_list: specification of versions to check against
:type match_list: list or tuple of strings in one of the two following forms:
``['>'|'>='|'<'|'<='] <integer>.<integer>`` form, where elements are ANDed,
diff --git a/repos/system_upgrade/common/libraries/kernel.py b/repos/system_upgrade/common/libraries/kernel.py
index fc8c1167..0a160e7f 100644
--- a/repos/system_upgrade/common/libraries/kernel.py
+++ b/repos/system_upgrade/common/libraries/kernel.py
@@ -1,6 +1,7 @@
from collections import namedtuple
from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.common.config.version import matches_version
from leapp.libraries.stdlib import api, CalledProcessError, run
KernelPkgInfo = namedtuple('KernelPkgInfo', ('name', 'version', 'release', 'arch', 'nevra'))
@@ -14,6 +15,8 @@ class KernelType:
REALTIME = 'realtime'
+# TODO: rename rhel_version to something distro agnostic in the new function
+# when this is deprecated
def determine_kernel_type_from_uname(rhel_version, kernel_uname_r):
"""
Determine kernel type from given kernel release (uname-r).
@@ -23,12 +26,7 @@ def determine_kernel_type_from_uname(rhel_version, kernel_uname_r):
:returns: Kernel type based on a given uname_r
:rtype: KernelType
"""
- version_fragments = rhel_version.split('.')
- major_ver = version_fragments[0]
- minor_ver = version_fragments[1] if len(version_fragments) > 1 else '0'
- rhel_version = (major_ver, minor_ver)
-
- if rhel_version <= ('9', '2'):
+ if matches_version(['<= 9.2'], rhel_version):
uname_r_infixes = {
'.rt': KernelType.REALTIME
}
diff --git a/repos/system_upgrade/common/libraries/tests/test_kernel_lib.py b/repos/system_upgrade/common/libraries/tests/test_kernel_lib.py
index a6696a3c..1687d0dc 100644
--- a/repos/system_upgrade/common/libraries/tests/test_kernel_lib.py
+++ b/repos/system_upgrade/common/libraries/tests/test_kernel_lib.py
@@ -5,23 +5,44 @@ import pytest
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import kernel as kernel_lib
from leapp.libraries.common.kernel import KernelType
+from leapp.libraries.common.testutils import CurrentActorMocked
+from leapp.libraries.stdlib import api
@pytest.mark.parametrize(
- ('rhel_version', 'uname_r', 'expected_kernel_type'),
+ ('version', 'uname_r', 'expected_kernel_type'),
(
- ('7.9', '3.10.0-1160.el7.x86_64', KernelType.ORDINARY),
- ('7.9', '3.10.0-1160.rt56.1131.el7.x86_64', KernelType.REALTIME),
- ('8.7', '4.18.0-425.3.1.el8.x86_64', KernelType.ORDINARY),
- ('8.7', '4.18.0-425.3.1.rt7.213.el8.x86_64', KernelType.REALTIME),
- ('9.2', '5.14.0-284.11.1.el9_2.x86_64', KernelType.ORDINARY),
- ('9.2', '5.14.0-284.11.1.rt14.296.el9_2.x86_64', KernelType.REALTIME),
- ('9.3', '5.14.0-354.el9.x86_64', KernelType.ORDINARY),
- ('9.3', '5.14.0-354.el9.x86_64+rt', KernelType.REALTIME),
+ # (version, virtual_version)
+ (('7.9', None), '3.10.0-1160.el7.x86_64', KernelType.ORDINARY),
+ (('7.9', None), '3.10.0-1160.rt56.1131.el7.x86_64', KernelType.REALTIME),
+ (('8.7', None), '4.18.0-425.3.1.el8.x86_64', KernelType.ORDINARY),
+ (('8.7', None), '4.18.0-425.3.1.rt7.213.el8.x86_64', KernelType.REALTIME),
+ (('9.2', None), '5.14.0-284.11.1.el9_2.x86_64', KernelType.ORDINARY),
+ (('9.2', None), '5.14.0-284.11.1.rt14.296.el9_2.x86_64', KernelType.REALTIME),
+ (('9.3', None), '5.14.0-354.el9.x86_64', KernelType.ORDINARY),
+ (('9.3', None), '5.14.0-354.el9.x86_64+rt', KernelType.REALTIME),
+ # centos
+ (('8', '8.7'), '4.18.0-425.3.1.el8.x86_64', KernelType.ORDINARY),
+ (('8', '8.7'), '4.18.0-425.3.1.rt7.213.el8.x86_64', KernelType.REALTIME),
+ (('9', '9.2'), '5.14.0-284.11.1.el9_2.x86_64', KernelType.ORDINARY),
+ (('9', '9.2'), '5.14.0-284.11.1.rt14.296.el9_2.x86_64', KernelType.REALTIME),
+ (('9', '9.3'), '5.14.0-354.el9.x86_64', KernelType.ORDINARY),
+ (('9', '9.3'), '5.14.0-354.el9.x86_64+rt', KernelType.REALTIME),
)
)
-def test_determine_kernel_type_from_uname(rhel_version, uname_r, expected_kernel_type):
- kernel_type = kernel_lib.determine_kernel_type_from_uname(rhel_version, uname_r)
+def test_determine_kernel_type_from_uname(monkeypatch, version, uname_r, expected_kernel_type):
+ real_ver, virtual_ver = version
+ # needed to for lookups of virtual versions in matches_version
+ actor_mock = CurrentActorMocked(
+ release_id='centos' if '.' not in real_ver else 'rhel',
+ src_ver=real_ver,
+ dst_ver="irrelevant",
+ virtual_source_version=virtual_ver or real_ver,
+ virtual_target_version="irrelevant",
+ )
+ monkeypatch.setattr(api, 'current_actor', actor_mock)
+
+ kernel_type = kernel_lib.determine_kernel_type_from_uname(real_ver, uname_r)
assert kernel_type == expected_kernel_type
--
2.54.0

View File

@ -0,0 +1,78 @@
From 1093f58a66a6221519714332d09126c76c5712c1 Mon Sep 17 00:00:00 2001
From: karolinku <kkula@redhat.com>
Date: Thu, 28 May 2026 17:11:17 +0200
Subject: [PATCH 087/105] Add CLAUDE.md and YAML frontmatter to skills for
Claude Code compatibility
CLAUDE.md imports AGENTS.md so Claude Code uses the same instructions
as other agents without duplicating content. YAML frontmatter (name,
description) is added to each SKILL.md to satisfy Claude Code's skill
metadata requirements. Add symlink ../skills -> .claude/skills to
enable Claude's auto-discovery of skills.
---
.claude/skills | 1 +
CLAUDE.md | 1 +
skills/leapp-actor-dev/SKILL.md | 5 +++++
skills/leapp-test-runner/SKILL.md | 5 +++++
skills/leapp-unit-tests/SKILL.md | 5 +++++
5 files changed, 17 insertions(+)
create mode 120000 .claude/skills
create mode 100644 CLAUDE.md
diff --git a/.claude/skills b/.claude/skills
new file mode 120000
index 00000000..42c5394a
--- /dev/null
+++ b/.claude/skills
@@ -0,0 +1 @@
+../skills
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..141137e7
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@import AGENTS.md
diff --git a/skills/leapp-actor-dev/SKILL.md b/skills/leapp-actor-dev/SKILL.md
index 124f3e99..5bfdc97a 100644
--- a/skills/leapp-actor-dev/SKILL.md
+++ b/skills/leapp-actor-dev/SKILL.md
@@ -1,3 +1,8 @@
+---
+name: leapp-actor-dev
+description: Develop or modify leapp-repository actors. Covers actor structure, phase selection, placement rules, and key constraints.
+---
+
# Developing Leapp Actors
## Actor structure
diff --git a/skills/leapp-test-runner/SKILL.md b/skills/leapp-test-runner/SKILL.md
index 579aeb5d..18640e3f 100644
--- a/skills/leapp-test-runner/SKILL.md
+++ b/skills/leapp-test-runner/SKILL.md
@@ -1,3 +1,8 @@
+---
+name: leapp-test-runner
+description: Run tests and linters for leapp-repository. Covers container-based CI testing, single-actor fast tests, and lint commands.
+---
+
# Running Tests and Linters
All non-container commands (`make lint`, `make test_no_lint`, `make fast_lint`, `make dev_test_no_lint`) require a local virtualenv and `REPOSITORIES` envar (e.g. `REPOSITORIES=common,el9toel10`). Create the virtualenv with `make install-deps` (RHEL/CentOS) or `make install-deps-fedora` (Fedora) — the Makefile handles activation internally, don't source the venv manually. Set `PYTHON_VENV=pythonX.X` to choose the Python version (default: `python3.6`). Container commands are self-contained and recommended as the default.
diff --git a/skills/leapp-unit-tests/SKILL.md b/skills/leapp-unit-tests/SKILL.md
index 894d4271..e5189f31 100644
--- a/skills/leapp-unit-tests/SKILL.md
+++ b/skills/leapp-unit-tests/SKILL.md
@@ -1,3 +1,8 @@
+---
+name: leapp-unit-tests
+description: Write or fix unit tests for leapp-repository actors and shared libraries. Covers test placement, mocking patterns, and project conventions.
+---
+
# Writing Unit Tests
--
2.54.0

View File

@ -0,0 +1,249 @@
From 342e14b4e05524fd0925aeba1f3294b3cbd8efcf Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Mon, 1 Jun 2026 16:38:46 +0530
Subject: [PATCH 088/105] Add inhibitor for incorrect /var/run symlink
configuration
In modern Linux systems that use systemd, /run is a tmpfs filesystem
managed by systemd and /var/run must be a symbolic link pointing to
../run for compatibility. Inhibit the upgrade if /var/run is not
configured as expected, as this can cause boot failures, service
startup failures, or login issues.
Extended the FileInfo model with a real_path field and add /var/run to
the scansourcefiles tracked files list. The ChecksPhase actor consumes
the TrackedFilesInfoSource message to detect this unsupported
configuration and inhibits the upgrade.
JIRA: RHEL-76846
---
.../common/actors/checkvarrunsymlink/actor.py | 25 ++++++++++
.../libraries/checkvarrunsymlink.py | 49 +++++++++++++++++++
.../tests/test_checkvarrunsymlink.py | 41 ++++++++++++++++
.../libraries/scansourcefiles.py | 5 +-
.../tests/unit_test_scansourcefiles.py | 23 +++++----
.../common/models/trackedfiles.py | 5 ++
6 files changed, 138 insertions(+), 10 deletions(-)
create mode 100644 repos/system_upgrade/common/actors/checkvarrunsymlink/actor.py
create mode 100644 repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
create mode 100644 repos/system_upgrade/common/actors/checkvarrunsymlink/tests/test_checkvarrunsymlink.py
diff --git a/repos/system_upgrade/common/actors/checkvarrunsymlink/actor.py b/repos/system_upgrade/common/actors/checkvarrunsymlink/actor.py
new file mode 100644
index 00000000..512ed1a8
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkvarrunsymlink/actor.py
@@ -0,0 +1,25 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import checkvarrunsymlink
+from leapp.models import TrackedFilesInfoSource
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckVarRunSymlink(Actor):
+ """
+ Check that /var/run is a symlink pointing to /run.
+
+ In modern Linux systems that use systemd, /run is a tmpfs filesystem
+ managed by systemd and /var/run must be a symbolic link pointing to
+ ../run for compatibility. Inhibit the upgrade if /var/run is not
+ configured as expected, as this can cause boot failures, service
+ startup failures, or login issues.
+ """
+
+ name = 'check_var_run_symlink'
+ consumes = (TrackedFilesInfoSource,)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ checkvarrunsymlink.process()
diff --git a/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
new file mode 100644
index 00000000..e2b198c1
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
@@ -0,0 +1,49 @@
+from leapp import reporting
+from leapp.libraries.stdlib import api
+from leapp.models import TrackedFilesInfoSource
+
+VAR_RUN_PATH = '/var/run'
+
+
+def _get_real_path(tracked_files):
+ for finfo in tracked_files.files:
+ if finfo.path == VAR_RUN_PATH:
+ return finfo.real_path
+ return None
+
+
+def process():
+ tracked_files = next(api.consume(TrackedFilesInfoSource), None)
+ if not tracked_files:
+ api.current_logger().warning(
+ 'The TrackedFilesInfoSource message is missing. Skipping the check of /var/run symlink.'
+ )
+ return
+
+ real_path = _get_real_path(tracked_files)
+ if real_path is None or real_path == '/run':
+ return
+
+ reporting.create_report([
+ reporting.Title('/var/run is not correctly configured'),
+ reporting.Summary(
+ 'In modern Linux systems that use systemd, /run is a tmpfs filesystem'
+ ' managed by systemd and /var/run must be a symbolic link pointing to'
+ ' ../run for compatibility. The current system does not have /var/run'
+ ' configured as expected. This can cause boot failures, service startup'
+ ' failures, or login issues both on the current system and after an'
+ ' in-place upgrade.'
+ ),
+ reporting.Remediation(
+ hint=(
+ 'Back up current /var/run directory if needed and replace it by'
+ ' symbolic link pointing to "../run".'
+ ),
+ commands=[
+ ['bash', '-c', 'mv /var/run /tmp/var_run.bak && ln -s ../run /var/run'],
+ ]
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.FILESYSTEM, reporting.Groups.INHIBITOR]),
+ reporting.RelatedResource('directory', VAR_RUN_PATH),
+ ])
diff --git a/repos/system_upgrade/common/actors/checkvarrunsymlink/tests/test_checkvarrunsymlink.py b/repos/system_upgrade/common/actors/checkvarrunsymlink/tests/test_checkvarrunsymlink.py
new file mode 100644
index 00000000..2df218e8
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkvarrunsymlink/tests/test_checkvarrunsymlink.py
@@ -0,0 +1,41 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor import checkvarrunsymlink
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import FileInfo, TrackedFilesInfoSource
+from leapp.utils.report import is_inhibitor
+
+
+def _msg_varrun(real_path):
+ return TrackedFilesInfoSource(files=[
+ FileInfo(path='/var/run', exists=True, is_modified=False, real_path=real_path)
+ ])
+
+
+@pytest.mark.parametrize('real_path, should_inhibit', [
+ ('/run', False),
+ ('/var/run', True),
+ ('/tmp/foo', True),
+])
+def test_check_var_run_symlink(monkeypatch, real_path, should_inhibit):
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ msgs=[_msg_varrun(real_path)]
+ ))
+
+ checkvarrunsymlink.process()
+
+ assert bool(reporting.create_report.called) == should_inhibit
+ if should_inhibit:
+ assert is_inhibitor(reporting.create_report.report_fields)
+
+
+def test_check_var_run_missing_message(monkeypatch):
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+
+ checkvarrunsymlink.process()
+
+ assert not reporting.create_report.called
diff --git a/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py b/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py
index 16c0e8aa..7c3ddbe3 100644
--- a/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py
+++ b/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py
@@ -10,6 +10,7 @@ from leapp.models import FileInfo, TrackedFilesInfoSource
TRACKED_FILES = {
'common': [
'/etc/pki/tls/openssl.cnf',
+ '/var/run',
],
'8': [
],
@@ -56,10 +57,12 @@ def is_modified(input_file):
def scan_file(input_file):
+ exists = os.path.exists(input_file)
data = {
'path': input_file,
- 'exists': os.path.exists(input_file),
+ 'exists': exists,
'rpm_name': _get_rpm_name(input_file),
+ 'real_path': os.path.realpath(input_file) if exists else '',
}
if data['rpm_name']:
diff --git a/repos/system_upgrade/common/actors/scansourcefiles/tests/unit_test_scansourcefiles.py b/repos/system_upgrade/common/actors/scansourcefiles/tests/unit_test_scansourcefiles.py
index 40ae2408..ed262126 100644
--- a/repos/system_upgrade/common/actors/scansourcefiles/tests/unit_test_scansourcefiles.py
+++ b/repos/system_upgrade/common/actors/scansourcefiles/tests/unit_test_scansourcefiles.py
@@ -66,22 +66,27 @@ def test_get_rpm_name_error(monkeypatch):
@pytest.mark.parametrize(
- ('input_file', 'exists', 'rpm_name', 'is_modified'),
+ ('input_file', 'exists', 'rpm_name', 'is_modified', 'real_path'),
(
- ('/not_existing_file', False, '', False),
- ('/not_existing_file_rpm_owned', False, 'rpm', False),
- ('/not_existing_file_rpm_owned_modified', False, 'rpm', True),
- ('/existing_file_not_modified', True, '', False),
- ('/existing_file_owned_by_rpm_not_modified', True, 'rpm', False),
- ('/existing_file_owned_by_rpm_modified', True, 'rpm', True),
+ ('/not_existing_file', False, '', False, ''),
+ ('/not_existing_file_rpm_owned', False, 'rpm', False, ''),
+ ('/not_existing_file_rpm_owned_modified', False, 'rpm', True, ''),
+ ('/existing_file_not_modified', True, '', False, '/existing_file_not_modified'),
+ ('/existing_file_owned_by_rpm_not_modified', True, 'rpm', False, '/existing_file_owned_by_rpm_not_modified'),
+ ('/existing_file_owned_by_rpm_modified', True, 'rpm', True, '/existing_file_owned_by_rpm_modified'),
+ ('/existing_symlink', True, '', False, '/target'),
)
)
-def test_scan_file(monkeypatch, input_file, exists, rpm_name, is_modified):
+def test_scan_file(monkeypatch, input_file, exists, rpm_name, is_modified, real_path):
monkeypatch.setattr(scansourcefiles, 'is_modified', lambda _: is_modified)
monkeypatch.setattr(scansourcefiles, '_get_rpm_name', lambda _: rpm_name)
monkeypatch.setattr(os.path, 'exists', lambda _: exists)
+ monkeypatch.setattr(os.path, 'realpath', lambda _: real_path)
- expected_model_output = FileInfo(path=input_file, exists=exists, rpm_name=rpm_name, is_modified=is_modified)
+ expected_model_output = FileInfo(
+ path=input_file, exists=exists, rpm_name=rpm_name,
+ is_modified=is_modified, real_path=real_path
+ )
assert scansourcefiles.scan_file(input_file) == expected_model_output
diff --git a/repos/system_upgrade/common/models/trackedfiles.py b/repos/system_upgrade/common/models/trackedfiles.py
index f7c2c809..a3f6e9f5 100644
--- a/repos/system_upgrade/common/models/trackedfiles.py
+++ b/repos/system_upgrade/common/models/trackedfiles.py
@@ -40,6 +40,11 @@ class FileInfo(Model):
In such a case the value is always false.
"""
+ real_path = fields.String(default="")
+ """
+ The resolved (real) path of the file. Empty string if path does not exist.
+ """
+
class TrackedFilesInfoSource(Model):
"""
--
2.54.0

View File

@ -0,0 +1,34 @@
From c5ef9301838854790d01c2cca086e80f8a781018 Mon Sep 17 00:00:00 2001
From: Pradeep Jagtap <prjagtap@redhat.com>
Date: Wed, 10 Jun 2026 11:42:53 +0530
Subject: [PATCH 089/105] checkvarrunsymlink: Log warning when /var/run is
missing from tracked files
Address review feedback from #1546: emit a warning when /var/run
is unexpectedly absent from TrackedFilesInfoSource instead of
silently skipping the check.
---
.../checkvarrunsymlink/libraries/checkvarrunsymlink.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
index e2b198c1..4e6f030e 100644
--- a/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
+++ b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
@@ -21,7 +21,12 @@ def process():
return
real_path = _get_real_path(tracked_files)
- if real_path is None or real_path == '/run':
+ if real_path is None:
+ api.current_logger().warning(
+ '/var/run not found in TrackedFilesInfoSource. Skipping the symlink check.'
+ )
+ return
+ if real_path == '/run':
return
reporting.create_report([
--
2.54.0

View File

@ -0,0 +1,419 @@
From a9e5a3e2343654504accb11f5d1305cacce22a90 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 9 Jun 2026 10:46:56 +0000
Subject: [PATCH 090/105] Add scandnfcomps actor to scan installed DNF comps
The actor scan installed DNF environments and groups (DNF comps)
which are represented by new InstalledDNFComps model.
The scan is performed using initialized dnf.Base to load all
available groups and envs and then it checks what is installed based
on the history.
About each group and environment, only ID and and (pretty) name
are collected. Skipping the scan of other data as we do not have a
use for that at this moment.
Unit-tests and part of docstrings are assisted by AI. All the other
code had to be manually updated unfortunately.
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../common/actors/scandnfcomps/actor.py | 22 +++
.../scandnfcomps/libraries/scandnfcomps.py | 87 +++++++++
.../scandnfcomps/tests/test_scandnfcomps.py | 175 ++++++++++++++++++
.../system_upgrade/common/models/dnfcomps.py | 76 ++++++++
4 files changed, 360 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/scandnfcomps/actor.py
create mode 100644 repos/system_upgrade/common/actors/scandnfcomps/libraries/scandnfcomps.py
create mode 100644 repos/system_upgrade/common/actors/scandnfcomps/tests/test_scandnfcomps.py
create mode 100644 repos/system_upgrade/common/models/dnfcomps.py
diff --git a/repos/system_upgrade/common/actors/scandnfcomps/actor.py b/repos/system_upgrade/common/actors/scandnfcomps/actor.py
new file mode 100644
index 00000000..ddebfce0
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfcomps/actor.py
@@ -0,0 +1,22 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import scandnfcomps
+from leapp.models import InstalledDNFComps
+from leapp.tags import FactsPhaseTag, IPUWorkflowTag
+
+
+class ScanDNFComps(Actor):
+ """
+ Scan installed DNF comps (environments and groups).
+
+ Collects information about DNF package environments and groups that are
+ currently installed on the source system and produce InstalledDNFComps
+ message.
+ """
+
+ name = 'scan_dnf_comps'
+ consumes = ()
+ produces = (InstalledDNFComps,)
+ tags = (FactsPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ scandnfcomps.process()
diff --git a/repos/system_upgrade/common/actors/scandnfcomps/libraries/scandnfcomps.py b/repos/system_upgrade/common/actors/scandnfcomps/libraries/scandnfcomps.py
new file mode 100644
index 00000000..31b07bc0
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfcomps/libraries/scandnfcomps.py
@@ -0,0 +1,87 @@
+import warnings
+
+from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.common.dnflibs import create_dnf_base, DNFRepoError
+from leapp.libraries.stdlib import api
+from leapp.models import DNFEnvironment, DNFGroup, InstalledDNFComps
+
+try:
+ import dnf
+except ImportError:
+ # NOTE(pstodulk): To stay consistent with other DNF related actors, but
+ # I guess we can get rid of it nowadays.
+ dnf = None
+ warnings.warn('Could not import the `dnf` python module.', ImportWarning)
+
+
+def _get_installed_groups(base):
+ """
+ Query installed DNF comps groups via the DNF base object.
+
+ Iterates over all available comps groups and filters to those recorded
+ as installed in the DNF history.
+
+ :param base: Initialized dnf.Base object with filled sack
+ :type base: dnf.Base
+ :returns: List of DNFGroup model instances sorted by group id
+ :rtype: List[DNFGroup]
+ """
+ groups = []
+ for grp in base.comps.groups_iter():
+ if not base.history.group.get(grp.id):
+ # not installed
+ continue
+ groups.append(DNFGroup(
+ id=grp.id,
+ name=grp.ui_name,
+ ))
+ return sorted(groups, key=lambda x: x.id)
+
+
+def _get_installed_environments(base):
+ """
+ Query installed DNF comps environments via the DNF base object.
+
+ Iterates over all available comps environments and filters to those
+ recorded as installed in the DNF history.
+
+ :param base: Initialized dnf.Base object with filled sack
+ :type base: dnf.Base
+ :returns: List of DNFEnvironment model instances sorted by environment id
+ :rtype: List[DNFEnvironment]
+ """
+ environments = []
+ for env in base.comps.environments_iter():
+ if not base.history.env.get(env.id):
+ continue
+ environments.append(DNFEnvironment(
+ id=env.id,
+ name=env.ui_name,
+ ))
+ return sorted(environments, key=lambda x: x.id)
+
+
+def process():
+ """
+ Scan installed DNF comps and produce InstalledDNFComps message.
+
+ .. seealso::
+ :func:`create_dnf_base` for exceptions raised when creating dnf.Base
+ """
+ if not dnf:
+ api.current_logger().debug('DNF is not available, skipping DNF comps scan.')
+ return
+
+ try:
+ base = create_dnf_base()
+ except DNFRepoError as e:
+ e.details['details'] = e.message
+ raise StopActorExecutionError(
+ message='Cannot obtain information about DNF Groups and Environments',
+ details=e.details
+ )
+
+ api.produce(InstalledDNFComps(
+ environments=_get_installed_environments(base),
+ groups=_get_installed_groups(base),
+ ))
diff --git a/repos/system_upgrade/common/actors/scandnfcomps/tests/test_scandnfcomps.py b/repos/system_upgrade/common/actors/scandnfcomps/tests/test_scandnfcomps.py
new file mode 100644
index 00000000..9641c918
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfcomps/tests/test_scandnfcomps.py
@@ -0,0 +1,175 @@
+import pytest
+
+from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.actor import scandnfcomps
+from leapp.libraries.common.dnflibs import DNFRepoError
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import DNFEnvironment, DNFGroup, InstalledDNFComps
+
+
+class MockCompsGroup():
+ def __init__(self, gid, ui_name):
+ self.id = gid
+ self.ui_name = ui_name
+
+
+class MockCompsEnvironment():
+ def __init__(self, env_id, ui_name):
+ self.id = env_id
+ self.ui_name = ui_name
+
+
+class MockComps():
+ def __init__(self, groups=None, environments=None):
+ self._groups = groups or []
+ self._environments = environments or []
+
+ def groups_iter(self):
+ return iter(self._groups)
+
+ def environments_iter(self):
+ return iter(self._environments)
+
+
+class MockHistory():
+
+ class _Lookup():
+ """
+ Simulates base.history.group or base.history.env
+
+ group & env have have same "API" that we use, so no need to create
+ a different class for each one.
+ """
+
+ def __init__(self, installed_ids):
+ self._installed = set(installed_ids)
+
+ def get(self, item_id):
+ return item_id in self._installed
+
+ def __init__(self, installed_group_ids=None, installed_env_ids=None):
+ self.group = MockHistory._Lookup(installed_group_ids or [])
+ self.env = MockHistory._Lookup(installed_env_ids or [])
+
+
+class MockDNFBase():
+ def __init__(self, comps=None, history=None):
+ self.comps = comps or MockComps()
+ self.history = history or MockHistory()
+
+
+def test_process_with_groups_and_environments(monkeypatch):
+ comps = MockComps(
+ groups=[
+ MockCompsGroup('core', 'Core'),
+ MockCompsGroup('base', 'Base'),
+ ],
+ environments=[
+ MockCompsEnvironment('minimal-environment', 'Minimal Install'),
+ ],
+ )
+ history = MockHistory(
+ installed_group_ids=['core', 'base'],
+ installed_env_ids=['minimal-environment'],
+ )
+ base = MockDNFBase(comps=comps, history=history)
+
+ mocked_producer = produce_mocked()
+ monkeypatch.setattr(scandnfcomps, 'create_dnf_base', lambda: base)
+ monkeypatch.setattr(scandnfcomps, 'dnf', True)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+
+ scandnfcomps.process()
+
+ assert mocked_producer.called == 1
+ msg = mocked_producer.model_instances[0]
+ assert isinstance(msg, InstalledDNFComps)
+ assert len(msg.groups) == 2
+ assert msg.groups[0].id == 'base'
+ assert msg.groups[0].name == 'Base'
+ assert msg.groups[1].id == 'core'
+ assert msg.groups[1].name == 'Core'
+ assert len(msg.environments) == 1
+ assert msg.environments[0].id == 'minimal-environment'
+ assert msg.environments[0].name == 'Minimal Install'
+
+
+def test_process_filters_not_installed(monkeypatch):
+ """Only groups/environments recorded as installed in history are returned."""
+ comps = MockComps(
+ groups=[
+ MockCompsGroup('core', 'Core'),
+ MockCompsGroup('base', 'Base'),
+ MockCompsGroup('extra', 'Extra'),
+ ],
+ environments=[
+ MockCompsEnvironment('minimal-environment', 'Minimal Install'),
+ MockCompsEnvironment('server-product-environment', 'Server'),
+ ],
+ )
+ history = MockHistory(
+ installed_group_ids=['core'],
+ installed_env_ids=['server-product-environment'],
+ )
+ base = MockDNFBase(comps=comps, history=history)
+
+ mocked_producer = produce_mocked()
+ monkeypatch.setattr(scandnfcomps, 'create_dnf_base', lambda: base)
+ monkeypatch.setattr(scandnfcomps, 'dnf', True)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+
+ scandnfcomps.process()
+
+ assert mocked_producer.called == 1
+ msg = mocked_producer.model_instances[0]
+ assert len(msg.groups) == 1
+ assert msg.groups[0].id == 'core'
+ assert len(msg.environments) == 1
+ assert msg.environments[0].id == 'server-product-environment'
+
+
+def test_process_empty_comps(monkeypatch):
+ base = MockDNFBase()
+
+ mocked_producer = produce_mocked()
+ monkeypatch.setattr(scandnfcomps, 'create_dnf_base', lambda: base)
+ monkeypatch.setattr(scandnfcomps, 'dnf', True)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+
+ scandnfcomps.process()
+
+ assert mocked_producer.called == 1
+ msg = mocked_producer.model_instances[0]
+ assert isinstance(msg, InstalledDNFComps)
+ assert msg.groups == []
+ assert msg.environments == []
+
+
+def test_process_no_dnf(monkeypatch):
+ mocked_producer = produce_mocked()
+ monkeypatch.setattr(scandnfcomps, 'dnf', None)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+
+ scandnfcomps.process()
+ assert mocked_producer.called == 0
+
+
+def test_process_dnf_repo_error(monkeypatch):
+ def raise_repo_error():
+ raise DNFRepoError(
+ message='DNF failed to load repositories: repo error',
+ details={'hint': 'Ensure the myrepo repository definition is correct.'}
+ )
+
+ monkeypatch.setattr(scandnfcomps, 'create_dnf_base', raise_repo_error)
+ monkeypatch.setattr(scandnfcomps, 'dnf', True)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ with pytest.raises(StopActorExecutionError, match='Cannot obtain information about DNF Groups and Environments'):
+ scandnfcomps.process()
diff --git a/repos/system_upgrade/common/models/dnfcomps.py b/repos/system_upgrade/common/models/dnfcomps.py
new file mode 100644
index 00000000..896f832d
--- /dev/null
+++ b/repos/system_upgrade/common/models/dnfcomps.py
@@ -0,0 +1,76 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemFactsTopic
+
+
+class DNFGroup(Model):
+ """
+ Provide information about a single installed DNF comps group.
+
+ This model is not expected to be produced or consumed by any actors as
+ a standalone message. It is always part of InstalledDNFComps message.
+ """
+
+ topic = SystemFactsTopic
+
+ id = fields.String()
+ """
+ The DNF comps group identifier (e.g. "core", "base").
+ """
+
+ name = fields.String()
+ """
+ Human-readable display name of the group.
+ """
+
+ # NOTE(pstodulk): possible extension if needed in future, but not implemented now:
+ # (( to list installed packages, split by various groups ))
+ # mandatory_packages = fields.List(fields.String, default=[])
+ # default_packages = fields.List(fields.String, default=[])
+ # conditional_packages = fields.List(fields.String, default=[])
+ # optional_packages = fields.List(fields.String, default=[])
+
+
+class DNFEnvironment(Model):
+ """
+ Provide information about a single installed DNF comps environment.
+
+ This model is not expected to be produced or consumed by any actors as
+ a standalone message. It is always part of InstalledDNFComps message.
+ """
+
+ topic = SystemFactsTopic
+
+ id = fields.String()
+ """
+ The DNF comps environment identifier (e.g. "minimal-environment").
+ """
+
+ name = fields.String()
+ """
+ Human-readable display name of the environment.
+ """
+
+ # NOTE(pstodulk): possible extension if needed in future, but not implemented now:
+ # mandatory_groups = fields.List(fields.String(), default=[])
+ # optional_groups = fields.List(fields.String(), default=[])
+
+
+class InstalledDNFComps(Model):
+ """
+ Provide information about installed DNF comps on the source system.
+
+ Contains lists of installed DNF package environments and groups as
+ tracked by the DNF history database.
+ """
+
+ topic = SystemFactsTopic
+
+ environments = fields.List(fields.Model(DNFEnvironment), default=[])
+ """
+ List of installed DNF comps environments.
+ """
+
+ groups = fields.List(fields.Model(DNFGroup), default=[])
+ """
+ List of installed DNF comps groups.
+ """
--
2.54.0

View File

@ -0,0 +1,165 @@
From 63ed8d0297197fab585f5c268638b20c07ea428e Mon Sep 17 00:00:00 2001
From: Tomas Popela <tpopela@redhat.com>
Date: Mon, 9 Mar 2026 10:58:44 +0100
Subject: [PATCH 091/105] IPU 9 -> 10: Fix the upgrade of Graphical Server
Prior to RHEL 10 we didn't have a way to enforce some of the desktop
environment changes to be only Server specific (such as disabling
the power button handling or disabling automatic suspendand) hence
we had to enable everything for everyone.
In RHEL 10 we've added a new subpackage of gnome-settings-daemon, the
gnome-settings-daemon-server-defaults which is only preinstalled on
"Server with GUI" product and on nothing else. But this only works for
clean installations as the comps groups are not synced when doing the
upgrades with Leapp. To fix that we've created this Leapp actor which
will detect if the graphical-server-environment comps group was
installed and only in the case it will preinstall the server-defaults
subpackage.
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-by: Petr Stodulka <pstodulk@redhat.com>
Jira: RHEL-148697
---
.../actors/gnomesettingsdaemoncheck/actor.py | 22 ++++++++
.../libraries/gnomesettingsdaemoncheck.py | 36 +++++++++++++
.../tests/test_gnomesettingsdaemoncheck.py | 54 +++++++++++++++++++
3 files changed, 112 insertions(+)
create mode 100644 repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/actor.py
create mode 100644 repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/libraries/gnomesettingsdaemoncheck.py
create mode 100644 repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/tests/test_gnomesettingsdaemoncheck.py
diff --git a/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/actor.py b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/actor.py
new file mode 100644
index 00000000..3056c242
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/actor.py
@@ -0,0 +1,22 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import gnomesettingsdaemoncheck
+from leapp.models import InstalledDNFComps, RpmTransactionTasks
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class GnomeSettingsDaemonCheck(Actor):
+ """
+ Install gnome-settings-daemon-server-defaults on graphical server upgrades.
+
+ If the graphical-server-environment comps environment group was installed
+ on the source RHEL 9 system, schedules the installation of the
+ gnome-settings-daemon-server-defaults package during the upgrade to RHEL 10.
+ """
+
+ name = 'gnome_settings_daemon_check'
+ consumes = (InstalledDNFComps,)
+ produces = (RpmTransactionTasks,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ gnomesettingsdaemoncheck.process()
diff --git a/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/libraries/gnomesettingsdaemoncheck.py b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/libraries/gnomesettingsdaemoncheck.py
new file mode 100644
index 00000000..6d2ff111
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/libraries/gnomesettingsdaemoncheck.py
@@ -0,0 +1,36 @@
+from leapp.libraries.stdlib import api
+from leapp.models import InstalledDNFComps, RpmTransactionTasks
+
+GSD_SERVER_DEFAULTS_PKG = 'gnome-settings-daemon-server-defaults'
+GRAPHICAL_SERVER_ENV = 'graphical-server-environment'
+
+
+def _is_graphical_server_environment_installed(installed_comps):
+ """
+ Return True if the `GRAPHICAL_SERVER_ENV` comps environment group
+ is installed on the source system, False otherwise.
+ """
+ return any(env.id == GRAPHICAL_SERVER_ENV for env in installed_comps.environments)
+
+
+def process():
+ installed_comps = next(api.consume(InstalledDNFComps), None)
+ if not installed_comps:
+ api.current_logger().warning(
+ 'Missing information about installed DNF comps: No InstalledDNFComps message.'
+ )
+ return
+
+ if not _is_graphical_server_environment_installed(installed_comps):
+ api.current_logger().debug(
+ 'The {} comps environment is not installed; skipping.'
+ .format(GRAPHICAL_SERVER_ENV)
+ )
+ return
+
+ api.current_logger().info(
+ 'The {} comps environment is installed.'
+ ' Scheduling installation of {} for the upgrade.'
+ .format(GRAPHICAL_SERVER_ENV, GSD_SERVER_DEFAULTS_PKG)
+ )
+ api.produce(RpmTransactionTasks(to_install=[GSD_SERVER_DEFAULTS_PKG]))
diff --git a/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/tests/test_gnomesettingsdaemoncheck.py b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/tests/test_gnomesettingsdaemoncheck.py
new file mode 100644
index 00000000..53681afd
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/gnomesettingsdaemoncheck/tests/test_gnomesettingsdaemoncheck.py
@@ -0,0 +1,54 @@
+import pytest
+
+from leapp.libraries.actor import gnomesettingsdaemoncheck
+from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import DNFEnvironment, InstalledDNFComps, RpmTransactionTasks
+
+
+def _installed_comps(env_ids=None):
+ envs = [DNFEnvironment(id=eid, name=eid) for eid in (env_ids or [])]
+ return InstalledDNFComps(environments=envs)
+
+
+@pytest.mark.parametrize('env_ids,expected', [
+ ([gnomesettingsdaemoncheck.GRAPHICAL_SERVER_ENV], True),
+ ([gnomesettingsdaemoncheck.GRAPHICAL_SERVER_ENV, 'minimal-environment'], True),
+ (['minimal-environment'], False),
+ ([], False),
+])
+def test_is_graphical_server_environment_installed(env_ids, expected):
+ comps = _installed_comps(env_ids)
+ assert gnomesettingsdaemoncheck._is_graphical_server_environment_installed(comps) is expected
+
+
+def test_process_produces_install_task(monkeypatch):
+ comps = _installed_comps([gnomesettingsdaemoncheck.GRAPHICAL_SERVER_ENV])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[comps]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ gnomesettingsdaemoncheck.process()
+
+ assert api.produce.called == 1
+ task = api.produce.model_instances[0]
+ assert isinstance(task, RpmTransactionTasks)
+ assert gnomesettingsdaemoncheck.GSD_SERVER_DEFAULTS_PKG in task.to_install
+
+
+def test_process_no_install_when_env_not_present(monkeypatch):
+ comps = _installed_comps(['minimal-environment'])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[comps]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ gnomesettingsdaemoncheck.process()
+
+ assert api.produce.called == 0
+
+
+def test_process_no_install_when_no_comps_message(monkeypatch):
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ gnomesettingsdaemoncheck.process()
+
+ assert api.produce.called == 0
--
2.54.0

View File

@ -0,0 +1,26 @@
From d976a0824d09a02c639a5648427150b77d01d394 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Thu, 11 Jun 2026 09:53:17 +0200
Subject: [PATCH 092/105] Bump leapp-framework
I forgot to bump it here and also in the framework with b70ff3.
---
packaging/leapp-repository.spec | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
index 70b1d7d3..627fd11a 100644
--- a/packaging/leapp-repository.spec
+++ b/packaging/leapp-repository.spec
@@ -120,7 +120,7 @@ Requires: leapp-repository-dependencies = %{leapp_repo_deps}
# IMPORTANT: this is capability provided by the leapp framework rpm.
# Check that 'version' instead of the real framework rpm version.
-Requires: leapp-framework >= 6.5, leapp-framework < 7
+Requires: leapp-framework >= 6.6, leapp-framework < 7
# Since we provide sub-commands for the leapp utility, we expect the leapp
# tool to be installed as well.
--
2.54.0

View File

@ -0,0 +1,491 @@
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/105] 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

View File

@ -0,0 +1,235 @@
From 19384aabcb467ed49e337809db9a0fd305bc3136 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Thu, 11 Jun 2026 12:18:22 +0200
Subject: [PATCH 094/105] feat(upgrade_initramfs_gen): write rd.md.uuids into
cmdline.d
To avoid inflating the kernel cmdline, the rd.md.uuid args required to
activate all md arrays during boot are written into
/etc/cmdline.d/50-leapp.conf inside the target userspace. Dracut then
loads these args during boot, avoiding boot loader limitations.
Jira-ref: RHEL-36249
---
.../upgradeinitramfsgenerator/actor.py | 4 +-
.../libraries/upgradeinitramfsgenerator.py | 40 +++++++++-
.../unit_test_upgradeinitramfsgenerator.py | 74 +++++++++++++++++++
3 files changed, 113 insertions(+), 5 deletions(-)
diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
index 32016ab0..ee6df26f 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py
@@ -7,7 +7,7 @@ from leapp.models import (
FIPSInfo,
LiveModeConfig,
LVMConfig,
- RaidInfo,
+ RAIDInfo,
TargetOSInstallationImage,
TargetUserSpaceInfo,
TargetUserSpaceUpgradeTasks,
@@ -34,7 +34,7 @@ class UpgradeInitramfsGenerator(Actor):
FIPSInfo,
LiveModeConfig,
LVMConfig,
- RaidInfo,
+ 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 f2b45ae7..f9a00eec 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
@@ -14,7 +14,7 @@ from leapp.models import (
BootContent,
LiveModeConfig,
LVMConfig,
- RaidInfo,
+ RAIDInfo,
TargetOSInstallationImage,
TargetUserSpaceInfo,
TargetUserSpaceUpgradeTasks,
@@ -25,6 +25,7 @@ from leapp.utils.deprecation import suppress_deprecation
INITRAM_GEN_SCRIPT_NAME = 'generate-initram.sh'
DRACUT_DIR = '/dracut'
+LEAPP_CMDLINE_CONF_PATH = '/etc/cmdline.d/50-leapp.conf'
DEDICATED_LEAPP_PART_URL = 'https://access.redhat.com/solutions/7011704'
@@ -329,6 +330,32 @@ def _check_free_space(context):
)
+def write_md_uuid_cmdline_conf(context):
+ """
+ Write /etc/cmdline.d/50-leapp.conf into the target userspace when MD RAID is in use.
+
+ :returns: Path of the written file, or None if no file was written.
+ :rtype: str or None
+ """
+ raid_info = next(api.consume(RAIDInfo), None)
+ if not raid_info or not raid_info.md_arrays:
+ return None
+
+ md_uuids = ['rd.md.uuid={}'.format(md_array.uuid) for md_array in raid_info.md_arrays]
+ content = '{}\n'.format(' '.join(md_uuids))
+
+ context.makedirs(os.path.dirname(LEAPP_CMDLINE_CONF_PATH), exists_ok=True)
+ with context.open(LEAPP_CMDLINE_CONF_PATH, mode='w') as cmdline_conf:
+ cmdline_conf.write(content)
+
+ api.current_logger().debug(
+ 'Written {path} with content: {content}'.format(
+ path=LEAPP_CMDLINE_CONF_PATH, content=content.rstrip('\n')
+ )
+ )
+ return LEAPP_CMDLINE_CONF_PATH
+
+
InitramfsIncludes = namedtuple('InitramfsIncludes', ('files', 'dracut_modules', 'kernel_modules'))
@@ -375,6 +402,10 @@ def generate_initram_disk(context):
def fmt_module_list(module_list):
return ','.join(mod.name for mod in module_list)
+ md_cmdline_conf = write_md_uuid_cmdline_conf(context)
+ if md_cmdline_conf:
+ initramfs_includes.files.append(md_cmdline_conf)
+
env_variables = [
'LEAPP_KERNEL_VERSION={kernel_version}',
'LEAPP_ADD_DRACUT_MODULES="{dracut_modules}"',
@@ -386,8 +417,7 @@ 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:
+ if md_cmdline_conf:
env_variables.append('LEAPP_DRACUT_MDADMCONF="1"')
env_variables = ' '.join(env_variables)
@@ -453,6 +483,10 @@ def _generate_livemode_initramfs(context, userspace_initramfs_dest, target_kerne
copy_dracut_modules(context, initramfs_includes.dracut_modules)
copy_kernel_modules(context, initramfs_includes.kernel_modules)
+ md_cmdline_conf = write_md_uuid_cmdline_conf(context)
+ if md_cmdline_conf:
+ initramfs_includes.files.append(md_cmdline_conf)
+
dracut_modules = ['livenet', 'dmsquash-live'] + [mod.name for mod in initramfs_includes.dracut_modules]
cmd = ['dracut', '--verbose', '--compress', 'xz',
diff --git a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py
index 165a4df0..b1960578 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/tests/unit_test_upgradeinitramfsgenerator.py
@@ -17,6 +17,8 @@ from leapp.models import ( # isort:skip
CopyFile,
DracutModule,
KernelModule,
+ MDArray,
+ RAIDInfo,
TargetUserSpaceUpgradeTasks,
UpgradeInitramfsTasks,
)
@@ -89,6 +91,7 @@ class MockedContext:
self.called_copy_to = []
self.called_call = []
self.called_makedirs = []
+ self.written_files = {}
self.content = set()
self.base_dir = "/base/dir"
"""
@@ -135,6 +138,25 @@ class MockedContext:
def full_path(self, path):
return os.path.join(self.base_dir, os.path.abspath(path).lstrip('/'))
+ def open(self, path, *args, mode='r', **kwargs): # pylint: disable=no-self-use
+ if 'w' not in mode:
+ raise NotImplementedError('MockedContext.open only supports write mode')
+
+ written_files = self.written_files
+ contents = []
+
+ class _Writer:
+ def write(self, data): # pylint: disable=no-self-use
+ contents.append(data)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *dummy):
+ written_files[path] = ''.join(contents)
+
+ return _Writer()
+
class MockedLogger(logger_mocked):
@@ -186,6 +208,58 @@ def _sort_files(copy_files):
return sorted(copy_files, key=lambda x: (x.src, x.dst))
+@pytest.mark.parametrize('raid_info,expected_path,expected_content', [
+ (None, None, None),
+ (RAIDInfo(md_arrays=[]), None, None),
+ (
+ RAIDInfo(md_arrays=[MDArray(uuid='aaa:bbb')]),
+ upgradeinitramfsgenerator.LEAPP_CMDLINE_CONF_PATH,
+ 'rd.md.uuid=aaa:bbb\n',
+ ),
+ (
+ RAIDInfo(md_arrays=[MDArray(uuid='aaa:bbb'), MDArray(uuid='ccc:ddd')]),
+ upgradeinitramfsgenerator.LEAPP_CMDLINE_CONF_PATH,
+ 'rd.md.uuid=aaa:bbb rd.md.uuid=ccc:ddd\n',
+ ),
+])
+def test_write_md_uuid_cmdline_conf(monkeypatch, raid_info, expected_path, expected_content):
+ context = MockedContext()
+ msgs = [raid_info] if raid_info is not None else []
+ monkeypatch.setattr(upgradeinitramfsgenerator.api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ result = upgradeinitramfsgenerator.write_md_uuid_cmdline_conf(context)
+
+ assert result == expected_path
+ if expected_path:
+ assert '/etc/cmdline.d' in context.called_makedirs
+ assert context.written_files[expected_path] == expected_content
+ else:
+ assert not context.written_files
+ assert not context.called_makedirs
+
+
+def test_generate_initram_disk_includes_md_cmdline_conf(monkeypatch):
+ context = MockedContext()
+ raid_info = RAIDInfo(md_arrays=[MDArray(uuid='aaa:bbb')])
+ input_msgs = gen_UDM_list(MODULES[0]) + [raid_info]
+ curr_actor = CurrentActorMocked(msgs=input_msgs, arch=architecture.ARCH_X86_64)
+ monkeypatch.setattr(upgradeinitramfsgenerator.api, 'current_actor', curr_actor)
+ monkeypatch.setattr(upgradeinitramfsgenerator, 'copy_dracut_modules', MockedCopyArgs())
+ monkeypatch.setattr(upgradeinitramfsgenerator, '_get_target_kernel_version', lambda _: '1.0-1.x86_64')
+ monkeypatch.setattr(upgradeinitramfsgenerator, 'copy_kernel_modules', MockedCopyArgs())
+ monkeypatch.setattr(upgradeinitramfsgenerator, 'copy_boot_files', lambda dummy: None)
+ monkeypatch.setattr(upgradeinitramfsgenerator, '_get_fspace', MockedGetFspace(2*2**30))
+
+ upgradeinitramfsgenerator.generate_initram_disk(context)
+
+ assert context.written_files[upgradeinitramfsgenerator.LEAPP_CMDLINE_CONF_PATH] == 'rd.md.uuid=aaa:bbb\n'
+ assert len(context.called_call) == 1
+ shell_cmd = context.called_call[0][0][0][2]
+ assert 'LEAPP_DRACUT_INSTALL_FILES="{path}"'.format(
+ path=upgradeinitramfsgenerator.LEAPP_CMDLINE_CONF_PATH
+ ) in shell_cmd
+ assert 'LEAPP_DRACUT_MDADMCONF="1"' in shell_cmd
+
+
def test_prepare_userspace_for_initram_no_script(monkeypatch):
monkeypatch.setattr(upgradeinitramfsgenerator.api, 'get_actor_file_path', lambda dummy: None)
with pytest.raises(StopActorExecutionError) as err:
--
2.54.0

View File

@ -0,0 +1,53 @@
From 19e6ac5f04af5d584c7ad5c2607aa35561a9eb67 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 18 Jun 2026 19:01:33 +0000
Subject: [PATCH 095/105] Update actions/checkout action to v7
---
.github/workflows/codespell.yml | 2 +-
.github/workflows/differential-shellcheck.yml | 2 +-
.github/workflows/unit-tests.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 2ac322e5..03a62d51 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: codespell-project/actions-codespell@v2
with:
ignore_words_list: ro,fo,couldn,repositor,zeor,bootup
diff --git a/.github/workflows/differential-shellcheck.yml b/.github/workflows/differential-shellcheck.yml
index 3b92d771..fc29bf30 100644
--- a/.github/workflows/differential-shellcheck.yml
+++ b/.github/workflows/differential-shellcheck.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Repository checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index f855baa2..bd829936 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -52,7 +52,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
# NOTE(ivasilev) fetch-depth 0 is critical here as leapp deps discovery depends on specific substring in
# commit message and default 1 option will get us just merge commit which has an unrelevant message.
--
2.54.0

View File

@ -0,0 +1,115 @@
From 53baea1521ed27991c807f75f03675dbb849701e Mon Sep 17 00:00:00 2001
From: Daniel Diblik <ddiblik@redhat.com>
Date: Wed, 10 Jun 2026 12:23:17 +0200
Subject: [PATCH 096/105] Update distro values for rhui jobs, follow up on
9to10
Signed-off-by: Daniel Diblik <ddiblik@redhat.com>
---
.packit.yaml | 62 ++++++++++++++++++++++++++++++++--------------------
1 file changed, 38 insertions(+), 24 deletions(-)
diff --git a/.packit.yaml b/.packit.yaml
index 49e251d8..63fe17d3 100644
--- a/.packit.yaml
+++ b/.packit.yaml
@@ -158,7 +158,7 @@ jobs:
- aws
targets:
epel-8-x86_64:
- distros: [RHEL-8.10-rhui]
+ distros: [RHEL-8-rhui]
identifier: sanity-abstract-8to9-aws
- &beaker-minimal-8to9-abstract-ondemand
@@ -235,28 +235,6 @@ jobs:
env:
<<: *env-810to96
-- &sanity-810to96-aws
- <<: *sanity-abstract-8to9-aws
- trigger: pull_request
- targets:
- epel-8-x86_64:
- distros: [RHEL-8.10-rhui]
- identifier: sanity-8.10to9.6-aws
- tf_extra_params:
- test:
- tmt:
- plan_filter: 'tag:8to9 & tag:rhui-aws-tier0 & enabled:true & tag:-rhsm'
- environments:
- - tmt:
- context: *tmt-context-810to96
- settings:
- provisioning:
- tags:
- BusinessUnit: sst_upgrades@leapp_upstream_test
- env:
- <<: *env-810to96
-
-
# ###################################################################### #
# ############################# 8.10 > 9.8 ############################# #
# ###################################################################### #
@@ -311,6 +289,24 @@ jobs:
env:
<<: *env-810to98
+- &sanity-810to98-aws
+ <<: *sanity-abstract-8to9-aws
+ trigger: pull_request
+ identifier: sanity-8.10to9.8-aws
+ tf_extra_params:
+ test:
+ tmt:
+ plan_filter: 'tag:8to9 & tag:rhui-aws-tier0 & enabled:true & tag:-rhsm'
+ environments:
+ - tmt:
+ context: *tmt-context-810to98
+ settings:
+ provisioning:
+ tags:
+ BusinessUnit: sst_upgrades@leapp_upstream_test
+ env:
+ <<: *env-810to98
+
# ###################################################################### #
# ############################# 8.10 > 9.9 ############################# #
# ###################################################################### #
@@ -452,7 +448,7 @@ jobs:
- aws
targets:
epel-9-x86_64:
- distros: [RHEL-9.6-rhui]
+ distros: [RHEL-9-rhui]
identifier: sanity-abstract-9to10-aws
- &beaker-minimal-9to10-abstract-ondemand
@@ -598,6 +594,24 @@ jobs:
env:
<<: *env-98to102
+- &sanity-98to102-aws
+ <<: *sanity-abstract-9to10-aws
+ trigger: pull_request
+ identifier: sanity-9.8to10.2-aws
+ tf_extra_params:
+ test:
+ tmt:
+ plan_filter: 'tag:9to10 & tag:rhui-aws-tier0 & enabled:true & tag:-rhsm'
+ environments:
+ - tmt:
+ context: *tmt-context-98to102
+ settings:
+ provisioning:
+ tags:
+ BusinessUnit: sst_upgrades@leapp_upstream_test
+ env:
+ <<: *env-98to102
+
# ###################################################################### #
# ############################# 9.9 > 10.3 ############################# #
# ###################################################################### #
--
2.54.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,752 @@
From 4ff499df1e88ef3a7af57aca5ac58a648ddd4707 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Thu, 4 Jun 2026 07:20:14 +0200
Subject: [PATCH 098/105] [2/9] targetcontentresolver: Init message consumption
centralization
Centralize consumption of RepositoriesFacts, RepositoriesSetupTasks,
and CustomTargetRepository into a new InputData class in
targetcontentresolver. This deduplicates logic previously scattered
across repositoriesblocklist and pes_events_scanner, and validates
that required messages are present before processing begins.
This is initial set of changes for future refactoring in individual
libraries of the actor.
Some details about individual changes:
* setuptargetrepos:
* Drop the rename of get_enabled_repoids to a private functions
when importing.
* targetcontentresolver:
* Introduce InputData class that consumes and validates required
messages, warns about unexpected duplicates, and raises
StopActorExecutionError when required messages are missing.
* Introduce ExternalRepoSetupTasks named tuple to collect all
external repository requests (to_enable, to_block, custom)
from RepositoriesSetupTasks and CustomTargetRepository messages.
* pes_events_scanner:
* Pass enabled_repoids as parameter through scan_pes_events,
get_pesid_to_repoid_map, and replace_pesids_with_repoids_in_packages
instead of fetching it internally via get_enabled_repoids().
* Drop default parameter for blacklisted_repoids in scan_pes_events
(must be always specified).
* repositoriesblocklist.compute_blocklist:
* Accept external_tasks (ExternalRepoSetupTasks) instead of consuming
RepositoriesSetupTasks directly.
* Return only the set of blocklisted repoids instead of a tuple of
(blocklisted, enabled).
* Move RepositoriesFacts missing-message check to InputData.
* Update docstring to describe expected precedence order for
conflicting requests (implementation deferred to next commit).
* Remove misused FAILURE group from the CRB exclusion report.
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Assisted-by: Claude Code (Opus 4.6)
Co-authored-by: Matej Matuska <mmatuska@redhat.com>
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/pes_event_parsing.py | 1 +
.../libraries/pes_events_scanner.py | 18 +--
.../libraries/repositoriesblocklist.py | 83 +++++++------
.../libraries/setuptargetrepos.py | 4 +-
.../libraries/targetcontentresolver.py | 93 ++++++++++++--
.../tests/test_pes_event_scanner.py | 12 +-
.../tests/test_repositoriesblocklist.py | 49 +++-----
.../tests/test_targetcontentresolver.py | 114 +++++++++++++++++-
8 files changed, 279 insertions(+), 95 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_event_parsing.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_event_parsing.py
index f24dda68..febf8578 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_event_parsing.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_event_parsing.py
@@ -109,6 +109,7 @@ def get_pes_events(pes_json_directory, pes_json_filename):
reporting.Groups([reporting.Groups.SANITY, reporting.Groups.INHIBITOR]),
reporting.RelatedResource('file', os.path.join(pes_json_directory, pes_json_filename))
])
+
raise StopActorExecution()
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
index a03071ae..cbc99adf 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
@@ -320,7 +320,7 @@ def report_skipped_packages(title, message, skipped_pkgs, remediation=None):
api.current_logger().info(summary)
-def remove_new_packages_from_blacklisted_repos(source_pkgs, target_pkgs, blacklisted_repoids=None):
+def remove_new_packages_from_blacklisted_repos(source_pkgs, target_pkgs, blacklisted_repoids):
"""
Remove newly installed packages from blacklisted repositories that were computed to be on the target system.
@@ -357,7 +357,7 @@ def get_enabled_repoids():
return enabled_repoids
-def get_pesid_to_repoid_map(target_pesids, repositories_map_msg):
+def get_pesid_to_repoid_map(target_pesids, repositories_map_msg, enabled_repoids):
"""
Get a dictionary mapping all PESID repositories to their corresponding repoid.
@@ -373,7 +373,6 @@ def get_pesid_to_repoid_map(target_pesids, repositories_map_msg):
# NOTE: We have to calculate expected target repositories like in the setuptargetrepos actor.
# It's planned to handle this in different a way in future...
- enabled_repoids = get_enabled_repoids()
default_channels = repomap_calc.get_default_repository_channels(repomap_handler, enabled_repoids)
repomap_handler.set_default_channels(default_channels)
@@ -433,7 +432,7 @@ def get_pesid_to_repoid_map(target_pesids, repositories_map_msg):
return repositories_mapping
-def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repositories_map_msg):
+def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repositories_map_msg, enabled_repoids):
"""Replace packages with PESID in their .repository field with ones that have repoid providing the package."""
# We want to map only PESIDs - if some package had no events, it will its repository set to source system repoid
packages_with_pesid = {pkg for pkg in packages if pkg.repository not in source_pkgs_repoids}
@@ -441,7 +440,7 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repos
required_target_pesids = {pkg.repository for pkg in packages_with_pesid}
- pesid_to_target_repoid_map = get_pesid_to_repoid_map(required_target_pesids, repositories_map_msg)
+ pesid_to_target_repoid_map = get_pesid_to_repoid_map(required_target_pesids, repositories_map_msg, enabled_repoids)
packages_with_unknown_target_repoid = {
pkg
@@ -555,7 +554,7 @@ def include_instructions_from_transaction_configuration(rpm_tasks, transaction_c
modules_to_reset=modules_to_reset)
-def scan_pes_events(repositories_map_msg, blacklisted_repoids=None):
+def scan_pes_events(repositories_map_msg, blacklisted_repoids, enabled_repoids):
"""
Process PES events and compute RPM transaction tasks.
@@ -586,7 +585,12 @@ def scan_pes_events(repositories_map_msg, blacklisted_repoids=None):
events, releases)
# Packages coming out of the events have PESID as their repository, however, we need real repoid
- target_pkgs = replace_pesids_with_repoids_in_packages(target_pkgs, repoids_of_source_pkgs, repositories_map_msg)
+ target_pkgs = replace_pesids_with_repoids_in_packages(
+ target_pkgs,
+ repoids_of_source_pkgs,
+ repositories_map_msg,
+ enabled_repoids
+ )
# Apply the desired repository blacklisting
blacklisted_repoids, target_pkgs = remove_new_packages_from_blacklisted_repos(pkgs_to_begin_computation_with,
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
index 48656187..216bea52 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
@@ -1,14 +1,7 @@
from leapp import reporting
-from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api
-from leapp.models import (
- CustomTargetRepository,
- RepositoriesBlacklisted,
- RepositoriesBlocklisted,
- RepositoriesFacts,
- RepositoriesSetupTasks
-)
+from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesBlocklisted, RepositoriesFacts
from leapp.utils.deprecation import suppress_deprecation
# {OS_MAJOR_VERSION: PESID}
@@ -47,7 +40,6 @@ def _report_excluded_repos(repos):
),
reporting.Severity(reporting.Severity.INFO),
reporting.Groups([reporting.Groups.REPOSITORY]),
- reporting.Groups([reporting.Groups.FAILURE]),
reporting.Remediation(
hint=(
'If some of excluded repositories are still required to be used'
@@ -136,7 +128,7 @@ def get_blocklisted_repoids(repo_mapping):
Conditions to exclude:
- there are not such repositories already enabled on the source system
- (e.g. "Optional" repositories)
+ (e.g. "CRB" repositories)
- such repositories are not required for the upgrade explicitly by the user
(e.g. via the --enablerepo option or via the /etc/leapp/files/leapp_upgrade_repositories.repo file)
@@ -146,13 +138,9 @@ def get_blocklisted_repoids(repo_mapping):
:returns: Set of repoids to blocklist on the target system.
:rtype: set
"""
+ # TODO(pstodulk): replace it by enabled_repos
repos_facts = next(api.consume(RepositoriesFacts), None)
- if not repos_facts:
- raise StopActorExecutionError('Actor didn\'t receive required messages: {0}'.format(
- 'RepositoriesFacts'
- ))
-
if not _are_optional_repos_disabled(repo_mapping, repos_facts):
# nothing to do - an optional repository is enabled
return set()
@@ -172,34 +160,51 @@ def get_blocklisted_repoids(repo_mapping):
@suppress_deprecation(RepositoriesBlacklisted)
-def compute_blocklist(repo_mapping):
+def compute_blocklist(repo_mapping, external_tasks):
"""
- Compute the full blocklist and extract external repoid requests.
+ Create the blocklist of repositories that should be blocked during the upgrade.
+
+ The content in the CRB repository is considered as unsupported. Hence
+ we do not want to enable such a repository by default on the target system
+ unless
+ * it is explicitely requested, or
+ * it is already enabled on the source system
+ This is important as some functionality originally supported on the source
+ source system can be moved to the CRB repository on the target system.
+ In such a case, we do not want to install automatically such a content
+ that lost a support.
+
+ For this reason, compute the list of repositories that should be blocklisted
+ and take into account also external requests.
+ In case that multiple requests are raised for a specific repository, the
+ priority order is following:
+ internal block rq > external enable rq > external block rq > external custom rq
+
+ The external custom request has the highest priority as this comes from
+ the system configuration or from the `--enablerepo` option used when executing
+ leapp - so it's understood as decision made by user explicitely for
+ the in-place upgrade purpose. Currently there is no explicit custom
+ configuration to disable a repo.
+
+ Once the list is calculated, the RepositoriesBlocklisted and
+ RepositoriesBlacklisted messages are produced.
- Merges the internally determined blocklist (unsupported repos like CRB)
- with external blocklist requests from ``RepositoriesSetupTasks.to_block``.
- When the resulting blocklist is non-empty, produces both
- ``RepositoriesBlocklisted`` and the deprecated ``RepositoriesBlacklisted``
- messages.
+ :param repo_mapping: RepositoriesMapping message.
+ :type repo_mapping: RepositoriesMapping
+ :param external_tasks: External repositories tasks represented by object with following fields:
- Also extracts ``to_enable`` repoids from the same external tasks so that
- the caller does not need to re-consume the messages.
+ * ``to_enable`` - repositories that should be enabled
+ * ``to_block`` - repositories that should be blocklisted
+ * ``custom`` - repositories explicitely requested by user to be used during the upgrade
- :param repo_mapping: RepositoriesMapping message.
- :returns: Tuple of (full_blocklist_repoids, external_repoids_requests).
- :rtype: tuple[set, set]
+ :type external_tasks: targetcontentresolver.ExternalRepoSetupTasks
+ :returns: List of blocklisted repositories
+ :rtype: List[str]
"""
internal_blocklist = get_blocklisted_repoids(repo_mapping)
+ full_blocklist = internal_blocklist.union(external_tasks.to_block)
+ if full_blocklist:
+ api.produce(RepositoriesBlocklisted(repoids=sorted(full_blocklist)))
+ api.produce(RepositoriesBlacklisted(repoids=sorted(full_blocklist)))
- external_blocklist_repoids = set()
- external_repoids_requests = set()
- for task in api.consume(RepositoriesSetupTasks):
- external_blocklist_repoids.update(task.to_block)
- external_repoids_requests.update(task.to_enable)
-
- full_blocklist_repoids = internal_blocklist.union(external_blocklist_repoids)
- if full_blocklist_repoids:
- api.produce(RepositoriesBlocklisted(repoids=sorted(full_blocklist_repoids)))
- api.produce(RepositoriesBlacklisted(repoids=sorted(full_blocklist_repoids)))
-
- return full_blocklist_repoids, external_repoids_requests
+ return full_blocklist
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
index b9e5a1c4..d6bfb91d 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
@@ -1,5 +1,5 @@
from leapp.libraries.actor import repomap_calc
-from leapp.libraries.actor.pes_events_scanner import get_enabled_repoids as _get_enabled_repoids
+from leapp.libraries.actor.pes_events_scanner import get_enabled_repoids
from leapp.libraries.common.config import get_source_distro_id, get_target_distro_id
from leapp.libraries.common.config.version import get_source_major_version, get_source_version
from leapp.libraries.stdlib import api
@@ -70,7 +70,7 @@ def setup_target_repos(repositories_map_msg, pes_requested_repoids=None,
"""
# Load relevant data from messages
used_repoids_dict = _get_used_repo_dict()
- enabled_repoids = _get_enabled_repoids()
+ enabled_repoids = get_enabled_repoids()
excluded_repoids = blacklisted_repoids if blacklisted_repoids is not None else set()
custom_repos = _get_custom_target_repos()
repoids_from_installed_packages = _get_repoids_from_installed_packages()
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index 4db9003d..c1b46916 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -1,5 +1,83 @@
+from collections import namedtuple
+
+from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import pes_events_scanner, repositoriesblocklist, setuptargetrepos
from leapp.libraries.actor.repomap_loader import scan_repositories
+from leapp.libraries.stdlib import api
+from leapp.models import CustomTargetRepository, RepositoriesFacts, RepositoriesSetupTasks
+
+ExternalRepoSetupTasks = namedtuple('ExternalRepoSetupTasks', ('to_enable', 'to_block', 'custom'))
+
+
+class InputData():
+ """
+ Provide data from consumed messages
+ """
+
+ def __init__(self):
+ self._missing_messages = []
+
+ self._get_enabled_repoids()
+ self._get_external_reposetup_tasks()
+
+ if self._missing_messages:
+ # NOTE(pstodulk): This would be an invalid object,
+ # so let's end the story here.
+ raise StopActorExecutionError(
+ message='Cannot calculate target content requirements',
+ details={
+ 'details': 'Some of required leapp messages are missing.',
+ 'missing': [i.__name__ for i in self._missing_messages]
+ }
+ )
+
+ def _treat_consume_msg(self, model):
+ msgs = api.consume(model)
+ msg = next(msgs, None)
+ if list(msgs):
+ w_msg = f'Unexpectedly received more than one {model.__name__} message.'
+ api.current_logger().warning(w_msg)
+ if not msg:
+ self._missing_messages.append(model)
+ return None
+ return msg
+
+ def _get_external_reposetup_tasks(self):
+ """
+ Collect repository related tasks from other actors (or configs in future).
+
+ This includes consumption of RepositoriesSetupTasks and CustomTargetRepository.
+ The second one should be understood as task as well based on the definition,
+ even when the name does not suggest it's actually task as well.
+
+ Return named tuple with `to_enable`, `to_block`, and `custom` fields.
+ The last one is based on reposids in CustomTargetRepository messages.
+ """
+ self.external_tasks = ExternalRepoSetupTasks(set(), set(), set())
+ for task in api.consume(RepositoriesSetupTasks):
+ self.external_tasks.to_block.update(task.to_block)
+ self.external_tasks.to_enable.update(task.to_enable)
+
+ self.external_tasks.custom.update({repo.repoid for repo in api.consume(CustomTargetRepository)})
+
+ def _get_enabled_repoids(self):
+ """
+ Return set of repository IDs enabled on the source system.
+
+ The information is consumed from the RepositoriesFacts message.
+
+ :return: Set of all enabled repository IDs present on the source system.
+ :rtype: Set[str]
+ """
+ self.enabled_repoids = set()
+ repos_facts = self._treat_consume_msg(RepositoriesFacts)
+ if repos_facts is None:
+ return
+
+ for repo_file in repos_facts.repositories:
+ for repo in repo_file.data:
+ if repo.enabled:
+ self.enabled_repoids.add(repo.repoid)
def process():
@@ -23,17 +101,18 @@ def process():
source repos, installed package repos, and custom/blacklisted repo
configuration.
"""
+ indata = InputData()
repositories_map_msg = scan_repositories()
-
- full_blocklist_repoids, external_repoids_requests = (
- repositoriesblocklist.compute_blocklist(repositories_map_msg)
+ blocklisted_repoids = repositoriesblocklist.compute_blocklist(repositories_map_msg, indata.external_tasks)
+ pes_requested_repoids = pes_events_scanner.scan_pes_events(
+ repositories_map_msg,
+ blocklisted_repoids,
+ indata.enabled_repoids
)
- pes_requested_repoids = pes_events_scanner.scan_pes_events(repositories_map_msg, full_blocklist_repoids)
-
setuptargetrepos.setup_target_repos(
repositories_map_msg,
pes_requested_repoids=pes_requested_repoids,
- blacklisted_repoids=full_blocklist_repoids,
- external_repoids_requests=external_repoids_requests,
+ blacklisted_repoids=blocklisted_repoids,
+ external_repoids_requests=indata.external_tasks.to_enable,
)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
index f872a947..a1f3a6e1 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
@@ -270,7 +270,7 @@ def test_actor_performs(monkeypatch):
monkeypatch.setattr(api, 'produce', produced_messages)
monkeypatch.setattr(reporting, 'create_report', created_report)
- pes_events_scanner.scan_pes_events(repositories_mapping)
+ pes_events_scanner.scan_pes_events(repositories_mapping, set(), {'rhel8-repo'})
assert produced_messages.called
@@ -341,13 +341,13 @@ def test_blacklisted_repoid_is_not_produced(monkeypatch):
monkeypatch.setattr(pes_events_scanner, 'get_pes_events', lambda folder, filename: events)
monkeypatch.setattr(pes_events_scanner, 'apply_transaction_configuration', lambda pkgs, transaction_cfg: pkgs)
monkeypatch.setattr(pes_events_scanner, 'replace_pesids_with_repoids_in_packages',
- lambda pkgs, src_pkgs_repoids, repo_map_msg=None: pkgs)
+ lambda pkgs, src_pkgs_repoids, repo_map_msg, enabled_repoids: pkgs)
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- setup_tasks = pes_events_scanner.scan_pes_events(None, blacklisted_repoids={'blacklisted-rhel9'})
+ setup_tasks = pes_events_scanner.scan_pes_events(None, {'blacklisted-rhel9'}, set())
assert not reporting.create_report.called
@@ -497,10 +497,10 @@ def test_transaction_configuration_is_applied(monkeypatch):
monkeypatch.setattr(pes_events_scanner, 'remove_undesired_events', lambda events, releases: events)
monkeypatch.setattr(pes_events_scanner, '_get_enabled_modules', lambda *args: [])
monkeypatch.setattr(pes_events_scanner, 'replace_pesids_with_repoids_in_packages',
- lambda target_pkgs, repoids_of_source_pkgs, repo_map_msg=None: target_pkgs)
+ lambda target_pkgs, repoids_of_source_pkgs, repo_map_msg, enabled_repoids: target_pkgs)
monkeypatch.setattr(pes_events_scanner,
'remove_new_packages_from_blacklisted_repos',
- lambda source_pkgs, target_pkgs, bl=None: (set(), target_pkgs))
+ lambda source_pkgs, target_pkgs, bl: (set(), target_pkgs))
msgs = [
RpmTransactionTasks(to_remove=['pkg-not-in-events']),
@@ -513,7 +513,7 @@ def test_transaction_configuration_is_applied(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())
- setup_tasks = pes_events_scanner.scan_pes_events(None)
+ setup_tasks = pes_events_scanner.scan_pes_events(None, set(), set())
assert api.produce.called == 1
assert setup_tasks is not None
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
index 5ffa0990..fc657824 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
@@ -2,6 +2,7 @@ import pytest
from leapp import reporting
from leapp.libraries.actor import repositoriesblocklist
+from leapp.libraries.actor.targetcontentresolver import ExternalRepoSetupTasks
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
@@ -12,7 +13,6 @@ from leapp.models import (
RepositoriesBlocklisted,
RepositoriesFacts,
RepositoriesMapping,
- RepositoriesSetupTasks,
RepositoryData,
RepositoryFile
)
@@ -228,21 +228,19 @@ def _get_produced_blacklisted():
def test_compute_blocklist_merges_internal_and_external(monkeypatch):
"""
compute_blocklist merges internal blocklist with external
- RepositoriesSetupTasks.to_block and produces both messages.
+ ExternalRepoSetupTasks.to_block and produces both messages.
"""
- external_tasks = [
- RepositoriesSetupTasks(to_enable=['some-repo'], to_block=['ext-blocked-1', 'ext-blocked-2']),
- ]
+ external_tasks = ExternalRepoSetupTasks(
+ to_enable={'some-repo'}, to_block={'ext-blocked-1', 'ext-blocked-2'}, custom=set()
+ )
monkeypatch.setattr(
repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'crb-repo-1'}
)
- monkeypatch.setattr(api, 'consume', lambda _model: iter(external_tasks))
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist, external_repoids = repositoriesblocklist.compute_blocklist(None)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
assert full_blocklist == {'crb-repo-1', 'ext-blocked-1', 'ext-blocked-2'}
- assert external_repoids == {'some-repo'}
blocklisted_msgs = _get_produced_blocklisted()
assert len(blocklisted_msgs) == 1
@@ -255,16 +253,15 @@ def test_compute_blocklist_merges_internal_and_external(monkeypatch):
def test_compute_blocklist_no_messages_when_empty(monkeypatch):
"""No blocklist messages are produced when blocklist is empty."""
+ external_tasks = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
monkeypatch.setattr(
repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: set()
)
- monkeypatch.setattr(api, 'consume', lambda _model: iter([]))
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist, external_repoids = repositoriesblocklist.compute_blocklist(None)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
assert full_blocklist == set()
- assert external_repoids == set()
assert len(_get_produced_blocklisted()) == 0
assert len(_get_produced_blacklisted()) == 0
@@ -272,53 +269,47 @@ def test_compute_blocklist_no_messages_when_empty(monkeypatch):
@suppress_deprecation(RepositoriesBlacklisted)
def test_compute_blocklist_only_internal(monkeypatch):
"""Only internal blocklist when no external tasks are present."""
+ external_tasks = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
monkeypatch.setattr(
repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'crb-repo-1'}
)
- monkeypatch.setattr(api, 'consume', lambda _model: iter([]))
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist, external_repoids = repositoriesblocklist.compute_blocklist(None)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
assert full_blocklist == {'crb-repo-1'}
- assert external_repoids == set()
assert len(_get_produced_blocklisted()) == 1
-def test_compute_blocklist_multiple_external_tasks(monkeypatch):
- """Blocklist and to_enable from multiple external tasks are aggregated."""
- external_tasks = [
- RepositoriesSetupTasks(to_block=['ext-1'], to_enable=['repo-a']),
- RepositoriesSetupTasks(to_block=['ext-2', 'ext-3'], to_enable=['repo-b']),
- ]
+def test_compute_blocklist_aggregated_external_tasks(monkeypatch):
+ """Blocklist from pre-aggregated external tasks is merged with internal."""
+ external_tasks = ExternalRepoSetupTasks(
+ to_enable={'repo-a', 'repo-b'}, to_block={'ext-1', 'ext-2', 'ext-3'}, custom=set()
+ )
monkeypatch.setattr(
repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: set()
)
- monkeypatch.setattr(api, 'consume', lambda _model: iter(external_tasks))
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist, external_repoids = repositoriesblocklist.compute_blocklist(None)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
assert full_blocklist == {'ext-1', 'ext-2', 'ext-3'}
- assert external_repoids == {'repo-a', 'repo-b'}
@suppress_deprecation(RepositoriesBlacklisted)
def test_compute_blocklist_overlapping_internal_and_external(monkeypatch):
"""Overlapping repoids between internal and external blocklists are deduplicated."""
- external_tasks = [
- RepositoriesSetupTasks(to_block=['shared-repo', 'ext-only']),
- ]
+ external_tasks = ExternalRepoSetupTasks(
+ to_enable=set(), to_block={'shared-repo', 'ext-only'}, custom=set()
+ )
monkeypatch.setattr(
repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'shared-repo', 'internal-only'}
)
- monkeypatch.setattr(api, 'consume', lambda _model: iter(external_tasks))
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist, external_repoids = repositoriesblocklist.compute_blocklist(None)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
assert full_blocklist == {'shared-repo', 'internal-only', 'ext-only'}
- assert external_repoids == set()
blocklisted_msgs = _get_produced_blocklisted()
assert len(blocklisted_msgs) == 1
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 4e425da6..98f19ea8 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -1,4 +1,17 @@
+import pytest
+
+from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import targetcontentresolver
+from leapp.libraries.actor.targetcontentresolver import ExternalRepoSetupTasks, InputData
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import (
+ CustomTargetRepository,
+ RepositoriesFacts,
+ RepositoriesSetupTasks,
+ RepositoryData,
+ RepositoryFile
+)
def test_process_orchestration(monkeypatch):
@@ -10,22 +23,33 @@ def test_process_orchestration(monkeypatch):
fake_repo_map = object()
fake_blocklist = frozenset({'blocked-repo'})
- fake_external_repoids = frozenset({'ext-repo'})
+ fake_external_tasks = ExternalRepoSetupTasks(
+ to_enable=frozenset({'ext-repo'}), to_block=frozenset(), custom=frozenset()
+ )
+ fake_enabled_repoids = frozenset({'enabled-repo'})
fake_pes_repoids = frozenset({'pes-repo'})
+ class FakeInputData:
+ def __init__(self):
+ call_log.append('InputData')
+ self.external_tasks = fake_external_tasks
+ self.enabled_repoids = fake_enabled_repoids
+
def mock_scan_repositories():
call_log.append('scan_repositories')
return fake_repo_map
- def mock_compute_blocklist(repo_mapping):
+ def mock_compute_blocklist(repo_mapping, external_tasks):
call_log.append('compute_blocklist')
assert repo_mapping is fake_repo_map
- return fake_blocklist, fake_external_repoids
+ assert external_tasks is fake_external_tasks
+ return fake_blocklist
- def mock_scan_pes_events(repo_mapping, blacklisted_repoids):
+ def mock_scan_pes_events(repo_mapping, blacklisted_repoids, enabled_repoids):
call_log.append('scan_pes_events')
assert repo_mapping is fake_repo_map
assert blacklisted_repoids is fake_blocklist
+ assert enabled_repoids is fake_enabled_repoids
return fake_pes_repoids
def mock_setup_target_repos(repo_mapping, pes_requested_repoids=None,
@@ -34,8 +58,12 @@ def test_process_orchestration(monkeypatch):
assert repo_mapping is fake_repo_map
assert pes_requested_repoids is fake_pes_repoids
assert blacklisted_repoids is fake_blocklist
- assert external_repoids_requests is fake_external_repoids
+ assert external_repoids_requests is fake_external_tasks.to_enable
+ monkeypatch.setattr(
+ 'leapp.libraries.actor.targetcontentresolver.InputData',
+ FakeInputData,
+ )
monkeypatch.setattr(
'leapp.libraries.actor.targetcontentresolver.scan_repositories',
mock_scan_repositories,
@@ -56,8 +84,84 @@ def test_process_orchestration(monkeypatch):
targetcontentresolver.process()
assert call_log == [
+ 'InputData',
'scan_repositories',
'compute_blocklist',
'scan_pes_events',
'setup_target_repos',
]
+
+
+def _make_repo_facts(repoids_enabled=None, repoids_disabled=None):
+ repos_data = []
+ for repoid in (repoids_enabled or []):
+ repos_data.append(RepositoryData(repoid=repoid, name=repoid, enabled=True))
+ for repoid in (repoids_disabled or []):
+ repos_data.append(RepositoryData(repoid=repoid, name=repoid, enabled=False))
+ return RepositoriesFacts(
+ repositories=[RepositoryFile(file='/etc/yum.repos.d/test.repo', data=repos_data)]
+ )
+
+
+def test_inputdata_collects_enabled_repoids(monkeypatch):
+ repo_facts = _make_repo_facts(
+ repoids_enabled=['repo-a', 'repo-b'],
+ repoids_disabled=['repo-c'],
+ )
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts]))
+
+ indata = InputData()
+
+ assert indata.enabled_repoids == {'repo-a', 'repo-b'}
+
+
+def test_inputdata_raises_when_repofacts_missing(monkeypatch):
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+
+ with pytest.raises(StopActorExecutionError):
+ InputData()
+
+
+def test_inputdata_aggregates_external_tasks(monkeypatch):
+ repo_facts = _make_repo_facts(repoids_enabled=['repo-a'])
+ setup_tasks_1 = RepositoriesSetupTasks(to_enable=['en-1'], to_block=['bl-1'])
+ setup_tasks_2 = RepositoriesSetupTasks(to_enable=['en-2'], to_block=['bl-2', 'bl-3'])
+ custom_1 = CustomTargetRepository(repoid='custom-1')
+ custom_2 = CustomTargetRepository(repoid='custom-2')
+
+ monkeypatch.setattr(
+ api, 'current_actor',
+ CurrentActorMocked(msgs=[repo_facts, setup_tasks_1, setup_tasks_2, custom_1, custom_2])
+ )
+
+ indata = InputData()
+
+ assert indata.external_tasks.to_enable == {'en-1', 'en-2'}
+ assert indata.external_tasks.to_block == {'bl-1', 'bl-2', 'bl-3'}
+ assert indata.external_tasks.custom == {'custom-1', 'custom-2'}
+
+
+def test_inputdata_empty_external_tasks(monkeypatch):
+ repo_facts = _make_repo_facts(repoids_enabled=['repo-a'])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts]))
+
+ indata = InputData()
+
+ assert indata.external_tasks.to_enable == set()
+ assert indata.external_tasks.to_block == set()
+ assert indata.external_tasks.custom == set()
+
+
+def test_inputdata_warns_on_duplicate_repofacts(monkeypatch):
+ repo_facts_1 = _make_repo_facts(repoids_enabled=['repo-a'])
+ repo_facts_2 = _make_repo_facts(repoids_enabled=['repo-b'])
+ monkeypatch.setattr(
+ api, 'current_actor',
+ CurrentActorMocked(msgs=[repo_facts_1, repo_facts_2])
+ )
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ indata = InputData()
+
+ assert indata.enabled_repoids == {'repo-a'}
+ assert any('more than one' in msg for msg in api.current_logger.warnmsg)
--
2.54.0

View File

@ -0,0 +1,309 @@
From 51af2cfd4b028efa58c75d34c55b7b65eac7df27 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sat, 6 Jun 2026 04:25:39 +0200
Subject: [PATCH 099/105] [3/9] targetcontentresolver: RepositoriesBlacklisted:
use it as task
Previous "refactoring" commits changes the purpose of the message
from
1. task specifying what should be blocklisted
2. fact what repositories are blocklisted
leaving it to be just the fact message. As discussed in team,
we believe that if the model has been used by any other third-party
actors, it was used in the first way to request blocklist of specific
repositories. So updating the behaviour in the way the message is
consumed and processed now instead of producing it.
Note that both purposes cannot be kept in this original message
due to planned changes - which is one of reasons why the model
becomes deprecated in previous commits.
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries-and-api/deprecations-list.md | 2 +-
.../actors/targetcontentresolver/actor.py | 4 +--
.../libraries/repositoriesblocklist.py | 8 ++---
.../libraries/targetcontentresolver.py | 8 ++++-
.../tests/test_repositoriesblocklist.py | 23 ++----------
.../tests/test_targetcontentresolver.py | 36 +++++++++++++++++++
.../common/models/repositoriesblocklisted.py | 8 +++--
7 files changed, 55 insertions(+), 34 deletions(-)
diff --git a/docs/source/libraries-and-api/deprecations-list.md b/docs/source/libraries-and-api/deprecations-list.md
index 301de558..1968c8af 100644
--- a/docs/source/libraries-and-api/deprecations-list.md
+++ b/docs/source/libraries-and-api/deprecations-list.md
@@ -18,12 +18,12 @@ Only the versions in which a deprecation has been made are listed.
- **`LEAPP_NO_NETWORK_RENAMING`** - It becomes obsoleted by the solution based on `net.naming-scheme` which replaces the legacy solution based on created udev link files correcting NIC names during the upgrade.
- Models:
- **`RenamedInterfaces`** - Information provided in this message is not always complete and it's not used since the `net.naming-scheme` kernel command line argument is set during the upgrade.
+ - **`RepositoriesBlacklisted`** - To get the list of blocklisted repositories, consume `RepositoriesBlocklisted` message. To request a repository to be blocklisted, use `RepositoriesSetupTasks.to_block` instead.
- Shared libraries
- **`leapp.libraries.common.dnfconfig`** - Moved to `leapp.libraries.common.dnflibs.dnfconfig`. Original library is deprecated.
- **`leapp.libraries.common.dnfplugin`** - Moved to `leapp.libraries.common.dnflibs.dnfplugin`. Original library is deprecated.
- **`leapp.libraries.common.module`** - Replaced by `leapp.libraries.common.dnflibs.dnfmodule` (renamed for clarity). Original library is deprecated.
-
## v0.24.0 <span style="font-size:0.5em; font-weight:normal">(till September 2026)</span>
- Shared libraries
- **`leapp.libraries.common.config.get_distro_id()`** - The function has been replaced by variants for source and target distros - `leapp.libraries.common.config.get_source_distro_id()` and `leapp.libraries.common.config.get_target_distro_id()`.
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/actor.py b/repos/system_upgrade/common/actors/targetcontentresolver/actor.py
index 6ea95732..e132cc3a 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/actor.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/actor.py
@@ -43,9 +43,10 @@ class TargetContentResolver(Actor):
DistributionSignedRPM,
EnabledModules,
InstalledRPM,
+ RHUIInfo,
+ RepositoriesBlacklisted,
RepositoriesFacts,
RepositoriesSetupTasks,
- RHUIInfo,
RpmTransactionTasks,
UsedRepositories,
)
@@ -53,7 +54,6 @@ class TargetContentResolver(Actor):
ConsumedDataAsset,
PESRpmTransactionTasks,
Report,
- RepositoriesBlacklisted,
RepositoriesBlocklisted,
RepositoriesMapping,
SkippedRepositories,
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
index 216bea52..f92f64fb 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
@@ -1,8 +1,7 @@
from leapp import reporting
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api
-from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesBlocklisted, RepositoriesFacts
-from leapp.utils.deprecation import suppress_deprecation
+from leapp.models import CustomTargetRepository, RepositoriesBlocklisted, RepositoriesFacts
# {OS_MAJOR_VERSION: PESID}
UNSUPPORTED_PESIDS = {
@@ -159,7 +158,6 @@ def get_blocklisted_repoids(repo_mapping):
return filtered_repos_to_exclude
-@suppress_deprecation(RepositoriesBlacklisted)
def compute_blocklist(repo_mapping, external_tasks):
"""
Create the blocklist of repositories that should be blocked during the upgrade.
@@ -186,8 +184,7 @@ def compute_blocklist(repo_mapping, external_tasks):
the in-place upgrade purpose. Currently there is no explicit custom
configuration to disable a repo.
- Once the list is calculated, the RepositoriesBlocklisted and
- RepositoriesBlacklisted messages are produced.
+ Once the list is calculated the RepositoriesBlocklisted message is produced.
:param repo_mapping: RepositoriesMapping message.
:type repo_mapping: RepositoriesMapping
@@ -205,6 +202,5 @@ def compute_blocklist(repo_mapping, external_tasks):
full_blocklist = internal_blocklist.union(external_tasks.to_block)
if full_blocklist:
api.produce(RepositoriesBlocklisted(repoids=sorted(full_blocklist)))
- api.produce(RepositoriesBlacklisted(repoids=sorted(full_blocklist)))
return full_blocklist
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index c1b46916..e6885e44 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -4,7 +4,8 @@ from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import pes_events_scanner, repositoriesblocklist, setuptargetrepos
from leapp.libraries.actor.repomap_loader import scan_repositories
from leapp.libraries.stdlib import api
-from leapp.models import CustomTargetRepository, RepositoriesFacts, RepositoriesSetupTasks
+from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesFacts, RepositoriesSetupTasks
+from leapp.utils.deprecation import suppress_deprecation
ExternalRepoSetupTasks = namedtuple('ExternalRepoSetupTasks', ('to_enable', 'to_block', 'custom'))
@@ -42,6 +43,7 @@ class InputData():
return None
return msg
+ @suppress_deprecation(RepositoriesBlacklisted)
def _get_external_reposetup_tasks(self):
"""
Collect repository related tasks from other actors (or configs in future).
@@ -58,6 +60,10 @@ class InputData():
self.external_tasks.to_block.update(task.to_block)
self.external_tasks.to_enable.update(task.to_enable)
+ # DEPRECATED: Drop after 2026-07
+ for task in api.consume(RepositoriesBlacklisted):
+ self.external_tasks.to_block.update(task.repoids)
+
self.external_tasks.custom.update({repo.repoid for repo in api.consume(CustomTargetRepository)})
def _get_enabled_repoids(self):
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
index fc657824..ce72dc9e 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
@@ -9,14 +9,12 @@ from leapp.models import (
CustomTargetRepository,
PESIDRepositoryEntry,
RepoMapEntry,
- RepositoriesBlacklisted,
RepositoriesBlocklisted,
RepositoriesFacts,
RepositoriesMapping,
RepositoryData,
RepositoryFile
)
-from leapp.utils.deprecation import suppress_deprecation
@pytest.fixture
@@ -216,19 +214,13 @@ def test_enablerepo_option(monkeypatch,
def _get_produced_blocklisted():
- return [m for m in api.produce.model_instances
- if isinstance(m, RepositoriesBlocklisted) and not isinstance(m, RepositoriesBlacklisted)]
+ return [m for m in api.produce.model_instances if isinstance(m, RepositoriesBlocklisted)]
-def _get_produced_blacklisted():
- return [m for m in api.produce.model_instances if isinstance(m, RepositoriesBlacklisted)]
-
-
-@suppress_deprecation(RepositoriesBlacklisted)
def test_compute_blocklist_merges_internal_and_external(monkeypatch):
"""
compute_blocklist merges internal blocklist with external
- ExternalRepoSetupTasks.to_block and produces both messages.
+ ExternalRepoSetupTasks.to_block and produces RepositoriesBlocklisted.
"""
external_tasks = ExternalRepoSetupTasks(
to_enable={'some-repo'}, to_block={'ext-blocked-1', 'ext-blocked-2'}, custom=set()
@@ -246,10 +238,6 @@ def test_compute_blocklist_merges_internal_and_external(monkeypatch):
assert len(blocklisted_msgs) == 1
assert set(blocklisted_msgs[0].repoids) == full_blocklist
- blacklisted_msgs = _get_produced_blacklisted()
- assert len(blacklisted_msgs) == 1
- assert set(blacklisted_msgs[0].repoids) == full_blocklist
-
def test_compute_blocklist_no_messages_when_empty(monkeypatch):
"""No blocklist messages are produced when blocklist is empty."""
@@ -263,10 +251,8 @@ def test_compute_blocklist_no_messages_when_empty(monkeypatch):
assert full_blocklist == set()
assert len(_get_produced_blocklisted()) == 0
- assert len(_get_produced_blacklisted()) == 0
-@suppress_deprecation(RepositoriesBlacklisted)
def test_compute_blocklist_only_internal(monkeypatch):
"""Only internal blocklist when no external tasks are present."""
external_tasks = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
@@ -296,7 +282,6 @@ def test_compute_blocklist_aggregated_external_tasks(monkeypatch):
assert full_blocklist == {'ext-1', 'ext-2', 'ext-3'}
-@suppress_deprecation(RepositoriesBlacklisted)
def test_compute_blocklist_overlapping_internal_and_external(monkeypatch):
"""Overlapping repoids between internal and external blocklists are deduplicated."""
external_tasks = ExternalRepoSetupTasks(
@@ -314,7 +299,3 @@ def test_compute_blocklist_overlapping_internal_and_external(monkeypatch):
blocklisted_msgs = _get_produced_blocklisted()
assert len(blocklisted_msgs) == 1
assert set(blocklisted_msgs[0].repoids) == full_blocklist
-
- blacklisted_msgs = _get_produced_blacklisted()
- assert len(blacklisted_msgs) == 1
- assert set(blacklisted_msgs[0].repoids) == full_blocklist
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 98f19ea8..97aaad33 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -7,11 +7,13 @@ from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
CustomTargetRepository,
+ RepositoriesBlacklisted,
RepositoriesFacts,
RepositoriesSetupTasks,
RepositoryData,
RepositoryFile
)
+from leapp.utils.deprecation import suppress_deprecation
def test_process_orchestration(monkeypatch):
@@ -165,3 +167,37 @@ def test_inputdata_warns_on_duplicate_repofacts(monkeypatch):
assert indata.enabled_repoids == {'repo-a'}
assert any('more than one' in msg for msg in api.current_logger.warnmsg)
+
+
+@suppress_deprecation(RepositoriesBlacklisted)
+def test_inputdata_consumes_deprecated_blacklisted(monkeypatch):
+ """RepositoriesBlacklisted repoids are added to external_tasks.to_block."""
+ repo_facts = _make_repo_facts(repoids_enabled=['repo-a'])
+ blacklisted = RepositoriesBlacklisted(repoids=['bl-1', 'bl-2'])
+
+ monkeypatch.setattr(
+ api, 'current_actor',
+ CurrentActorMocked(msgs=[repo_facts, blacklisted])
+ )
+
+ indata = InputData()
+
+ assert indata.external_tasks.to_block == {'bl-1', 'bl-2'}
+
+
+@suppress_deprecation(RepositoriesBlacklisted)
+def test_inputdata_merges_blacklisted_with_setup_tasks(monkeypatch):
+ """RepositoriesBlacklisted repoids are merged with RepositoriesSetupTasks.to_block."""
+ repo_facts = _make_repo_facts(repoids_enabled=['repo-a'])
+ setup_tasks = RepositoriesSetupTasks(to_enable=['en-1'], to_block=['bl-from-setup'])
+ blacklisted = RepositoriesBlacklisted(repoids=['bl-from-deprecated'])
+
+ monkeypatch.setattr(
+ api, 'current_actor',
+ CurrentActorMocked(msgs=[repo_facts, setup_tasks, blacklisted])
+ )
+
+ indata = InputData()
+
+ assert indata.external_tasks.to_block == {'bl-from-setup', 'bl-from-deprecated'}
+ assert indata.external_tasks.to_enable == {'en-1'}
diff --git a/repos/system_upgrade/common/models/repositoriesblocklisted.py b/repos/system_upgrade/common/models/repositoriesblocklisted.py
index c1825ec3..95fbd2dc 100644
--- a/repos/system_upgrade/common/models/repositoriesblocklisted.py
+++ b/repos/system_upgrade/common/models/repositoriesblocklisted.py
@@ -23,12 +23,14 @@ class RepositoriesBlocklisted(Model):
since='2026-06-01',
message=(
'This model has been deprecated and replaced. '
- 'To get the list of blocklisted repositories, use RepositoriesBlocklisted. '
- 'To request a repository to be blocklisted, use RepositoriesSetupTasks.blocklist.'
+ 'To get the list of blocklisted repositories, consume RepositoriesBlocklisted. '
+ 'To request a repository to be blocklisted, produce RepositoriesSetupTasks.to_block.'
),
)
class RepositoriesBlacklisted(RepositoriesBlocklisted):
"""
- Repository IDs that should be ignored by Leapp during the upgrade process.
+ Specify list of repository IDs that should be blocked during the upgrade.
+
+ Note this is deprecated and you should use RepositoriesSetupTasks.to_block
"""
topic = SystemFactsTopic
--
2.54.0

View File

@ -0,0 +1,786 @@
From 6d588f44f726eb2e16b15498caa1fc2887faccec Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sat, 6 Jun 2026 06:52:09 +0200
Subject: [PATCH 100/105] [4/9] targetcontentresolver: repositoriesblacklist:
redesign
* The library does not consume any messages anymore:
* RepositoriesFacts were consumed to obtain information about
enabled repositories on the source system. This information is now
propagated - given to the entry `compute_blocklist()` function
as `enabled_repoids` parameter
* ConsumeTargetRepository is not needed to consume anymore as the
information is included inside external_tasks.
* Simplify the logig around "internal" (or implicit) blocklisting.
The original code has been written in more generic way as a relict
from IPU 7 -> 8 time, where "Optional" repositories existed
and CRB repositories have been newly introduced. Nowadays, we have
just CRB repositories and it's clear that only CRB repositories
will need still this special handling. So the solution has been
adjusted to CRB repositories only and all related code and comments
have been updated.
* Specify the priority order when processing implicit/internal (CRB),
external, and explicit customer requests. The order is following:
internal (CRB) < external enable rq < external block rq < explicit user request
* Work with sets instead of lists everywhere. This reduced amount
of code by another 25% when keeping it simple.
* Filter out irrelevant CRB repositories from the report.
Originally the report about blocklisted CRB repositories included
all target CRB repoids - for all architectures and distributions.
Include only repoids related to the current architurecture.
Another filtering is planned to be applied later in the repomapping
itself, so only relevant data will be provided.
* Update generated reports.
* Remove the "Using repository not supported by Red Hat" report.
It was inconsistent (printed only when a CRB repo has not been
enabled on the source system and user enabled it manually for the
upgrade process only. Also the report does not make sense for any
other (non-RHEL) distro. Discussing it with PO, we agreed on the
removal of the report.
* Add the stable Key to the exclusion report
* Adjust the logic around implicit CRB repositories blocking.
* Do not blocklist any CRB repositories implicitly when any of them
is enabled by user manually. The behaviour is now consistent with
the one when a CRB repository is enabled on the source system.
Having a different behaviour to these both cases did not make a sense.
* Additionally blocks CRB repositories only on RHEL systems,
as other distros are not supported by us directly so the report
does not make a sense.
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/repositoriesblocklist.py | 177 +++++------
.../libraries/targetcontentresolver.py | 6 +-
.../tests/test_repositoriesblocklist.py | 280 ++++++++++--------
.../tests/test_targetcontentresolver.py | 3 +-
.../common/models/repositoriessetuptasks.py | 7 +
5 files changed, 245 insertions(+), 228 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
index f92f64fb..e526bcbd 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
@@ -1,28 +1,8 @@
from leapp import reporting
+from leapp.libraries.common.config import get_target_distro_id
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api
-from leapp.models import CustomTargetRepository, RepositoriesBlocklisted, RepositoriesFacts
-
-# {OS_MAJOR_VERSION: PESID}
-UNSUPPORTED_PESIDS = {
- "8": "rhel8-CRB",
- "9": "rhel9-CRB",
- "10": "rhel10-CRB"
-}
-
-
-def _report_using_unsupported_repos(repos):
- report = [
- reporting.Title('Using repository not supported by Red Hat'),
- reporting.Summary(
- 'The following repositories have been used for the '
- 'upgrade, but they are not supported by the Red Hat.:\n'
- '- {}'.format('\n - '.join(repos))
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.REPOSITORY]),
- ]
- reporting.create_report(report)
+from leapp.models import RepositoriesBlocklisted
def _report_excluded_repos(repos):
@@ -47,125 +27,110 @@ def _report_excluded_repos(repos):
' as an argument (the option can be used multiple times).'
)
),
+ reporting.Key('1b9132cb2362ae7830e48eee7811be9527747de8')
]
reporting.create_report(report)
-def _get_manually_enabled_repos():
+def _get_crb_repos(repo_mapping, major_version):
"""
- Get a set of repositories (repoids) that are manually enabled.
-
- manually enabled means (
- specified by --enablerepo option of the leapp command or
- inside the /etc/leapp/files/leapp_upgrade_repositories.repo),
- )
- :rtype: set [repoid]
- """
- try:
- return {repo.repoid for repo in api.consume(CustomTargetRepository)}
- except StopIteration:
- return set()
+ Return set of relevant CRB repoids for the specified major version
+ Only CRB repositories for the current architecture and distribution are
+ relevant.
-def _get_pesid_repos(repo_mapping, pesid, major_version):
- """
- Returns a list of pesid repos with the specified pesid and major version.
-
- :param str pesid: The PES ID representing the family of repositories.
- :param str major_version: The major version of the RHEL OS.
+ :param repo_mapping: RepositoriesMapping message.
+ :type repo_mapping: RepositoriesMapping
+ :param major_version: The OS major version.
+ :type major_version: str
:returns: A set of repoids with the specified pesid and major version.
- :rtype: List[PESIDRepositoryEntry]
+ :rtype: Set[str]
"""
- pesid_repos = []
+ pesid = f'rhel{major_version}-CRB'
+ curr_arch = api.current_actor().configuration.architecture
+ crb_repoids = set()
for pesid_repo in repo_mapping.repositories:
+ if pesid_repo.major_version != major_version or pesid_repo.arch != curr_arch:
+ # irrelevant repository
+ continue
if pesid_repo.pesid == pesid and pesid_repo.major_version == major_version:
- pesid_repos.append(pesid_repo)
- return pesid_repos
-
-
-def _get_repoids_to_exclude(repo_mapping):
- """
- Returns a set of repoids that should be blocklisted on the target system.
-
- :param RepositoriesMapping repo_mapping: Repository mapping data.
- :returns: A set of repoids to blocklist on the target system.
- :rtype: Set[str]
- """
- pesid_repos_to_exclude = _get_pesid_repos(repo_mapping,
- UNSUPPORTED_PESIDS[get_target_major_version()],
- get_target_major_version())
- return {pesid_repo.repoid for pesid_repo in pesid_repos_to_exclude}
+ crb_repoids.add(pesid_repo.repoid)
+ return crb_repoids
-def _are_optional_repos_disabled(repo_mapping, repos_on_system):
+def _are_crb_repos_disabled(repo_mapping, enabled_repoids):
"""
- Checks whether all optional repositories are disabled.
+ Checks whether all CRB repositories are disabled.
- :param RepositoriesMapping repo_mapping: Repository mapping data.
- :param RepositoriesFacts repos_on_system: Installed repositories on the source system.
- :returns: True if there are any optional repositories enabled on the source system.
+ :param repo_mapping: RepositoriesMapping message.
+ :type repo_mapping: RepositoriesMapping
+ :param enabled_repoids: Set of repoids enabled on the source system.
+ :type enabled_repoids: Set[str]
+ :returns: False if any CRB repositories are enabled on the source system, True otherwise.
+ :rtype: bool
"""
+ return enabled_repoids.isdisjoint(_get_crb_repos(repo_mapping, get_source_major_version()))
- # Get a set of all repo_ids that are optional
- optional_pesid_repos = _get_pesid_repos(repo_mapping,
- UNSUPPORTED_PESIDS[get_source_major_version()],
- get_source_major_version())
-
- optional_repoids = [optional_pesid_repo.repoid for optional_pesid_repo in optional_pesid_repos]
- # Gather all optional repositories on the source system that are not enabled
- for repofile in repos_on_system.repositories:
- for repository in repofile.data:
- if repository.repoid in optional_repoids and repository.enabled:
- return False
- return True
-
-
-def get_blocklisted_repoids(repo_mapping):
+def _calc_internal_blocklist(repo_mapping, external_tasks, enabled_repoids):
"""
- Exclude target repositories provided by Red Hat without support.
+ Calculate the internal blocklist of target CRB repositories.
Conditions to exclude:
- - there are not such repositories already enabled on the source system
- (e.g. "CRB" repositories)
- - such repositories are not required for the upgrade explicitly by the user
+ - no CRB repositories are enabled on the source system
+ - repository is not explicitly configured by user to be used during the upgrade
(e.g. via the --enablerepo option or via the /etc/leapp/files/leapp_upgrade_repositories.repo file)
- E.g. CRB repository is provided by Red Hat but it is without the support.
+ Also report explicitly enabled and blocklisted CRB repositories, unless
+ CRB is already already enabled on the source system.
:param repo_mapping: RepositoriesMapping message.
- :returns: Set of repoids to blocklist on the target system.
- :rtype: set
- """
- # TODO(pstodulk): replace it by enabled_repos
- repos_facts = next(api.consume(RepositoriesFacts), None)
+ :type repo_mapping: RepositoriesMapping
+ :param external_tasks: External repositories tasks represented by object with following fields:
- if not _are_optional_repos_disabled(repo_mapping, repos_facts):
- # nothing to do - an optional repository is enabled
+ * ``to_enable`` - repositories that should be enabled
+ * ``to_block`` - repositories that should be blocklisted
+ * ``custom`` - repositories explicitly requested by user to be used during the upgrade
+
+ :type external_tasks: targetcontentresolver.ExternalRepoSetupTasks
+ :param enabled_repoids: Set of repoids enabled on the source system.
+ :type enabled_repoids: Set[str]
+ :returns: Set of CRB repoids to blocklist on the target system.
+ :rtype: Set[str]
+ """
+ if not _are_crb_repos_disabled(repo_mapping, enabled_repoids):
+ # nothing to do - a CRB repo is enabled
return set()
- repos_to_exclude = _get_repoids_to_exclude(repo_mapping)
+ repos_to_exclude = _get_crb_repos(repo_mapping, get_target_major_version())
- # Do not exclude repos manually enabled from the CLI
- manually_enabled_repos = _get_manually_enabled_repos() & repos_to_exclude
- filtered_repos_to_exclude = repos_to_exclude - manually_enabled_repos
+ # Do not exclude repositories explicitly required by user for the upgrade
+ manually_enabled_repos = external_tasks.custom & repos_to_exclude
if manually_enabled_repos:
- _report_using_unsupported_repos(manually_enabled_repos)
- if filtered_repos_to_exclude:
- _report_excluded_repos(filtered_repos_to_exclude)
+ # NOTE(pstodulk): Same like in case that a CRB repo is enabled on src OS
+ return set()
+
+ if get_target_distro_id() == 'rhel':
+ # TODO(pstodulk): unify reports about blocklisted repos and effect on
+ # rpms tasks in pes_events_scanner
+ _report_excluded_repos(repos_to_exclude)
- return filtered_repos_to_exclude
+ return repos_to_exclude
-def compute_blocklist(repo_mapping, external_tasks):
+def compute_blocklist(repo_mapping, external_tasks, enabled_repoids):
"""
Create the blocklist of repositories that should be blocked during the upgrade.
+ Additional effects:
+ * Once the list is calculated the RepositoriesBlocklisted message is produced.
+ * Generate reports related CRB repositories
+
The content in the CRB repository is considered as unsupported. Hence
we do not want to enable such a repository by default on the target system
unless
- * it is explicitely requested, or
+ * it is explicitly requested, or
* it is already enabled on the source system
This is important as some functionality originally supported on the source
source system can be moved to the CRB repository on the target system.
@@ -180,25 +145,25 @@ def compute_blocklist(repo_mapping, external_tasks):
The external custom request has the highest priority as this comes from
the system configuration or from the `--enablerepo` option used when executing
- leapp - so it's understood as decision made by user explicitely for
+ leapp - so it's understood as decision made by user explicitly for
the in-place upgrade purpose. Currently there is no explicit custom
configuration to disable a repo.
- Once the list is calculated the RepositoriesBlocklisted message is produced.
-
:param repo_mapping: RepositoriesMapping message.
:type repo_mapping: RepositoriesMapping
:param external_tasks: External repositories tasks represented by object with following fields:
* ``to_enable`` - repositories that should be enabled
* ``to_block`` - repositories that should be blocklisted
- * ``custom`` - repositories explicitely requested by user to be used during the upgrade
+ * ``custom`` - repositories explicitly requested by user to be used during the upgrade
:type external_tasks: targetcontentresolver.ExternalRepoSetupTasks
+ :param enabled_repoids: Set of repoids enabled on the source system.
+ :type enabled_repoids: Set[str]
:returns: List of blocklisted repositories
:rtype: List[str]
"""
- internal_blocklist = get_blocklisted_repoids(repo_mapping)
+ internal_blocklist = _calc_internal_blocklist(repo_mapping, external_tasks, enabled_repoids)
full_blocklist = internal_blocklist.union(external_tasks.to_block)
if full_blocklist:
api.produce(RepositoriesBlocklisted(repoids=sorted(full_blocklist)))
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index e6885e44..e70346b0 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -109,7 +109,11 @@ def process():
"""
indata = InputData()
repositories_map_msg = scan_repositories()
- blocklisted_repoids = repositoriesblocklist.compute_blocklist(repositories_map_msg, indata.external_tasks)
+ blocklisted_repoids = repositoriesblocklist.compute_blocklist(
+ repositories_map_msg,
+ indata.external_tasks,
+ indata.enabled_repoids
+ )
pes_requested_repoids = pes_events_scanner.scan_pes_events(
repositories_map_msg,
blocklisted_repoids,
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
index ce72dc9e..843c8f74 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
@@ -5,31 +5,9 @@ from leapp.libraries.actor import repositoriesblocklist
from leapp.libraries.actor.targetcontentresolver import ExternalRepoSetupTasks
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
-from leapp.models import (
- CustomTargetRepository,
- PESIDRepositoryEntry,
- RepoMapEntry,
- RepositoriesBlocklisted,
- RepositoriesFacts,
- RepositoriesMapping,
- RepositoryData,
- RepositoryFile
-)
+from leapp.models import PESIDRepositoryEntry, RepoMapEntry, RepositoriesBlocklisted, RepositoriesMapping
-
-@pytest.fixture
-def repofacts_opts_disabled():
- repos_data = [
- RepositoryData(
- repoid="codeready-builder-for-rhel-8-x86_64-rpms",
- name="RHEL 8 CRB",
- enabled=False,
- )
- ]
- repos_files = [
- RepositoryFile(file="/etc/yum.repos.d/redhat.repo", data=repos_data)
- ]
- return RepositoriesFacts(repositories=repos_files)
+_NO_TASKS = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
@pytest.fixture
@@ -68,149 +46,211 @@ def repomap_opts_only(rhel8_crb_pesidrepo, rhel9_crb_pesidrepo):
)
-def test_all_target_optionals_blacklisted_when_no_optional_on_source(monkeypatch, repomap_opts_only):
+def test_crb_repos_blocked_when_no_crb_on_source(monkeypatch, repomap_opts_only):
"""
- Tests whether every target optional repository gets blacklisted
- if no optional repositories are used on the source system.
+ Target CRB repos are blocklisted when no CRB repository
+ is enabled on the source system.
"""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- repos_data = [
- RepositoryData(
- repoid="rhel-8-server-rpms",
- name="RHEL 8 Server",
- enabled=True,
- )
- ]
- repos_files = [
- RepositoryFile(file="/etc/yum.repos.d/redhat.repo", data=repos_data)
- ]
- repo_facts = RepositoriesFacts(repositories=repos_files)
-
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts]))
- monkeypatch.setattr(api, 'produce', produce_mocked())
- monkeypatch.setattr(reporting, 'create_report', produce_mocked())
-
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids={'rhel-8-server-rpms'}
+ )
assert 'codeready-builder-for-rhel-9-x86_64-rpms' in result
-def test_with_no_mapping_for_optional_repos(monkeypatch, repomap_opts_only, repofacts_opts_disabled):
+def test_empty_result_when_no_crb_pesid_in_mapping(monkeypatch, repomap_opts_only):
"""
- Tests whether an empty set is returned if no valid target is found for an optional repository in mapping data.
+ Empty set is returned if no valid CRB target is found in mapping data.
"""
-
repomap_opts_only.repositories[1].pesid = 'test_pesid'
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ )
assert not result
-def test_blacklist_produced_when_optional_repo_disabled(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
+def test_blocklist_generated_when_crb_disabled(monkeypatch, repomap_opts_only):
"""
- Tests whether a correct blacklist is generated when there is disabled optional repo on the system.
+ Blocklist is generated when CRB repos are disabled on the source system.
"""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- monkeypatch.setattr(
- api,
- "current_actor",
- CurrentActorMocked(msgs=[repofacts_opts_disabled]),
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids=set()
)
- monkeypatch.setattr(api, "produce", produce_mocked())
- monkeypatch.setattr(reporting, "create_report", produce_mocked())
-
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
- assert result, 'A blacklist should get generated.'
+ assert result, 'A blocklist should get generated.'
- expected_blacklisted_repoid = 'codeready-builder-for-rhel-9-x86_64-rpms'
- err_msg = 'Blacklist does not contain expected repoid.'
- assert expected_blacklisted_repoid in result, err_msg
+ expected_blocklisted_repoid = 'codeready-builder-for-rhel-9-x86_64-rpms'
+ err_msg = 'Blocklist does not contain expected repoid.'
+ assert expected_blocklisted_repoid in result, err_msg
-def test_no_blacklist_produced_when_optional_repo_enabled(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
+def test_no_blocklist_when_crb_enabled_on_source(monkeypatch, repomap_opts_only):
"""
- Tests whether an empty set is returned when an optional repository is enabled.
-
- Data are set up in such a fashion so that the determined blacklist would not be empty.
+ Empty set is returned when a CRB repository is enabled on the source system.
"""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
- repofacts_opts_disabled.repositories[0].data[0].enabled = True
-
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled]))
-
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS,
+ enabled_repoids={'codeready-builder-for-rhel-8-x86_64-rpms'}
+ )
assert not result
-def test_repositoriesblacklist_not_empty(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
+def test_blocklist_not_empty_with_mocked_crb_repos(monkeypatch, repomap_opts_only):
"""
- Tests whether the correct set of blacklisted repoids is returned.
+ Non-empty blocklist is returned when _get_crb_repos returns repos to exclude.
"""
-
blacklisted_repoid = 'test'
- monkeypatch.setattr(repositoriesblocklist, "_get_repoids_to_exclude", lambda dummy_mapping: {blacklisted_repoid})
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled]))
- monkeypatch.setattr(api, "produce", produce_mocked())
- monkeypatch.setattr(reporting, "create_report", produce_mocked())
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(
+ repositoriesblocklist, '_are_crb_repos_disabled', lambda _m, _e: True
+ )
+ monkeypatch.setattr(
+ repositoriesblocklist, '_get_crb_repos', lambda _m, _v: {blacklisted_repoid}
+ )
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ )
assert blacklisted_repoid in result
assert reporting.create_report.called == 1
-def test_repositoriesblacklist_empty(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
+def test_blocklist_empty_with_mocked_empty_crb_repos(monkeypatch, repomap_opts_only):
"""
- Tests whether an empty set is returned if there are some disabled optional
- repos, but an empty blacklist is determined from the repo mapping data.
+ Empty set returned when _get_crb_repos returns no repos, even though
+ CRB is disabled on the source.
"""
-
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(
+ repositoriesblocklist, '_are_crb_repos_disabled', lambda _m, _e: True
+ )
monkeypatch.setattr(
- repositoriesblocklist,
- "_get_repoids_to_exclude",
- lambda dummy_mapping: set()
+ repositoriesblocklist, '_get_crb_repos', lambda _m, _v: set()
)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ )
assert not result
@pytest.mark.parametrize(
- ("enabled_repo", "exp_report_title", "message_produced"),
+ ('custom_repoids', 'exp_report_title', 'exp_blocklist_empty'),
[
- ("codeready-builder-for-rhel-9-x86_64-rpms", "Using repository not supported by Red Hat", False),
- ("some_other_enabled_repo", "Excluded target system repositories", True),
- (None, "Excluded target system repositories", True),
+ (
+ {'codeready-builder-for-rhel-9-x86_64-rpms'},
+ None,
+ True,
+ ),
+ (
+ {'some_other_enabled_repo'},
+ 'Excluded target system repositories',
+ False,
+ ),
+ (
+ set(),
+ 'Excluded target system repositories',
+ False,
+ ),
],
)
-def test_enablerepo_option(monkeypatch,
- repofacts_opts_disabled,
- repomap_opts_only,
- enabled_repo,
- exp_report_title,
- message_produced):
+def test_custom_enablerepo_effect(monkeypatch, repomap_opts_only,
+ custom_repoids, exp_report_title,
+ exp_blocklist_empty):
"""
- Tests whether the actor respects CustomTargetRepository messages when constructing the blacklist.
+ When a CRB repo is in external_tasks.custom the blocklist is empty.
+ When custom contains only non-CRB repos or is empty, CRB repos are excluded and reported.
"""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- msgs_to_feed = [repofacts_opts_disabled]
+ external_tasks = ExternalRepoSetupTasks(
+ to_enable=set(), to_block=set(), custom=custom_repoids
+ )
- if enabled_repo:
- msgs_to_feed.append(CustomTargetRepository(repoid=enabled_repo))
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs_to_feed))
- monkeypatch.setattr(api, 'produce', produce_mocked())
- monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- result = repositoriesblocklist.get_blocklisted_repoids(repomap_opts_only)
- assert reporting.create_report.report_fields["title"] == exp_report_title
- if message_produced:
- assert result
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, external_tasks, enabled_repoids=set()
+ )
+
+ if exp_report_title:
+ assert reporting.create_report.report_fields['title'] == exp_report_title
else:
+ assert reporting.create_report.called == 0
+
+ if exp_blocklist_empty:
assert not result
+ else:
+ assert result
+
+
+def test_get_crb_repos_filters_by_architecture(monkeypatch):
+ """Only CRB repos matching the current architecture are returned."""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
+ ))
+
+ repos = [
+ PESIDRepositoryEntry(
+ pesid='rhel9-CRB', major_version='9',
+ repoid='crb-x86_64', rhui='', arch='x86_64',
+ channel='ga', repo_type='rpm', distro='rhel',
+ ),
+ PESIDRepositoryEntry(
+ pesid='rhel9-CRB', major_version='9',
+ repoid='crb-aarch64', rhui='', arch='aarch64',
+ channel='ga', repo_type='rpm', distro='rhel',
+ ),
+ ]
+ repo_mapping = RepositoriesMapping(mapping=[], repositories=repos)
+
+ result = repositoriesblocklist._get_crb_repos(repo_mapping, '9')
+
+ assert result == {'crb-x86_64'}
+
+
+def test_no_report_for_non_rhel_distro(monkeypatch, repomap_opts_only):
+ """No exclusion report generated for non-RHEL distros, but repos are still excluded."""
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='centos'
+ ))
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ result = repositoriesblocklist._calc_internal_blocklist(
+ repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ )
+
+ assert result
+ assert reporting.create_report.called == 0
def _get_produced_blocklisted():
@@ -226,11 +266,11 @@ def test_compute_blocklist_merges_internal_and_external(monkeypatch):
to_enable={'some-repo'}, to_block={'ext-blocked-1', 'ext-blocked-2'}, custom=set()
)
monkeypatch.setattr(
- repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'crb-repo-1'}
+ repositoriesblocklist, '_calc_internal_blocklist', lambda _rm, _et, _er: {'crb-repo-1'}
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks, set())
assert full_blocklist == {'crb-repo-1', 'ext-blocked-1', 'ext-blocked-2'}
@@ -243,11 +283,11 @@ def test_compute_blocklist_no_messages_when_empty(monkeypatch):
"""No blocklist messages are produced when blocklist is empty."""
external_tasks = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
monkeypatch.setattr(
- repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: set()
+ repositoriesblocklist, '_calc_internal_blocklist', lambda _rm, _et, _er: set()
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks, set())
assert full_blocklist == set()
assert len(_get_produced_blocklisted()) == 0
@@ -257,11 +297,11 @@ def test_compute_blocklist_only_internal(monkeypatch):
"""Only internal blocklist when no external tasks are present."""
external_tasks = ExternalRepoSetupTasks(to_enable=set(), to_block=set(), custom=set())
monkeypatch.setattr(
- repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'crb-repo-1'}
+ repositoriesblocklist, '_calc_internal_blocklist', lambda _rm, _et, _er: {'crb-repo-1'}
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks, set())
assert full_blocklist == {'crb-repo-1'}
assert len(_get_produced_blocklisted()) == 1
@@ -273,11 +313,11 @@ def test_compute_blocklist_aggregated_external_tasks(monkeypatch):
to_enable={'repo-a', 'repo-b'}, to_block={'ext-1', 'ext-2', 'ext-3'}, custom=set()
)
monkeypatch.setattr(
- repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: set()
+ repositoriesblocklist, '_calc_internal_blocklist', lambda _rm, _et, _er: set()
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks, set())
assert full_blocklist == {'ext-1', 'ext-2', 'ext-3'}
@@ -288,11 +328,11 @@ def test_compute_blocklist_overlapping_internal_and_external(monkeypatch):
to_enable=set(), to_block={'shared-repo', 'ext-only'}, custom=set()
)
monkeypatch.setattr(
- repositoriesblocklist, 'get_blocklisted_repoids', lambda _repo_map: {'shared-repo', 'internal-only'}
+ repositoriesblocklist, '_calc_internal_blocklist', lambda _rm, _et, _er: {'shared-repo', 'internal-only'}
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks)
+ full_blocklist = repositoriesblocklist.compute_blocklist(None, external_tasks, set())
assert full_blocklist == {'shared-repo', 'internal-only', 'ext-only'}
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 97aaad33..0f28d656 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -41,10 +41,11 @@ def test_process_orchestration(monkeypatch):
call_log.append('scan_repositories')
return fake_repo_map
- def mock_compute_blocklist(repo_mapping, external_tasks):
+ def mock_compute_blocklist(repo_mapping, external_tasks, enabled_repoids):
call_log.append('compute_blocklist')
assert repo_mapping is fake_repo_map
assert external_tasks is fake_external_tasks
+ assert enabled_repoids is fake_enabled_repoids
return fake_blocklist
def mock_scan_pes_events(repo_mapping, blacklisted_repoids, enabled_repoids):
diff --git a/repos/system_upgrade/common/models/repositoriessetuptasks.py b/repos/system_upgrade/common/models/repositoriessetuptasks.py
index 35f627e4..88056d97 100644
--- a/repos/system_upgrade/common/models/repositoriessetuptasks.py
+++ b/repos/system_upgrade/common/models/repositoriessetuptasks.py
@@ -10,6 +10,13 @@ class RepositoriesSetupTasks(Model):
upgrade process. This information should be processed by an actor dedicated to manage
repositories.
* 'to_block' field consists of a list of repositories that should be ignored during upgrade process.
+
+ The priority order of the requests is following:
+ to_enable < to_block < "external custom enablement request"
+ The external custom request can be made by user:
+
+ * execute leapp with --enablerepo option
+ * using configuration files
"""
topic = SystemFactsTopic
--
2.54.0

View File

@ -0,0 +1,288 @@
From 614a7f3d793ce1d47b7360a4133f2e605cdbe8a3 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sat, 6 Jun 2026 12:30:55 +0200
Subject: [PATCH 101/105] [5/9] repositoriesmapping: scan_repositories ->
load_repositories_mapping
The original `scan_repositories()` name was confusing and it did not
reflect the purpose of the function. The new name
`load_repositories_mapping()` seems to be more intuitive.
Also dropped obsolete check for the "repomap.csv" file. The file existed
only on RHEL 7, more than 3 years ago. So it's a good time to remove
this relict from the past.
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/repomap_loader.py | 31 ++++++--------
.../libraries/targetcontentresolver.py | 4 +-
.../tests/test_targetcontentresolver.py | 12 +++---
.../tests/unit_test_repomap_loader.py | 40 +++++++++----------
4 files changed, 41 insertions(+), 46 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repomap_loader.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repomap_loader.py
index 52b8337d..81716f01 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repomap_loader.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repomap_loader.py
@@ -9,9 +9,6 @@ from leapp.libraries.stdlib import api
from leapp.models import PESIDRepositoryEntry, RepoMapEntry, RepositoriesMapping
from leapp.models.fields import ModelViolationError
-OLD_REPOMAP_FILE = 'repomap.csv'
-"""The name of the old, deprecated repository mapping file (no longer used)."""
-
REPOMAP_FILE = 'repomap.json'
"""The name of the new repository mapping file."""
@@ -159,23 +156,21 @@ def _read_repofile(repofile):
return repofile_data
-def scan_repositories(read_repofile_func=_read_repofile):
+def load_repositories_mapping(read_repofile_func=_read_repofile):
"""
- Scan the repository mapping file and produce RepositoriesMap msg.
+ Load the mapping of DNF repositories related to the current upgrade path.
+
+ Read the mapping from the repomap.json file and filter out data irrelevant
+ for the current and target system major version.
+
+ Produce and return the RepositoriesMapping message.
- See the description of the actor for more details.
+ :param read_repofile_func: The function that accept filename string to read the repomap file and return dict
+ :type read_repofile_func: Callable[[str], Dict]
+ :rtype: RepositoriesMapping
+ :raises StopActorExecutionError: When cannot produce valid RepositoriesMapping
"""
# TODO: add filter based on the current arch
- # TODO: deprecate the product type and introduce the "channels" ?.. more or less
- # NOTE: product type is changed, now it's channel: eus,e4s,aus,tus,ga,beta
-
- if os.path.exists(os.path.join('/etc/leapp/files', OLD_REPOMAP_FILE)):
- # NOTE: what about creating the report (instead of warning)
- api.current_logger().warning(
- 'The old repomap file /etc/leapp/files/repomap.csv is present.'
- ' The file has been replaced by the repomap.json file and it is'
- ' not used anymore.'
- )
json_data = read_repofile_func(REPOMAP_FILE)
try:
@@ -188,7 +183,6 @@ def scan_repositories(read_repofile_func=_read_repofile):
repositories=repomap_data.get_repositories(valid_major_versions)
)
api.produce(repositories_mapping)
- return repositories_mapping
except ModelViolationError as err:
err_message = (
'The repository mapping file is invalid: '
@@ -202,4 +196,5 @@ def scan_repositories(read_repofile_func=_read_repofile):
except ValueError as err:
# The error should contain enough information, so we do not need to clarify it further
_inhibit_upgrade('The repository mapping file is invalid: {}'.format(err))
- return None
+
+ return repositories_mapping
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index e70346b0..b66168e6 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -2,7 +2,7 @@ from collections import namedtuple
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import pes_events_scanner, repositoriesblocklist, setuptargetrepos
-from leapp.libraries.actor.repomap_loader import scan_repositories
+from leapp.libraries.actor.repomap_loader import load_repositories_mapping
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesFacts, RepositoriesSetupTasks
from leapp.utils.deprecation import suppress_deprecation
@@ -108,7 +108,7 @@ def process():
configuration.
"""
indata = InputData()
- repositories_map_msg = scan_repositories()
+ repositories_map_msg = load_repositories_mapping()
blocklisted_repoids = repositoriesblocklist.compute_blocklist(
repositories_map_msg,
indata.external_tasks,
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 0f28d656..970fd797 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -19,7 +19,7 @@ from leapp.utils.deprecation import suppress_deprecation
def test_process_orchestration(monkeypatch):
"""
Tests that process() wires data between the four stages correctly:
- scan_repositories -> compute_blocklist -> scan_pes_events -> setup_target_repos.
+ load_repositories_mapping -> compute_blocklist -> scan_pes_events -> setup_target_repos.
"""
call_log = []
@@ -37,8 +37,8 @@ def test_process_orchestration(monkeypatch):
self.external_tasks = fake_external_tasks
self.enabled_repoids = fake_enabled_repoids
- def mock_scan_repositories():
- call_log.append('scan_repositories')
+ def mock_load_repositories_mapping():
+ call_log.append('load_repositories_mapping')
return fake_repo_map
def mock_compute_blocklist(repo_mapping, external_tasks, enabled_repoids):
@@ -68,8 +68,8 @@ def test_process_orchestration(monkeypatch):
FakeInputData,
)
monkeypatch.setattr(
- 'leapp.libraries.actor.targetcontentresolver.scan_repositories',
- mock_scan_repositories,
+ 'leapp.libraries.actor.targetcontentresolver.load_repositories_mapping',
+ mock_load_repositories_mapping,
)
monkeypatch.setattr(
'leapp.libraries.actor.targetcontentresolver.repositoriesblocklist.compute_blocklist',
@@ -88,7 +88,7 @@ def test_process_orchestration(monkeypatch):
assert call_log == [
'InputData',
- 'scan_repositories',
+ 'load_repositories_mapping',
'compute_blocklist',
'scan_pes_events',
'setup_target_repos',
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/unit_test_repomap_loader.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/unit_test_repomap_loader.py
index 50cf5962..be9b3bb2 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/unit_test_repomap_loader.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/unit_test_repomap_loader.py
@@ -32,10 +32,10 @@ def test_scan_existing_valid_data(monkeypatch, adjust_cwd):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
monkeypatch.setattr(api, 'produce', produce_mocked())
- result = repomap_loader.scan_repositories(lambda dummy: data)
+ result = repomap_loader.load_repositories_mapping(lambda dummy: data)
assert api.produce.called, 'Actor did not produce any message when deserializing valid repomap data.'
- assert result is not None, 'scan_repositories should return the RepositoriesMapping message.'
+ assert result is not None, 'load_repositories_mapping should return the RepositoriesMapping message.'
assert result is api.produce.model_instances[0], (
'The returned mapping should be the same object that was produced.'
)
@@ -126,9 +126,9 @@ def test_scan_existing_valid_data(monkeypatch, adjust_cwd):
assert expected_pesid_repo in pesid_repos, fail_description
-def test_scan_repositories_with_missing_data(monkeypatch):
+def test_load_repositories_mapping_with_missing_data(monkeypatch):
"""
- Tests whether the scanning process fails gracefully when no data are read.
+ Tests whether the loading process fails gracefully when no data are read.
"""
mocked_actor = CurrentActorMocked(src_ver='7.9', dst_ver='8.4', msgs=[])
@@ -144,25 +144,25 @@ def test_scan_repositories_with_missing_data(monkeypatch):
monkeypatch.setattr(fetch, 'read_or_fetch', read_or_fetch_mocked)
with pytest.raises(StopActorExecutionError) as missing_data_error:
- repomap_loader.scan_repositories()
+ repomap_loader.load_repositories_mapping()
assert 'does not contain a valid JSON object' in str(missing_data_error)
-def test_scan_repositories_with_empty_data(monkeypatch):
+def test_load_repositories_mapping_with_empty_data(monkeypatch):
"""
- Tests whether the scanning process fails gracefully when empty json data received.
+ Tests whether the loading process fails gracefully when empty json data received.
"""
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError) as empty_data_error:
- repomap_loader.scan_repositories(lambda dummy: {})
+ repomap_loader.load_repositories_mapping(lambda dummy: {})
assert 'the JSON is missing a required field' in str(empty_data_error)
@pytest.mark.parametrize('version_format', ('0.0.0', '1.0.1', '1.1.0', '2.0.0'))
-def test_scan_repositories_with_bad_json_data_version(monkeypatch, version_format):
+def test_load_repositories_mapping_with_bad_json_data_version(monkeypatch, version_format):
"""
Tests whether the json data is checked for the version field and error is raised if the version
does not match the latest one.
@@ -179,12 +179,12 @@ def test_scan_repositories_with_bad_json_data_version(monkeypatch, version_forma
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError) as bad_version_error:
- repomap_loader.scan_repositories(lambda dummy: json_data)
+ repomap_loader.load_repositories_mapping(lambda dummy: json_data)
assert 'mapping file is invalid' in str(bad_version_error)
-def test_scan_repositories_with_mapping_to_pesid_without_repos(monkeypatch):
+def test_load_repositories_mapping_with_mapping_to_pesid_without_repos(monkeypatch):
"""
Tests that the loading of repositories mapping recognizes when there is a mapping with target pesid that does
not have any repositories and inhibits the upgrade.
@@ -225,12 +225,12 @@ def test_scan_repositories_with_mapping_to_pesid_without_repos(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError) as error_info:
- repomap_loader.scan_repositories(lambda dummy: json_data)
+ repomap_loader.load_repositories_mapping(lambda dummy: json_data)
assert 'pesid is not related to any repository' in error_info.value.message
-def test_scan_repositories_with_repo_entry_missing_required_fields(monkeypatch):
+def test_load_repositories_mapping_with_repo_entry_missing_required_fields(monkeypatch):
"""
Tests whether deserialization of pesid repo entries missing some of the required fields
is handled internally and StopActorExecutionError is propagated to the user.
@@ -279,12 +279,12 @@ def test_scan_repositories_with_repo_entry_missing_required_fields(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError) as error_info:
- repomap_loader.scan_repositories(lambda dummy: json_data)
+ repomap_loader.load_repositories_mapping(lambda dummy: json_data)
assert 'the JSON is missing a required field' in error_info.value.message
-def test_scan_repositories_with_repo_entry_mapping_target_not_a_list(monkeypatch):
+def test_load_repositories_mapping_with_repo_entry_mapping_target_not_a_list(monkeypatch):
"""
Tests whether deserialization of a mapping entry that has its target field set to a string
is handled internally and StopActorExecutionError is propagated to the user.
@@ -333,18 +333,18 @@ def test_scan_repositories_with_repo_entry_mapping_target_not_a_list(monkeypatch
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError) as error_info:
- repomap_loader.scan_repositories(lambda dummy: json_data)
+ repomap_loader.load_repositories_mapping(lambda dummy: json_data)
assert 'repository mapping file is invalid' in error_info.value.message
-def test_scan_repositories_returns_none_on_error(monkeypatch):
+def test_load_repositories_mapping_raises_on_invalid_data(monkeypatch):
"""
- Tests that scan_repositories returns None when the data is invalid
- and a StopActorExecutionError is raised.
+ Tests that load_repositories_mapping raises StopActorExecutionError
+ when the data is invalid.
"""
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
monkeypatch.setattr(api, 'produce', produce_mocked())
with pytest.raises(StopActorExecutionError):
- repomap_loader.scan_repositories(lambda dummy: {})
+ repomap_loader.load_repositories_mapping(lambda dummy: {})
--
2.54.0

View File

@ -0,0 +1,615 @@
From d28e4c283de9ac3e6f172c900f58f91e7cbb66d1 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sun, 7 Jun 2026 01:31:39 +0200
Subject: [PATCH 102/105] [6/9] targetcontentresolver: operate with
RepoMapDataHandler
Originally various libraries required RepositoriesMapping message
in the input parameters. Then some of them just initialized
RepoMapDataHandler (which requires to consume RHUIInfo message)
and in case of the `repositoriesblocklist` library it even duplicated
functionality implemented in the handler.
The handler is now initialized inside the orchestrator and all other
libraries accept just the handler as the input parameter. This leads
to another deduplication and simplification of the codebase.
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/pes_events_scanner.py | 27 +++++------
.../libraries/repositoriesblocklist.py | 47 +++++++++----------
.../libraries/setuptargetrepos.py | 14 ++----
.../libraries/targetcontentresolver.py | 24 ++++++++--
.../tests/test_pes_event_scanner.py | 5 +-
.../tests/test_repositoriesblocklist.py | 41 ++++++++++++----
.../tests/test_setuptargetrepos.py | 23 +++++----
.../tests/test_targetcontentresolver.py | 20 ++++----
8 files changed, 116 insertions(+), 85 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
index cbc99adf..96f26a59 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
@@ -17,7 +17,6 @@ from leapp.models import (
PESIDRepositoryEntry,
PESRpmTransactionTasks,
RepositoriesFacts,
- RHUIInfo,
RpmTransactionTasks
)
@@ -112,6 +111,7 @@ def get_relevant_releases(events):
def _get_enabled_modules():
+ # TODO(pstodulk): take a look
enabled_modules_msgs = api.consume(EnabledModules)
enabled_modules_msg = next(enabled_modules_msgs, None)
if list(enabled_modules_msgs):
@@ -357,22 +357,20 @@ def get_enabled_repoids():
return enabled_repoids
-def get_pesid_to_repoid_map(target_pesids, repositories_map_msg, enabled_repoids):
+def get_pesid_to_repoid_map(target_pesids, repomap_handler, enabled_repoids):
"""
Get a dictionary mapping all PESID repositories to their corresponding repoid.
:param target_pesids: The set of target PES IDs needed to be mapped
- :param repositories_map_msg: RepositoriesMapping message.
+ :param repomap_handler: Operator to work with the repositories mapping data
+ :type repomap_handler: repomap_calc.RepoMapDataHandler
:return: Dictionary mapping the target_pesids to their corresponding repoid
"""
- rhui_info = next(api.consume(RHUIInfo), None)
- cloud_provider = rhui_info.provider if rhui_info else ''
-
- repomap_handler = repomap_calc.RepoMapDataHandler(repositories_map_msg, cloud_provider=cloud_provider)
-
# NOTE: We have to calculate expected target repositories like in the setuptargetrepos actor.
# It's planned to handle this in different a way in future...
-
+ # TODO(pstodulk): ?? hmm..what about moving it to the orchestrator?
+ # TODO(pstodulk): what about returning requests just for PESIDS? Setuptargetrepo
+ # lib will pick the right one for us later.
default_channels = repomap_calc.get_default_repository_channels(repomap_handler, enabled_repoids)
repomap_handler.set_default_channels(default_channels)
@@ -432,7 +430,7 @@ def get_pesid_to_repoid_map(target_pesids, repositories_map_msg, enabled_repoids
return repositories_mapping
-def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repositories_map_msg, enabled_repoids):
+def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repomap_handler, enabled_repoids):
"""Replace packages with PESID in their .repository field with ones that have repoid providing the package."""
# We want to map only PESIDs - if some package had no events, it will its repository set to source system repoid
packages_with_pesid = {pkg for pkg in packages if pkg.repository not in source_pkgs_repoids}
@@ -440,7 +438,7 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repos
required_target_pesids = {pkg.repository for pkg in packages_with_pesid}
- pesid_to_target_repoid_map = get_pesid_to_repoid_map(required_target_pesids, repositories_map_msg, enabled_repoids)
+ pesid_to_target_repoid_map = get_pesid_to_repoid_map(required_target_pesids, repomap_handler, enabled_repoids)
packages_with_unknown_target_repoid = {
pkg
@@ -554,11 +552,12 @@ def include_instructions_from_transaction_configuration(rpm_tasks, transaction_c
modules_to_reset=modules_to_reset)
-def scan_pes_events(repositories_map_msg, blacklisted_repoids, enabled_repoids):
+def scan_pes_events(repomap_handler, blacklisted_repoids, enabled_repoids):
"""
Process PES events and compute RPM transaction tasks.
- :param repositories_map_msg: RepositoriesMapping message.
+ :param repomap_handler: Operator to work with the repositories mapping data
+ :type repomap_handler: repomap_calc.RepoMapDataHandler
:param blacklisted_repoids: Set of repoids to exclude from target packages. If None, an empty set is used.
:returns: Set of target repoids that need to be enabled, or None if no PES events are found.
:rtype: Optional[set]
@@ -588,7 +587,7 @@ def scan_pes_events(repositories_map_msg, blacklisted_repoids, enabled_repoids):
target_pkgs = replace_pesids_with_repoids_in_packages(
target_pkgs,
repoids_of_source_pkgs,
- repositories_map_msg,
+ repomap_handler,
enabled_repoids
)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
index e526bcbd..c77a1f13 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
@@ -32,44 +32,39 @@ def _report_excluded_repos(repos):
reporting.create_report(report)
-def _get_crb_repos(repo_mapping, major_version):
+def _get_crb_repos(repo_mapping, flag_source_os):
"""
- Return set of relevant CRB repoids for the specified major version
+ Return set of relevant CRB repoids for the source or target OS.
- Only CRB repositories for the current architecture and distribution are
- relevant.
-
- :param repo_mapping: RepositoriesMapping message.
- :type repo_mapping: RepositoriesMapping
- :param major_version: The OS major version.
- :type major_version: str
+ :param repo_mapping: Operator to work with the repositories mapping data
+ :type repo_mapping: repomap_calc.RepoMapDataHandler
+ :param flag_source_os: Set True for the source OS, False for the target OS.
+ :type flag_source_os: bool
:returns: A set of repoids with the specified pesid and major version.
:rtype: Set[str]
"""
- pesid = f'rhel{major_version}-CRB'
curr_arch = api.current_actor().configuration.architecture
- crb_repoids = set()
- for pesid_repo in repo_mapping.repositories:
- if pesid_repo.major_version != major_version or pesid_repo.arch != curr_arch:
- # irrelevant repository
- continue
- if pesid_repo.pesid == pesid and pesid_repo.major_version == major_version:
- crb_repoids.add(pesid_repo.repoid)
- return crb_repoids
+ if flag_source_os:
+ pesid = 'rhel{}-CRB'.format(get_source_major_version())
+ crb_repos = repo_mapping.get_source_pesid_repos(pesid)
+ else:
+ pesid = 'rhel{}-CRB'.format(get_target_major_version())
+ crb_repos = repo_mapping.get_target_pesid_repos(pesid)
+ return {repo.repoid for repo in crb_repos if repo.arch == curr_arch}
def _are_crb_repos_disabled(repo_mapping, enabled_repoids):
"""
Checks whether all CRB repositories are disabled.
- :param repo_mapping: RepositoriesMapping message.
- :type repo_mapping: RepositoriesMapping
+ :param repo_mapping: Operator to work with the repositories mapping data
+ :type repo_mapping: repomap_calc.RepoMapDataHandler
:param enabled_repoids: Set of repoids enabled on the source system.
:type enabled_repoids: Set[str]
:returns: False if any CRB repositories are enabled on the source system, True otherwise.
:rtype: bool
"""
- return enabled_repoids.isdisjoint(_get_crb_repos(repo_mapping, get_source_major_version()))
+ return enabled_repoids.isdisjoint(_get_crb_repos(repo_mapping, True))
def _calc_internal_blocklist(repo_mapping, external_tasks, enabled_repoids):
@@ -84,8 +79,8 @@ def _calc_internal_blocklist(repo_mapping, external_tasks, enabled_repoids):
Also report explicitly enabled and blocklisted CRB repositories, unless
CRB is already already enabled on the source system.
- :param repo_mapping: RepositoriesMapping message.
- :type repo_mapping: RepositoriesMapping
+ :param repo_mapping: Operator to work with the repositories mapping data
+ :type repo_mapping: repomap_calc.RepoMapDataHandler
:param external_tasks: External repositories tasks represented by object with following fields:
* ``to_enable`` - repositories that should be enabled
@@ -102,7 +97,7 @@ def _calc_internal_blocklist(repo_mapping, external_tasks, enabled_repoids):
# nothing to do - a CRB repo is enabled
return set()
- repos_to_exclude = _get_crb_repos(repo_mapping, get_target_major_version())
+ repos_to_exclude = _get_crb_repos(repo_mapping, False)
# Do not exclude repositories explicitly required by user for the upgrade
manually_enabled_repos = external_tasks.custom & repos_to_exclude
@@ -149,8 +144,8 @@ def compute_blocklist(repo_mapping, external_tasks, enabled_repoids):
the in-place upgrade purpose. Currently there is no explicit custom
configuration to disable a repo.
- :param repo_mapping: RepositoriesMapping message.
- :type repo_mapping: RepositoriesMapping
+ :param repo_mapping: Operator to work with the repositories mapping data
+ :type repo_mapping: repomap_calc.RepoMapDataHandler
:param external_tasks: External repositories tasks represented by object with following fields:
* ``to_enable`` - repositories that should be enabled
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
index d6bfb91d..4f7ffd9f 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
@@ -8,7 +8,6 @@ from leapp.models import (
DistroTargetRepository,
InstalledRPM,
RHELTargetRepository,
- RHUIInfo,
SkippedRepositories,
TargetRepositories,
UsedRepositories
@@ -58,12 +57,13 @@ def _get_mapped_repoids(repomap, src_repoids):
@suppress_deprecation(RHELTargetRepository)
-def setup_target_repos(repositories_map_msg, pes_requested_repoids=None,
+def setup_target_repos(repomap_handler, pes_requested_repoids=None,
blacklisted_repoids=None, external_repoids_requests=None):
"""
Determine the final list of target repositories.
- :param repositories_map_msg: RepositoriesMapping message.
+ :param repomap_handler: Operator to work with the repositories mapping data
+ :type repomap_handler: repomap_calc.RepoMapDataHandler
:param pes_requested_repoids: Set of repoids derived from PES events that need to be enabled.
:param blacklisted_repoids: Set of repoids to exclude from target repos. If None, an empty set is used.
:param external_repoids_requests: Set of repoids requested by external actors (e.g. satellite_upgrade_facts).
@@ -75,14 +75,6 @@ def setup_target_repos(repositories_map_msg, pes_requested_repoids=None,
custom_repos = _get_custom_target_repos()
repoids_from_installed_packages = _get_repoids_from_installed_packages()
- # Setup repomap handler
- repo_mapping_msg = repositories_map_msg
-
- rhui_info = next(api.consume(RHUIInfo), None)
- cloud_provider = rhui_info.provider if rhui_info else ''
-
- repomap_handler = repomap_calc.RepoMapDataHandler(repo_mapping_msg, cloud_provider=cloud_provider)
-
# Filter set of repoids from installed packages so that it contains only repoids with mapping
repoids_from_installed_packages_with_mapping = _get_mapped_repoids(
repomap_handler, repoids_from_installed_packages
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index b66168e6..942a2680 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -2,9 +2,16 @@ from collections import namedtuple
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import pes_events_scanner, repositoriesblocklist, setuptargetrepos
+from leapp.libraries.actor.repomap_calc import RepoMapDataHandler
from leapp.libraries.actor.repomap_loader import load_repositories_mapping
from leapp.libraries.stdlib import api
-from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesFacts, RepositoriesSetupTasks
+from leapp.models import (
+ CustomTargetRepository,
+ RepositoriesBlacklisted,
+ RepositoriesFacts,
+ RepositoriesSetupTasks,
+ RHUIInfo
+)
from leapp.utils.deprecation import suppress_deprecation
ExternalRepoSetupTasks = namedtuple('ExternalRepoSetupTasks', ('to_enable', 'to_block', 'custom'))
@@ -86,6 +93,13 @@ class InputData():
self.enabled_repoids.add(repo.repoid)
+def _init_repomap_handler():
+ repomap_msg = load_repositories_mapping()
+ rhui_info = next(api.consume(RHUIInfo), None)
+ cloud_provider = rhui_info.provider if rhui_info else ''
+ return RepoMapDataHandler(repomap_msg, cloud_provider=cloud_provider)
+
+
def process():
"""
Orchestrate the four stages of target content resolution.
@@ -108,20 +122,20 @@ def process():
configuration.
"""
indata = InputData()
- repositories_map_msg = load_repositories_mapping()
+ repomap_handler = _init_repomap_handler()
blocklisted_repoids = repositoriesblocklist.compute_blocklist(
- repositories_map_msg,
+ repomap_handler,
indata.external_tasks,
indata.enabled_repoids
)
pes_requested_repoids = pes_events_scanner.scan_pes_events(
- repositories_map_msg,
+ repomap_handler,
blocklisted_repoids,
indata.enabled_repoids
)
setuptargetrepos.setup_target_repos(
- repositories_map_msg,
+ repomap_handler,
pes_requested_repoids=pes_requested_repoids,
blacklisted_repoids=blocklisted_repoids,
external_repoids_requests=indata.external_tasks.to_enable,
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
index a1f3a6e1..c6f16823 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
@@ -13,6 +13,7 @@ from leapp.libraries.actor.pes_events_scanner import (
reporting,
TransactionConfiguration
)
+from leapp.libraries.actor.repomap_calc import RepoMapDataHandler
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.models import (
DistributionSignedRPM,
@@ -265,12 +266,14 @@ def test_actor_performs(monkeypatch):
),
)
+ repomap_handler = RepoMapDataHandler(repositories_mapping)
+
produced_messages = produce_mocked()
created_report = create_report_mocked()
monkeypatch.setattr(api, 'produce', produced_messages)
monkeypatch.setattr(reporting, 'create_report', created_report)
- pes_events_scanner.scan_pes_events(repositories_mapping, set(), {'rhel8-repo'})
+ pes_events_scanner.scan_pes_events(repomap_handler, set(), {'rhel8-repo'})
assert produced_messages.called
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
index 843c8f74..5a2d1705 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_repositoriesblocklist.py
@@ -2,6 +2,7 @@ import pytest
from leapp import reporting
from leapp.libraries.actor import repositoriesblocklist
+from leapp.libraries.actor.repomap_calc import RepoMapDataHandler
from leapp.libraries.actor.targetcontentresolver import ExternalRepoSetupTasks
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
@@ -56,8 +57,9 @@ def test_crb_repos_blocked_when_no_crb_on_source(monkeypatch, repomap_opts_only)
))
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ handler = RepoMapDataHandler(repomap_opts_only)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, _NO_TASKS, enabled_repoids={'rhel-8-server-rpms'}
+ handler, _NO_TASKS, enabled_repoids={'rhel-8-server-rpms'}
)
assert 'codeready-builder-for-rhel-9-x86_64-rpms' in result
@@ -74,8 +76,9 @@ def test_empty_result_when_no_crb_pesid_in_mapping(monkeypatch, repomap_opts_onl
))
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ handler = RepoMapDataHandler(repomap_opts_only)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ handler, _NO_TASKS, enabled_repoids=set()
)
assert not result
@@ -90,8 +93,9 @@ def test_blocklist_generated_when_crb_disabled(monkeypatch, repomap_opts_only):
))
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ handler = RepoMapDataHandler(repomap_opts_only)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ handler, _NO_TASKS, enabled_repoids=set()
)
assert result, 'A blocklist should get generated.'
@@ -109,8 +113,9 @@ def test_no_blocklist_when_crb_enabled_on_source(monkeypatch, repomap_opts_only)
arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='rhel'
))
+ handler = RepoMapDataHandler(repomap_opts_only)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, _NO_TASKS,
+ handler, _NO_TASKS,
enabled_repoids={'codeready-builder-for-rhel-8-x86_64-rpms'}
)
@@ -198,8 +203,9 @@ def test_custom_enablerepo_effect(monkeypatch, repomap_opts_only,
to_enable=set(), to_block=set(), custom=custom_repoids
)
+ handler = RepoMapDataHandler(repomap_opts_only)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, external_tasks, enabled_repoids=set()
+ handler, external_tasks, enabled_repoids=set()
)
if exp_report_title:
@@ -232,21 +238,38 @@ def test_get_crb_repos_filters_by_architecture(monkeypatch):
),
]
repo_mapping = RepositoriesMapping(mapping=[], repositories=repos)
+ handler = RepoMapDataHandler(repo_mapping)
- result = repositoriesblocklist._get_crb_repos(repo_mapping, '9')
+ result = repositoriesblocklist._get_crb_repos(handler, False)
assert result == {'crb-x86_64'}
-def test_no_report_for_non_rhel_distro(monkeypatch, repomap_opts_only):
+def test_no_report_for_non_rhel_distro(monkeypatch):
"""No exclusion report generated for non-RHEL distros, but repos are still excluded."""
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
- arch='x86_64', src_ver='8.10', dst_ver='9.6', dst_distro='centos'
+ arch='x86_64', src_ver='8.10', dst_ver='9.6', src_distro='centos', dst_distro='centos'
))
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+ centos_repomap = RepositoriesMapping(
+ mapping=[RepoMapEntry(source='rhel8-CRB', target=['rhel9-CRB'])],
+ repositories=[
+ PESIDRepositoryEntry(
+ pesid='rhel8-CRB', major_version='8',
+ repoid='codeready-builder-for-centos-8-x86_64-rpms',
+ rhui='', arch='x86_64', channel='ga', repo_type='rpm', distro='centos',
+ ),
+ PESIDRepositoryEntry(
+ pesid='rhel9-CRB', major_version='9',
+ repoid='codeready-builder-for-centos-9-x86_64-rpms',
+ rhui='', arch='x86_64', channel='ga', repo_type='rpm', distro='centos',
+ ),
+ ]
+ )
+ handler = RepoMapDataHandler(centos_repomap)
result = repositoriesblocklist._calc_internal_blocklist(
- repomap_opts_only, _NO_TASKS, enabled_repoids=set()
+ handler, _NO_TASKS, enabled_repoids=set()
)
assert result
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
index 652bf30a..5192e65a 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
@@ -1,6 +1,7 @@
import pytest
from leapp.libraries.actor import setuptargetrepos
+from leapp.libraries.actor.repomap_calc import RepoMapDataHandler
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
@@ -27,14 +28,13 @@ def test_minimal_execution(monkeypatch):
"""
Tests whether the actor does not fail if no messages except the RepositoriesMapping are provided.
"""
- msgs = [
- RepositoriesMapping(mapping=[], repositories=[])
- ]
+ repos_mapping = RepositoriesMapping(mapping=[], repositories=[])
+ msgs = [repos_mapping]
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
- setuptargetrepos.setup_target_repos(msgs[0])
+ setuptargetrepos.setup_target_repos(RepoMapDataHandler(repos_mapping))
def test_custom_repos(monkeypatch):
@@ -62,7 +62,8 @@ def test_custom_repos(monkeypatch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
- setuptargetrepos.setup_target_repos(repositories_mapping, blacklisted_repoids={'rhel-8-blacklisted-rpms'})
+ handler = RepoMapDataHandler(repositories_mapping)
+ setuptargetrepos.setup_target_repos(handler, blacklisted_repoids={'rhel-8-blacklisted-rpms'})
assert api.produce.called
@@ -82,8 +83,9 @@ def test_repositories_setup_tasks(monkeypatch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
+ handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
- repositories_mapping,
+ handler,
blacklisted_repoids={'rhel-8-blacklisted-rpms'},
external_repoids_requests={'rhel-8-server-rpms', 'rhel-8-blacklisted-rpms'})
@@ -192,7 +194,8 @@ def test_repos_mapping_for_distro(monkeypatch, src_distro, dst_distro):
)
monkeypatch.setattr(api, 'produce', produce_mocked())
- setuptargetrepos.setup_target_repos(repomap, blacklisted_repoids={'{}-9-blacklisted-rpms'.format(dst_distro)})
+ handler = RepoMapDataHandler(repomap)
+ setuptargetrepos.setup_target_repos(handler, blacklisted_repoids={'{}-9-blacklisted-rpms'.format(dst_distro)})
assert api.produce.called
distro_repos = api.produce.model_instances[0].distro_repos
@@ -228,8 +231,9 @@ def test_pes_requested_repoids_added_to_target(monkeypatch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
+ handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
- repositories_mapping,
+ handler,
pes_requested_repoids={'pes-repo-1', 'pes-repo-2', 'pes-blocked'},
blacklisted_repoids={'pes-blocked'},
)
@@ -255,8 +259,9 @@ def test_pes_and_external_repoids_combined(monkeypatch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
+ handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
- repositories_mapping,
+ handler,
pes_requested_repoids={'pes-repo'},
blacklisted_repoids={'blocked-repo'},
external_repoids_requests={'ext-repo', 'blocked-repo'},
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 970fd797..750f6f9e 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -23,7 +23,7 @@ def test_process_orchestration(monkeypatch):
"""
call_log = []
- fake_repo_map = object()
+ fake_repomap_handler = object()
fake_blocklist = frozenset({'blocked-repo'})
fake_external_tasks = ExternalRepoSetupTasks(
to_enable=frozenset({'ext-repo'}), to_block=frozenset(), custom=frozenset()
@@ -37,20 +37,20 @@ def test_process_orchestration(monkeypatch):
self.external_tasks = fake_external_tasks
self.enabled_repoids = fake_enabled_repoids
- def mock_load_repositories_mapping():
- call_log.append('load_repositories_mapping')
- return fake_repo_map
+ def mock_init_repomap_handler():
+ call_log.append('_init_repomap_handler')
+ return fake_repomap_handler
def mock_compute_blocklist(repo_mapping, external_tasks, enabled_repoids):
call_log.append('compute_blocklist')
- assert repo_mapping is fake_repo_map
+ assert repo_mapping is fake_repomap_handler
assert external_tasks is fake_external_tasks
assert enabled_repoids is fake_enabled_repoids
return fake_blocklist
def mock_scan_pes_events(repo_mapping, blacklisted_repoids, enabled_repoids):
call_log.append('scan_pes_events')
- assert repo_mapping is fake_repo_map
+ assert repo_mapping is fake_repomap_handler
assert blacklisted_repoids is fake_blocklist
assert enabled_repoids is fake_enabled_repoids
return fake_pes_repoids
@@ -58,7 +58,7 @@ def test_process_orchestration(monkeypatch):
def mock_setup_target_repos(repo_mapping, pes_requested_repoids=None,
blacklisted_repoids=None, external_repoids_requests=None):
call_log.append('setup_target_repos')
- assert repo_mapping is fake_repo_map
+ assert repo_mapping is fake_repomap_handler
assert pes_requested_repoids is fake_pes_repoids
assert blacklisted_repoids is fake_blocklist
assert external_repoids_requests is fake_external_tasks.to_enable
@@ -68,8 +68,8 @@ def test_process_orchestration(monkeypatch):
FakeInputData,
)
monkeypatch.setattr(
- 'leapp.libraries.actor.targetcontentresolver.load_repositories_mapping',
- mock_load_repositories_mapping,
+ 'leapp.libraries.actor.targetcontentresolver._init_repomap_handler',
+ mock_init_repomap_handler,
)
monkeypatch.setattr(
'leapp.libraries.actor.targetcontentresolver.repositoriesblocklist.compute_blocklist',
@@ -88,7 +88,7 @@ def test_process_orchestration(monkeypatch):
assert call_log == [
'InputData',
- 'load_repositories_mapping',
+ '_init_repomap_handler',
'compute_blocklist',
'scan_pes_events',
'setup_target_repos',
--
2.54.0

View File

@ -0,0 +1,262 @@
From c8efe616de0859c22de73573bb55d65949f2d180 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sun, 7 Jun 2026 01:53:07 +0200
Subject: [PATCH 103/105] [7/9] targetcontentresolver: setuptargetrepos: minor
updates
* accept enabled repositories
* update the blacklist -> blocklist wording
* drop unneeded rename blocklisted_repoids -> excluded_repoids
- the rename was there just for the purpose to check whether
the blocklisted_repoids input parameter is a set or not. but we
know it will be always the set
* make all input parameters in setup_target_repos mandatory
- dropping default value which does not make sense anymore
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Co-authored-by: Matej Matuska <mmatuska@redhat.com>
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/setuptargetrepos.py | 15 ++---
.../libraries/targetcontentresolver.py | 3 +-
.../tests/test_setuptargetrepos.py | 56 ++++++++++++++-----
.../tests/test_targetcontentresolver.py | 7 ++-
4 files changed, 54 insertions(+), 27 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
index 4f7ffd9f..4896bf9b 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
@@ -1,5 +1,4 @@
from leapp.libraries.actor import repomap_calc
-from leapp.libraries.actor.pes_events_scanner import get_enabled_repoids
from leapp.libraries.common.config import get_source_distro_id, get_target_distro_id
from leapp.libraries.common.config.version import get_source_major_version, get_source_version
from leapp.libraries.stdlib import api
@@ -57,21 +56,19 @@ def _get_mapped_repoids(repomap, src_repoids):
@suppress_deprecation(RHELTargetRepository)
-def setup_target_repos(repomap_handler, pes_requested_repoids=None,
- blacklisted_repoids=None, external_repoids_requests=None):
+def setup_target_repos(repomap_handler, enabled_repoids, pes_requested_repoids,
+ blocklisted_repoids, external_repoids_requests):
"""
Determine the final list of target repositories.
:param repomap_handler: Operator to work with the repositories mapping data
:type repomap_handler: repomap_calc.RepoMapDataHandler
:param pes_requested_repoids: Set of repoids derived from PES events that need to be enabled.
- :param blacklisted_repoids: Set of repoids to exclude from target repos. If None, an empty set is used.
+ :param blocklisted_repoids: Set of repoids to exclude from target repos. If None, an empty set is used.
:param external_repoids_requests: Set of repoids requested by external actors (e.g. satellite_upgrade_facts).
"""
# Load relevant data from messages
used_repoids_dict = _get_used_repo_dict()
- enabled_repoids = get_enabled_repoids()
- excluded_repoids = blacklisted_repoids if blacklisted_repoids is not None else set()
custom_repos = _get_custom_target_repos()
repoids_from_installed_packages = _get_repoids_from_installed_packages()
@@ -117,7 +114,7 @@ def setup_target_repos(repomap_handler, pes_requested_repoids=None,
.format(target_pesid)
)
continue
- if target_pesidrepo.repoid in excluded_repoids:
+ if target_pesidrepo.repoid in blocklisted_repoids:
api.current_logger().debug('Skipping the {} repo (excluded).'.format(target_pesidrepo.repoid))
continue
target_distro_repoids.add(target_pesidrepo.repoid)
@@ -129,7 +126,7 @@ def setup_target_repos(repomap_handler, pes_requested_repoids=None,
all_requested_repoids.update(external_repoids_requests or set())
for repo in all_requested_repoids:
- if repo in excluded_repoids:
+ if repo in blocklisted_repoids:
api.current_logger().debug('Skipping the {} repo from setup task (excluded).'.format(repo))
continue
target_distro_repoids.add(repo)
@@ -140,7 +137,7 @@ def setup_target_repos(repomap_handler, pes_requested_repoids=None,
else:
rhel_repos = []
distro_repos = [DistroTargetRepository(repoid=repoid) for repoid in sorted(target_distro_repoids)]
- custom_repos = [repo for repo in custom_repos if repo.repoid not in excluded_repoids]
+ custom_repos = [repo for repo in custom_repos if repo.repoid not in blocklisted_repoids]
custom_repos = sorted(custom_repos, key=lambda x: x.repoid)
# produce message about skipped repositories
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index 942a2680..374f1498 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -136,7 +136,8 @@ def process():
setuptargetrepos.setup_target_repos(
repomap_handler,
+ indata.enabled_repoids,
pes_requested_repoids=pes_requested_repoids,
- blacklisted_repoids=blocklisted_repoids,
+ blocklisted_repoids=blocklisted_repoids,
external_repoids_requests=indata.external_tasks.to_enable,
)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
index 5192e65a..b2b4b8db 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_setuptargetrepos.py
@@ -34,22 +34,28 @@ def test_minimal_execution(monkeypatch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
- setuptargetrepos.setup_target_repos(RepoMapDataHandler(repos_mapping))
+ setuptargetrepos.setup_target_repos(
+ RepoMapDataHandler(repos_mapping),
+ enabled_repoids=set(),
+ pes_requested_repoids=set(),
+ blocklisted_repoids=set(),
+ external_repoids_requests=set(),
+ )
def test_custom_repos(monkeypatch):
"""
Tests whether the CustomRepos provided to the actor are propagated to the TargetRepositories after
- blacklist filtering is applied on them.
+ blocklist filtering is applied on them.
"""
custom = CustomTargetRepository(repoid='rhel-8-server-rpms',
name='RHEL 8 Server (RPMs)',
baseurl='https://.../dist/rhel/server/8/os',
enabled=True)
- blacklisted = CustomTargetRepository(repoid='rhel-8-blacklisted-rpms',
- name='RHEL 8 Blacklisted (RPMs)',
- baseurl='https://.../dist/rhel/blacklisted/8/os',
+ blocklisted = CustomTargetRepository(repoid='rhel-8-blocklisted-rpms',
+ name='RHEL 8 Blocklisted (RPMs)',
+ baseurl='https://.../dist/rhel/blocklisted/8/os',
enabled=True)
repositories_mapping = RepositoriesMapping(
@@ -57,13 +63,19 @@ def test_custom_repos(monkeypatch):
repositories=[]
)
- msgs = [custom, blacklisted, repositories_mapping]
+ msgs = [custom, blocklisted, repositories_mapping]
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
monkeypatch.setattr(api, 'produce', produce_mocked())
handler = RepoMapDataHandler(repositories_mapping)
- setuptargetrepos.setup_target_repos(handler, blacklisted_repoids={'rhel-8-blacklisted-rpms'})
+ setuptargetrepos.setup_target_repos(
+ handler,
+ enabled_repoids=set(),
+ pes_requested_repoids=set(),
+ blocklisted_repoids={'rhel-8-blocklisted-rpms'},
+ external_repoids_requests=set(),
+ )
assert api.produce.called
@@ -75,7 +87,7 @@ def test_custom_repos(monkeypatch):
def test_repositories_setup_tasks(monkeypatch):
"""
Tests whether the actor propagates requested repoids
- to the resulting TargetRepositories (and blacklist filtering is applied to them).
+ to the resulting TargetRepositories (and blocklist filtering is applied to them).
"""
repositories_mapping = RepositoriesMapping(mapping=[], repositories=[])
msgs = [repositories_mapping]
@@ -86,8 +98,11 @@ def test_repositories_setup_tasks(monkeypatch):
handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
handler,
- blacklisted_repoids={'rhel-8-blacklisted-rpms'},
- external_repoids_requests={'rhel-8-server-rpms', 'rhel-8-blacklisted-rpms'})
+ enabled_repoids=set(),
+ pes_requested_repoids=set(),
+ blocklisted_repoids={'rhel-8-blacklisted-rpms'},
+ external_repoids_requests={'rhel-8-server-rpms', 'rhel-8-blacklisted-rpms'},
+ )
assert api.produce.called
@@ -195,7 +210,17 @@ def test_repos_mapping_for_distro(monkeypatch, src_distro, dst_distro):
monkeypatch.setattr(api, 'produce', produce_mocked())
handler = RepoMapDataHandler(repomap)
- setuptargetrepos.setup_target_repos(handler, blacklisted_repoids={'{}-9-blacklisted-rpms'.format(dst_distro)})
+ enabled_repoids = {
+ '{}-8-server-rpms'.format(src_distro),
+ '{}-8-blacklisted-rpms'.format(src_distro),
+ }
+ setuptargetrepos.setup_target_repos(
+ handler,
+ enabled_repoids=enabled_repoids,
+ pes_requested_repoids=set(),
+ blocklisted_repoids={'{}-9-blacklisted-rpms'.format(dst_distro)},
+ external_repoids_requests=set(),
+ )
assert api.produce.called
distro_repos = api.produce.model_instances[0].distro_repos
@@ -223,7 +248,7 @@ def test_repos_mapping_for_distro(monkeypatch, src_distro, dst_distro):
def test_pes_requested_repoids_added_to_target(monkeypatch):
"""
Tests whether PES-requested repoids are added to the target repositories
- and that blacklisted PES-requested repoids are excluded.
+ and that blocklisted PES-requested repoids are excluded.
"""
repositories_mapping = RepositoriesMapping(mapping=[], repositories=[])
msgs = [repositories_mapping]
@@ -234,8 +259,10 @@ def test_pes_requested_repoids_added_to_target(monkeypatch):
handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
handler,
+ enabled_repoids=set(),
pes_requested_repoids={'pes-repo-1', 'pes-repo-2', 'pes-blocked'},
- blacklisted_repoids={'pes-blocked'},
+ blocklisted_repoids={'pes-blocked'},
+ external_repoids_requests=set(),
)
assert api.produce.called
@@ -262,8 +289,9 @@ def test_pes_and_external_repoids_combined(monkeypatch):
handler = RepoMapDataHandler(repositories_mapping)
setuptargetrepos.setup_target_repos(
handler,
+ enabled_repoids=set(),
pes_requested_repoids={'pes-repo'},
- blacklisted_repoids={'blocked-repo'},
+ blocklisted_repoids={'blocked-repo'},
external_repoids_requests={'ext-repo', 'blocked-repo'},
)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index 750f6f9e..c0ca2d93 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -55,12 +55,13 @@ def test_process_orchestration(monkeypatch):
assert enabled_repoids is fake_enabled_repoids
return fake_pes_repoids
- def mock_setup_target_repos(repo_mapping, pes_requested_repoids=None,
- blacklisted_repoids=None, external_repoids_requests=None):
+ def mock_setup_target_repos(repo_mapping, enabled_repoids, pes_requested_repoids,
+ blocklisted_repoids, external_repoids_requests):
call_log.append('setup_target_repos')
assert repo_mapping is fake_repomap_handler
+ assert enabled_repoids is fake_enabled_repoids
assert pes_requested_repoids is fake_pes_repoids
- assert blacklisted_repoids is fake_blocklist
+ assert blocklisted_repoids is fake_blocklist
assert external_repoids_requests is fake_external_tasks.to_enable
monkeypatch.setattr(
--
2.54.0

View File

@ -0,0 +1,849 @@
From e8be7466b224df83691796a913792c759f736cd3 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Sat, 6 Jun 2026 12:30:21 +0200
Subject: [PATCH 104/105] [8/9] targetcontentresolver: pes_events_scanner:
refactoring
* Distinguish public & private functions
* Require installed rpms and enabled modules as input parameters
instead of consuming them directly
* Deduplication of enabled modules is moved to the orchestrator
library
* Code-stylistic changes
* Updated the blacklist -> blocklist wording in the code
* Drop dead code
Unit tests adjusted with assistance of AI.
Jira: RHEL-115867
Co-authored-by: Matej Matuska <mmatuska@redhat.com>
Assisted-by: Claude Code (Opus 4.6)
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/pes_events_scanner.py | 227 +++++++-----------
.../libraries/targetcontentresolver.py | 25 +-
.../tests/test_pes_event_scanner.py | 60 +++--
.../tests/test_targetcontentresolver.py | 32 ++-
4 files changed, 162 insertions(+), 182 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
index 96f26a59..2ea63f72 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/pes_events_scanner.py
@@ -2,7 +2,6 @@ from collections import defaultdict, namedtuple
from functools import partial
from leapp import reporting
-from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import repomap_calc
from leapp.libraries.actor.pes_event_parsing import Action, get_pes_events, Package
from leapp.libraries.common import rpms
@@ -10,15 +9,7 @@ from leapp.libraries.common.config import get_target_distro_id, version
from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.libraries.stdlib.config import is_verbose
-from leapp.models import (
- DistributionSignedRPM,
- EnabledModules,
- Module,
- PESIDRepositoryEntry,
- PESRpmTransactionTasks,
- RepositoriesFacts,
- RpmTransactionTasks
-)
+from leapp.models import Module, PESIDRepositoryEntry, PESRpmTransactionTasks, RpmTransactionTasks
SKIPPED_PKGS_MSG = (
'packages will be skipped because they are available only in '
@@ -32,48 +23,22 @@ SKIPPED_PKGS_MSG = (
TransactionConfiguration = namedtuple('TransactionConfiguration', ('to_install', 'to_remove', 'to_keep'))
-def get_cloud_provider_name(cloud_provider_variant):
- for cloud_provider_prefix in ('aws', 'azure', 'google'):
- if cloud_provider_variant.startswith(cloud_provider_prefix):
- return cloud_provider_prefix
- return cloud_provider_variant
-
-
-def get_best_pesid_candidate(candidate_a, candidate_b, cloud_provider):
- cdn_candidate = None
- for candidate in (candidate_a, candidate_b):
- if candidate.rhui == cloud_provider:
- return candidate
- if not candidate.rhui:
- cdn_candidate = candidate
-
- # None of the candidate matches cloud provider and none of them is from CDN -
- # do not return anything as we don't want to get content from different cloud providers
- return cdn_candidate
-
-
-def get_installed_pkgs():
+def _rpms_to_package_set(rpm_list):
installed_pkgs = set()
-
- installed_rh_signed_rpm_msgs = api.consume(DistributionSignedRPM)
- installed_rh_signed_rpm_msg = next(installed_rh_signed_rpm_msgs, None)
- if list(installed_rh_signed_rpm_msgs):
- api.current_logger().warning('Unexpectedly received more than one DistributionSignedRPM message.')
- if not installed_rh_signed_rpm_msg:
- raise StopActorExecutionError('Cannot parse PES data properly due to missing list of installed packages',
- details={'Problem': 'Did not receive a message with installed Red Hat-signed '
- 'packages (DistributionSignedRPM)'})
-
- for pkg in installed_rh_signed_rpm_msg.items:
+ for rpm in rpm_list:
modulestream = None
- if pkg.module and pkg.stream:
- modulestream = (pkg.module, pkg.stream)
- installed_pkgs.add(Package(name=pkg.name, repository=pkg.repository, modulestream=modulestream))
+ if rpm.module and rpm.stream:
+ modulestream = (rpm.module, rpm.stream)
+ installed_pkgs.add(Package(
+ name=rpm.name,
+ repository=rpm.repository,
+ modulestream=modulestream
+ ))
return installed_pkgs
-def get_transaction_configuration():
+def _get_transaction_configuration():
"""
Get pkgs to install, keep and remove from RpmTransactionTasks messages.
@@ -94,7 +59,7 @@ def get_transaction_configuration():
return transaction_configuration
-def get_relevant_releases(events):
+def _get_relevant_releases(events):
"""
Get releases present in the PES Events that are relevant for this IPU.
@@ -110,24 +75,11 @@ def get_relevant_releases(events):
return sorted(releases)
-def _get_enabled_modules():
- # TODO(pstodulk): take a look
- enabled_modules_msgs = api.consume(EnabledModules)
- enabled_modules_msg = next(enabled_modules_msgs, None)
- if list(enabled_modules_msgs):
- api.current_logger().warning('Unexpectedly received more than one EnabledModules message.')
- if not enabled_modules_msg:
- raise StopActorExecutionError('Cannot parse PES data properly due to missing list of enabled modules',
- details={'Problem': 'Did not receive a message with enabled module '
- 'streams (EnabledModules)'})
- return enabled_modules_msg.modules
-
-
-def compute_pkg_changes_between_consequent_releases(source_installed_pkgs,
- events,
- release,
- seen_pkgs,
- pkgs_to_demodularize):
+def _compute_pkg_changes_between_consequent_releases(source_installed_pkgs,
+ events,
+ release,
+ seen_pkgs,
+ pkgs_to_demodularize):
logger = api.current_logger()
# Start with the installed packages and modify the set according to release events
target_pkgs = set(source_installed_pkgs)
@@ -194,7 +146,7 @@ def compute_pkg_changes_between_consequent_releases(source_installed_pkgs,
return (target_pkgs, pkgs_to_demodularize)
-def remove_undesired_events(events, relevant_to_releases):
+def _remove_undesired_events(events, relevant_to_releases):
"""
Conservatively remove events that needless, or cause problems for the current implementation:
- (needless) events with to_release not in relevant releases
@@ -243,7 +195,7 @@ def remove_undesired_events(events, relevant_to_releases):
return cleaned_events
-def compute_packages_on_target_system(source_pkgs, events, releases):
+def _compute_packages_on_target_system(source_pkgs, events, releases):
seen_pkgs = set(source_pkgs) # Used to track whether PRESENCE events can be applied
target_pkgs = set(source_pkgs)
@@ -257,9 +209,9 @@ def compute_packages_on_target_system(source_pkgs, events, releases):
did_processing_cross_major_version = True
pkgs_to_demodularize = {pkg for pkg in target_pkgs if pkg.modulestream}
- target_pkgs, pkgs_to_demodularize = compute_pkg_changes_between_consequent_releases(target_pkgs, events,
- release, seen_pkgs,
- pkgs_to_demodularize)
+ target_pkgs, pkgs_to_demodularize = _compute_pkg_changes_between_consequent_releases(target_pkgs, events,
+ release, seen_pkgs,
+ pkgs_to_demodularize)
seen_pkgs = seen_pkgs.union(target_pkgs)
demodularized_pkgs = {Package(pkg.name, pkg.repository, None) for pkg in pkgs_to_demodularize}
@@ -268,34 +220,33 @@ def compute_packages_on_target_system(source_pkgs, events, releases):
return (demodularized_target_pkgs, pkgs_to_demodularize)
-def compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, pkgs_to_demodularize):
+def _compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, pkgs_to_demodularize, modules_to_reset):
source_state_pkg_names = {pkg.name for pkg in source_pkgs}
target_state_pkg_names = {pkg.name for pkg in target_pkgs}
pkgs_to_install = sorted(target_state_pkg_names.difference(source_state_pkg_names))
pkgs_to_remove = sorted(source_state_pkg_names.difference(target_state_pkg_names))
- if pkgs_to_install or pkgs_to_remove:
- # NOTE(mhecko): Here we do not want to consider any package that does not have a reference in PES data. There
- # might be missing modularity information, and although the algorithm is correct, trying to enable
- # a non-existent modulestream due to missing modulestream information results in a crash.
- target_pkgs_without_demodularized_pkgs = target_pkgs.difference(pkgs_to_demodularize)
+ if not (pkgs_to_install or pkgs_to_remove):
+ return None
- # Collect the enabled modules as tuples in a set, so we produce every module to reset exactly once
- enabled_modules = {(module.name, module.stream) for module in _get_enabled_modules()}
- modules_to_reset = [Module(name=ms[0], stream=ms[1]) for ms in enabled_modules]
+ # NOTE(mhecko): Here we do not want to consider any package that does not have a reference in PES data. There
+ # might be missing modularity information, and although the algorithm is correct, trying to enable
+ # a non-existent modulestream due to missing modulestream information results in a crash.
+ target_pkgs_without_demodularized_pkgs = target_pkgs.difference(pkgs_to_demodularize)
- target_modulestreams = {pkg.modulestream for pkg in target_pkgs_without_demodularized_pkgs if pkg.modulestream}
- modules_to_enable = [Module(name=ms[0], stream=ms[1]) for ms in target_modulestreams]
+ target_modulestreams = {pkg.modulestream for pkg in target_pkgs_without_demodularized_pkgs if pkg.modulestream}
+ modules_to_enable = [Module(name=ms[0], stream=ms[1]) for ms in target_modulestreams]
- return PESRpmTransactionTasks(to_install=pkgs_to_install,
- to_remove=pkgs_to_remove,
- modules_to_enable=modules_to_enable,
- modules_to_reset=modules_to_reset)
- return None
+ return PESRpmTransactionTasks(
+ to_install=pkgs_to_install,
+ to_remove=pkgs_to_remove,
+ modules_to_enable=modules_to_enable,
+ modules_to_reset=modules_to_reset
+ )
-def report_skipped_packages(title, message, skipped_pkgs, remediation=None):
+def _report_skipped_packages(title, message, skipped_pkgs, remediation=None):
skipped_pkgs = sorted(skipped_pkgs)
def make_summary_entry_for_skipped_pkg(pkg):
@@ -320,44 +271,25 @@ def report_skipped_packages(title, message, skipped_pkgs, remediation=None):
api.current_logger().info(summary)
-def remove_new_packages_from_blacklisted_repos(source_pkgs, target_pkgs, blacklisted_repoids):
+def _remove_new_packages_from_blocklisted_repos(source_pkgs, target_pkgs, blocklisted_repoids):
"""
- Remove newly installed packages from blacklisted repositories that were computed to be on the target system.
+ Remove newly installed packages from blocklisted repositories that were computed to be on the target system.
- :param blacklisted_repoids: Set of repoids to exclude. If None, an empty set is used.
+ :param blocklisted_repoids: Set of repoids to exclude. If None, an empty set is used.
"""
- if blacklisted_repoids is None:
- blacklisted_repoids = set()
new_pkgs = target_pkgs.difference(source_pkgs)
- pkgs_from_blacklisted_repos = set(pkg for pkg in new_pkgs if pkg.repository in blacklisted_repoids)
+ pkgs_from_blocklisted_repos = set(pkg for pkg in new_pkgs if pkg.repository in blocklisted_repoids)
- if pkgs_from_blacklisted_repos:
- report_skipped_packages(
+ if pkgs_from_blocklisted_repos:
+ _report_skipped_packages(
title='Packages available in excluded repositories will not be installed',
message=SKIPPED_PKGS_MSG,
- skipped_pkgs=pkgs_from_blacklisted_repos,
+ skipped_pkgs=pkgs_from_blocklisted_repos,
)
- return blacklisted_repoids, target_pkgs.difference(pkgs_from_blacklisted_repos)
+ return target_pkgs.difference(pkgs_from_blocklisted_repos)
-def get_enabled_repoids():
- """
- Collects repoids of all enabled repositories on the source system.
-
- :param repositories_facts: Iterable of RepositoriesFacts containing repositories related info about source system.
- :return: Set of all enabled repository IDs present on the source system.
- :rtype: Set[str]
- """
- enabled_repoids = set()
- for repos in api.consume(RepositoriesFacts):
- for repo_file in repos.repositories:
- for repo in repo_file.data:
- if repo.enabled:
- enabled_repoids.add(repo.repoid)
- return enabled_repoids
-
-
-def get_pesid_to_repoid_map(target_pesids, repomap_handler, enabled_repoids):
+def _get_pesid_to_repoid_map(target_pesids, repomap_handler, enabled_repoids):
"""
Get a dictionary mapping all PESID repositories to their corresponding repoid.
@@ -430,7 +362,7 @@ def get_pesid_to_repoid_map(target_pesids, repomap_handler, enabled_repoids):
return repositories_mapping
-def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repomap_handler, enabled_repoids):
+def _replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repomap_handler, enabled_repoids):
"""Replace packages with PESID in their .repository field with ones that have repoid providing the package."""
# We want to map only PESIDs - if some package had no events, it will its repository set to source system repoid
packages_with_pesid = {pkg for pkg in packages if pkg.repository not in source_pkgs_repoids}
@@ -438,7 +370,7 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repom
required_target_pesids = {pkg.repository for pkg in packages_with_pesid}
- pesid_to_target_repoid_map = get_pesid_to_repoid_map(required_target_pesids, repomap_handler, enabled_repoids)
+ pesid_to_target_repoid_map = _get_pesid_to_repoid_map(required_target_pesids, repomap_handler, enabled_repoids)
packages_with_unknown_target_repoid = {
pkg
@@ -447,7 +379,7 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repom
}
if packages_with_unknown_target_repoid:
- report_skipped_packages(
+ _report_skipped_packages(
title='Packages from unknown repositories may not be installed',
message='packages may not be installed or upgraded due to repositories unknown to leapp:',
skipped_pkgs=packages_with_unknown_target_repoid,
@@ -458,7 +390,8 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repom
' You can also review the projected DNF upgrade transaction result'
' in the logs to see what is going to happen, as this does not necessarily mean'
' that the listed packages will not be upgraded. You can also'
- ' install any missing packages after the in-place upgrade manually.'.format(
+ ' install any missing packages after the in-place upgrade manually.'
+ .format(
'RHEL (provided by Red Hat on CDN)'
if get_target_distro_id() == 'rhel'
else DISTRO_REPORT_NAMES.target
@@ -474,7 +407,7 @@ def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids, repom
return packages_with_repoid.union(packages_without_pesid)
-def apply_transaction_configuration(source_pkgs, transaction_configuration):
+def _apply_transaction_configuration(source_pkgs, transaction_configuration):
source_pkgs_with_conf_applied = set(source_pkgs)
source_pkgs_with_conf_applied = source_pkgs.union(transaction_configuration.to_install)
@@ -494,7 +427,7 @@ def apply_transaction_configuration(source_pkgs, transaction_configuration):
return source_pkgs_with_conf_applied
-def remove_leapp_related_events(events):
+def _remove_leapp_related_events(events):
major_vers = ['7', '8', '9']
leapp_pkgs = rpms.get_leapp_dep_packages(major_vers) + rpms.get_leapp_packages(major_vers)
res = []
@@ -508,7 +441,7 @@ def remove_leapp_related_events(events):
return res
-def include_instructions_from_transaction_configuration(rpm_tasks, transaction_configuration, installed_pkgs):
+def _include_instructions_from_transaction_configuration(rpm_tasks, transaction_configuration, installed_pkgs):
"""
Extend current rpm_tasks applying data from transaction_configuration
@@ -552,13 +485,13 @@ def include_instructions_from_transaction_configuration(rpm_tasks, transaction_c
modules_to_reset=modules_to_reset)
-def scan_pes_events(repomap_handler, blacklisted_repoids, enabled_repoids):
+def scan_pes_events(repomap_handler, blocklisted_repoids, enabled_repoids, installed_rpms, enabled_modules):
"""
Process PES events and compute RPM transaction tasks.
:param repomap_handler: Operator to work with the repositories mapping data
:type repomap_handler: repomap_calc.RepoMapDataHandler
- :param blacklisted_repoids: Set of repoids to exclude from target packages. If None, an empty set is used.
+ :param blocklisted_repoids: Set of repoids to exclude from target packages. If None, an empty set is used.
:returns: Set of target repoids that need to be enabled, or None if no PES events are found.
:rtype: Optional[set]
"""
@@ -567,41 +500,55 @@ def scan_pes_events(repomap_handler, blacklisted_repoids, enabled_repoids):
if not events:
return None
- releases = get_relevant_releases(events)
- installed_pkgs = get_installed_pkgs()
- transaction_configuration = get_transaction_configuration()
- pkgs_to_begin_computation_with = apply_transaction_configuration(installed_pkgs, transaction_configuration)
+ releases = _get_relevant_releases(events)
+ installed_pkgs = _rpms_to_package_set(installed_rpms)
+ transaction_configuration = _get_transaction_configuration()
+ pkgs_to_begin_computation_with = _apply_transaction_configuration(installed_pkgs, transaction_configuration)
# Keep track of what repoids have the source packages to be able to determine what are the PESIDs of the computed
# packages of the target system, so we can distinguish what needs to be repomapped
repoids_of_source_pkgs = {pkg.repository for pkg in pkgs_to_begin_computation_with}
- events = remove_leapp_related_events(events)
- events = remove_undesired_events(events, releases)
+ events = _remove_leapp_related_events(events)
+ events = _remove_undesired_events(events, releases)
# Apply events - compute what packages should the target system have
- target_pkgs, pkgs_to_demodularize = compute_packages_on_target_system(pkgs_to_begin_computation_with,
- events, releases)
+ target_pkgs, pkgs_to_demodularize = _compute_packages_on_target_system(
+ pkgs_to_begin_computation_with,
+ events,
+ releases
+ )
# Packages coming out of the events have PESID as their repository, however, we need real repoid
- target_pkgs = replace_pesids_with_repoids_in_packages(
+ target_pkgs = _replace_pesids_with_repoids_in_packages(
target_pkgs,
repoids_of_source_pkgs,
repomap_handler,
enabled_repoids
)
- # Apply the desired repository blacklisting
- blacklisted_repoids, target_pkgs = remove_new_packages_from_blacklisted_repos(pkgs_to_begin_computation_with,
- target_pkgs, blacklisted_repoids)
+ # Apply the desired repository blocklisting
+ target_pkgs = _remove_new_packages_from_blocklisted_repos(
+ pkgs_to_begin_computation_with,
+ target_pkgs,
+ blocklisted_repoids
+ )
# Look at the target packages and determine what repositories to enable
- pes_requested_repoids = set(p.repository for p in target_pkgs) - blacklisted_repoids - repoids_of_source_pkgs
+ pes_requested_repoids = set(p.repository for p in target_pkgs) - blocklisted_repoids - repoids_of_source_pkgs
# Compare the packages on source system and the computed packages on target system and determine what to install
- rpm_tasks = compute_rpm_tasks_from_pkg_set_diff(pkgs_to_begin_computation_with, target_pkgs, pkgs_to_demodularize)
- rpm_tasks = include_instructions_from_transaction_configuration(rpm_tasks, transaction_configuration,
- installed_pkgs)
+ rpm_tasks = _compute_rpm_tasks_from_pkg_set_diff(
+ pkgs_to_begin_computation_with,
+ target_pkgs,
+ pkgs_to_demodularize,
+ enabled_modules
+ )
+ rpm_tasks = _include_instructions_from_transaction_configuration(
+ rpm_tasks,
+ transaction_configuration,
+ installed_pkgs
+ )
if rpm_tasks:
api.produce(rpm_tasks)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index 374f1498..83460d99 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -7,6 +7,9 @@ from leapp.libraries.actor.repomap_loader import load_repositories_mapping
from leapp.libraries.stdlib import api
from leapp.models import (
CustomTargetRepository,
+ DistributionSignedRPM,
+ EnabledModules,
+ Module,
RepositoriesBlacklisted,
RepositoriesFacts,
RepositoriesSetupTasks,
@@ -26,6 +29,8 @@ class InputData():
self._missing_messages = []
self._get_enabled_repoids()
+ self._get_installed_rpms()
+ self._get_enabled_modules()
self._get_external_reposetup_tasks()
if self._missing_messages:
@@ -92,6 +97,22 @@ class InputData():
if repo.enabled:
self.enabled_repoids.add(repo.repoid)
+ def _get_enabled_modules(self):
+ self.enabled_modules = []
+ modules_facts = self._treat_consume_msg(EnabledModules)
+ if modules_facts is None:
+ return
+
+ # NOTE(pstodulk): This deduplication is weird, not sure why the input
+ # data would not be unique. But let's rather keep for now.
+ uniq_modules = {(module.name, module.stream) for module in modules_facts.modules}
+ self.enabled_modules = [Module(name=ms[0], stream=ms[1]) for ms in uniq_modules]
+
+ def _get_installed_rpms(self):
+ distro_rpms = self._treat_consume_msg(DistributionSignedRPM)
+ if distro_rpms:
+ self.installed_rpms = distro_rpms.items
+
def _init_repomap_handler():
repomap_msg = load_repositories_mapping()
@@ -131,7 +152,9 @@ def process():
pes_requested_repoids = pes_events_scanner.scan_pes_events(
repomap_handler,
blocklisted_repoids,
- indata.enabled_repoids
+ indata.enabled_repoids,
+ indata.installed_rpms,
+ indata.enabled_modules
)
setuptargetrepos.setup_target_repos(
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
index c6f16823..b0beebbb 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_pes_event_scanner.py
@@ -5,10 +5,10 @@ import pytest
from leapp.libraries.actor import pes_events_scanner
from leapp.libraries.actor.pes_event_parsing import Event
from leapp.libraries.actor.pes_events_scanner import (
+ _compute_packages_on_target_system,
+ _compute_rpm_tasks_from_pkg_set_diff,
Action,
api,
- compute_packages_on_target_system,
- compute_rpm_tasks_from_pkg_set_diff,
Package,
reporting,
TransactionConfiguration
@@ -16,8 +16,6 @@ from leapp.libraries.actor.pes_events_scanner import (
from leapp.libraries.actor.repomap_calc import RepoMapDataHandler
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.models import (
- DistributionSignedRPM,
- EnabledModules,
PESIDRepositoryEntry,
PESRpmTransactionTasks,
RepoMapEntry,
@@ -135,7 +133,7 @@ def pkgs_into_tuples(pkgs):
def test_event_application_fundamentals(monkeypatch, installed_pkgs, events, releases, expected_target_pkgs):
"""Trivial checks validating that the core event application algorithm reflects event semantics as expected."""
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
- actual_target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, releases)
+ actual_target_pkgs, dummy_demodularized_pkgs = _compute_packages_on_target_system(installed_pkgs, events, releases)
# Perform strict comparison
actual_pkg_tuple_set = {(pkg.name, pkg.repository, pkg.modulestream) for pkg in actual_target_pkgs}
@@ -174,7 +172,9 @@ def test_compute_pkg_state(monkeypatch):
Package('reintroduced', 'rhel7-repo', None),
}
- target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 0), (8, 1)])
+ target_pkgs, dummy_demodularized_pkgs = _compute_packages_on_target_system(
+ installed_pkgs, events, [(8, 0), (8, 1)]
+ )
expected_target_pkgs = {
Package('split01', 'rhel8-repo', None),
@@ -186,7 +186,6 @@ def test_compute_pkg_state(monkeypatch):
def test_compute_rpm_tasks_from_pkg_set_diff(monkeypatch):
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[EnabledModules(modules=[])]))
source_pkgs = {
Package('removed1', '7repo', None),
Package('removed2', '7repo', None),
@@ -201,7 +200,7 @@ def test_compute_rpm_tasks_from_pkg_set_diff(monkeypatch):
Package('installed2', '8repo2', None),
}
- rpm_tasks = compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, set())
+ rpm_tasks = _compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, set(), [])
assert rpm_tasks.to_install == ['installed1', 'installed2']
assert rpm_tasks.to_remove == ['removed1', 'removed2']
@@ -236,9 +235,9 @@ def test_actor_performs(monkeypatch):
_RPM = partial(RPM, epoch='', packager='', version='', release='', arch='', pgpsig='')
- installed_pkgs = DistributionSignedRPM(items=[
+ installed_rpms = [
_RPM(name='split-in'), _RPM(name='moved-in'), _RPM(name='removed')
- ])
+ ]
repositories_mapping = RepositoriesMapping(
mapping=[
@@ -251,7 +250,6 @@ def test_actor_performs(monkeypatch):
repo_type='rpm', channel='ga', rhui='', distro='rhel')]
)
- enabled_modules = EnabledModules(modules=[])
repo_facts = RepositoriesFacts(
repositories=[RepositoryFile(file='', data=[RepositoryData(repoid='rhel8-repo', name='RHEL8 repo')])]
)
@@ -260,7 +258,7 @@ def test_actor_performs(monkeypatch):
api,
"current_actor",
CurrentActorMocked(
- msgs=[installed_pkgs, repositories_mapping, enabled_modules, repo_facts],
+ msgs=[repositories_mapping, repo_facts],
src_ver="8.10",
dst_ver="9.1",
),
@@ -273,7 +271,7 @@ def test_actor_performs(monkeypatch):
monkeypatch.setattr(api, 'produce', produced_messages)
monkeypatch.setattr(reporting, 'create_report', created_report)
- pes_events_scanner.scan_pes_events(repomap_handler, set(), {'rhel8-repo'})
+ pes_events_scanner.scan_pes_events(repomap_handler, set(), {'rhel8-repo'}, installed_rpms, [])
assert produced_messages.called
@@ -299,14 +297,14 @@ def test_transaction_configuration_has_effect(monkeypatch):
)
packages = {_Pkg('pkg-a'), _Pkg('pkg-c')}
- _result = pes_events_scanner.apply_transaction_configuration(packages, transaction_cfg)
+ _result = pes_events_scanner._apply_transaction_configuration(packages, transaction_cfg)
result = {(p.name, p.repository, p.modulestream) for p in _result}
expected = {('pkg-a', None, None), ('pkg-b', None, None)}
assert result == expected
-def test_repository_blacklist_is_correctly_applied(monkeypatch):
+def test_repository_blocklist_is_correctly_applied(monkeypatch):
_Pkg = partial(Package, modulestream=None)
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
@@ -315,11 +313,10 @@ def test_repository_blacklist_is_correctly_applied(monkeypatch):
source_pkgs = {_Pkg('pkg-b', 'repo-b')}
target_pkgs = {_Pkg('pkg-a', 'repo-a'), _Pkg('pkg-b', 'repo-b'), _Pkg('pkg-c', 'repo-c'), _Pkg('pkg-d', 'repo-d')}
- blacklisted_repoids, target_pkgs = pes_events_scanner.remove_new_packages_from_blacklisted_repos(
+ target_pkgs = pes_events_scanner._remove_new_packages_from_blocklisted_repos(
source_pkgs, target_pkgs, {'repo-a', 'repo-b', 'repo-c'})
result = pkgs_into_tuples(target_pkgs)
- assert blacklisted_repoids == {'repo-a', 'repo-b', 'repo-c'}
assert result == {('pkg-b', 'repo-b', None), ('pkg-d', 'repo-d', None)}
assert reporting.create_report.called
@@ -340,17 +337,17 @@ def test_blacklisted_repoid_is_not_produced(monkeypatch):
(9, 0), (9, 1), []),
]
- monkeypatch.setattr(pes_events_scanner, 'get_installed_pkgs', lambda: installed_pkgs)
+ monkeypatch.setattr(pes_events_scanner, '_rpms_to_package_set', lambda rpm_list: installed_pkgs)
monkeypatch.setattr(pes_events_scanner, 'get_pes_events', lambda folder, filename: events)
- monkeypatch.setattr(pes_events_scanner, 'apply_transaction_configuration', lambda pkgs, transaction_cfg: pkgs)
- monkeypatch.setattr(pes_events_scanner, 'replace_pesids_with_repoids_in_packages',
+ monkeypatch.setattr(pes_events_scanner, '_apply_transaction_configuration', lambda pkgs, transaction_cfg: pkgs)
+ monkeypatch.setattr(pes_events_scanner, '_replace_pesids_with_repoids_in_packages',
lambda pkgs, src_pkgs_repoids, repo_map_msg, enabled_repoids: pkgs)
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
- setup_tasks = pes_events_scanner.scan_pes_events(None, {'blacklisted-rhel9'}, set())
+ setup_tasks = pes_events_scanner.scan_pes_events(None, {'blacklisted-rhel9'}, set(), [], [])
assert not reporting.create_report.called
@@ -383,7 +380,7 @@ def test_modularity_info_distinguishes_pkgs(monkeypatch, installed_pkgs, expecte
]
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
- target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 1)])
+ target_pkgs, dummy_demodularized_pkgs = _compute_packages_on_target_system(installed_pkgs, events, [(8, 1)])
assert pkgs_into_tuples(target_pkgs) == expected_target_pkgs
@@ -403,7 +400,7 @@ def test_pkgs_are_demodularized_when_crossing_major_version(monkeypatch):
Package('demodularized', 'repo', ('module-demodularized', 'stream'))
}
- target_pkgs, demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 0)])
+ target_pkgs, demodularized_pkgs = _compute_packages_on_target_system(installed_pkgs, events, [(8, 0)])
expected_target_pkgs = {
Package('modular', 'repo1-out', ('module2', 'stream')),
@@ -473,7 +470,7 @@ def test_remove_leapp_related_events(monkeypatch):
{Package('other-pkg-with-leapp-in-the-name', 'repoid-rhel8', None)}, (7, 0), (8, 0), []),
]
- out_events = pes_events_scanner.remove_leapp_related_events(in_events)
+ out_events = pes_events_scanner._remove_leapp_related_events(in_events)
assert out_events == expected_out_events
@@ -483,7 +480,7 @@ def test_transaction_configuration_is_applied(monkeypatch):
Package(name='split-in', repository='rhel7-base', modulestream=None),
Package(name='pkg-not-in-events', repository='rhel7-base', modulestream=None),
}
- monkeypatch.setattr(pes_events_scanner, 'get_installed_pkgs', lambda *args, **kwags: installed_pkgs)
+ monkeypatch.setattr(pes_events_scanner, '_rpms_to_package_set', lambda rpm_list: installed_pkgs)
Pkg = partial(Package, modulestream=None)
events = [
@@ -496,14 +493,13 @@ def test_transaction_configuration_is_applied(monkeypatch):
(7, 9), (8, 0), []),
]
monkeypatch.setattr(pes_events_scanner, 'get_pes_events', lambda *args, **kwargs: events)
- monkeypatch.setattr(pes_events_scanner, 'remove_leapp_related_events', lambda events: events)
- monkeypatch.setattr(pes_events_scanner, 'remove_undesired_events', lambda events, releases: events)
- monkeypatch.setattr(pes_events_scanner, '_get_enabled_modules', lambda *args: [])
- monkeypatch.setattr(pes_events_scanner, 'replace_pesids_with_repoids_in_packages',
+ monkeypatch.setattr(pes_events_scanner, '_remove_leapp_related_events', lambda events: events)
+ monkeypatch.setattr(pes_events_scanner, '_remove_undesired_events', lambda events, releases: events)
+ monkeypatch.setattr(pes_events_scanner, '_replace_pesids_with_repoids_in_packages',
lambda target_pkgs, repoids_of_source_pkgs, repo_map_msg, enabled_repoids: target_pkgs)
monkeypatch.setattr(pes_events_scanner,
- 'remove_new_packages_from_blacklisted_repos',
- lambda source_pkgs, target_pkgs, bl: (set(), target_pkgs))
+ '_remove_new_packages_from_blocklisted_repos',
+ lambda source_pkgs, target_pkgs, bl: target_pkgs)
msgs = [
RpmTransactionTasks(to_remove=['pkg-not-in-events']),
@@ -516,7 +512,7 @@ def test_transaction_configuration_is_applied(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())
- setup_tasks = pes_events_scanner.scan_pes_events(None, set(), set())
+ setup_tasks = pes_events_scanner.scan_pes_events(None, set(), set(), [], [])
assert api.produce.called == 1
assert setup_tasks is not None
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
index c0ca2d93..f4fbe32e 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/tests/test_targetcontentresolver.py
@@ -7,11 +7,14 @@ from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
CustomTargetRepository,
+ DistributionSignedRPM,
+ EnabledModules,
RepositoriesBlacklisted,
RepositoriesFacts,
RepositoriesSetupTasks,
RepositoryData,
- RepositoryFile
+ RepositoryFile,
+ RPM
)
from leapp.utils.deprecation import suppress_deprecation
@@ -29,6 +32,8 @@ def test_process_orchestration(monkeypatch):
to_enable=frozenset({'ext-repo'}), to_block=frozenset(), custom=frozenset()
)
fake_enabled_repoids = frozenset({'enabled-repo'})
+ fake_installed_rpms = []
+ fake_enabled_modules = []
fake_pes_repoids = frozenset({'pes-repo'})
class FakeInputData:
@@ -36,6 +41,8 @@ def test_process_orchestration(monkeypatch):
call_log.append('InputData')
self.external_tasks = fake_external_tasks
self.enabled_repoids = fake_enabled_repoids
+ self.installed_rpms = fake_installed_rpms
+ self.enabled_modules = fake_enabled_modules
def mock_init_repomap_handler():
call_log.append('_init_repomap_handler')
@@ -48,11 +55,13 @@ def test_process_orchestration(monkeypatch):
assert enabled_repoids is fake_enabled_repoids
return fake_blocklist
- def mock_scan_pes_events(repo_mapping, blacklisted_repoids, enabled_repoids):
+ def mock_scan_pes_events(repo_mapping, blacklisted_repoids, enabled_repoids, installed_rpms, enabled_modules):
call_log.append('scan_pes_events')
assert repo_mapping is fake_repomap_handler
assert blacklisted_repoids is fake_blocklist
assert enabled_repoids is fake_enabled_repoids
+ assert installed_rpms is fake_installed_rpms
+ assert enabled_modules is fake_enabled_modules
return fake_pes_repoids
def mock_setup_target_repos(repo_mapping, enabled_repoids, pes_requested_repoids,
@@ -96,6 +105,11 @@ def test_process_orchestration(monkeypatch):
]
+def _make_base_msgs():
+ """Create DistributionSignedRPM and EnabledModules messages required by InputData."""
+ return [DistributionSignedRPM(items=[]), EnabledModules(modules=[])]
+
+
def _make_repo_facts(repoids_enabled=None, repoids_disabled=None):
repos_data = []
for repoid in (repoids_enabled or []):
@@ -112,7 +126,7 @@ def test_inputdata_collects_enabled_repoids(monkeypatch):
repoids_enabled=['repo-a', 'repo-b'],
repoids_disabled=['repo-c'],
)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts] + _make_base_msgs()))
indata = InputData()
@@ -120,7 +134,7 @@ def test_inputdata_collects_enabled_repoids(monkeypatch):
def test_inputdata_raises_when_repofacts_missing(monkeypatch):
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=_make_base_msgs()))
with pytest.raises(StopActorExecutionError):
InputData()
@@ -135,7 +149,7 @@ def test_inputdata_aggregates_external_tasks(monkeypatch):
monkeypatch.setattr(
api, 'current_actor',
- CurrentActorMocked(msgs=[repo_facts, setup_tasks_1, setup_tasks_2, custom_1, custom_2])
+ CurrentActorMocked(msgs=[repo_facts, setup_tasks_1, setup_tasks_2, custom_1, custom_2] + _make_base_msgs())
)
indata = InputData()
@@ -147,7 +161,7 @@ def test_inputdata_aggregates_external_tasks(monkeypatch):
def test_inputdata_empty_external_tasks(monkeypatch):
repo_facts = _make_repo_facts(repoids_enabled=['repo-a'])
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts] + _make_base_msgs()))
indata = InputData()
@@ -161,7 +175,7 @@ def test_inputdata_warns_on_duplicate_repofacts(monkeypatch):
repo_facts_2 = _make_repo_facts(repoids_enabled=['repo-b'])
monkeypatch.setattr(
api, 'current_actor',
- CurrentActorMocked(msgs=[repo_facts_1, repo_facts_2])
+ CurrentActorMocked(msgs=[repo_facts_1, repo_facts_2] + _make_base_msgs())
)
monkeypatch.setattr(api, 'current_logger', logger_mocked())
@@ -179,7 +193,7 @@ def test_inputdata_consumes_deprecated_blacklisted(monkeypatch):
monkeypatch.setattr(
api, 'current_actor',
- CurrentActorMocked(msgs=[repo_facts, blacklisted])
+ CurrentActorMocked(msgs=[repo_facts, blacklisted] + _make_base_msgs())
)
indata = InputData()
@@ -196,7 +210,7 @@ def test_inputdata_merges_blacklisted_with_setup_tasks(monkeypatch):
monkeypatch.setattr(
api, 'current_actor',
- CurrentActorMocked(msgs=[repo_facts, setup_tasks, blacklisted])
+ CurrentActorMocked(msgs=[repo_facts, setup_tasks, blacklisted] + _make_base_msgs())
)
indata = InputData()
--
2.54.0

View File

@ -0,0 +1,52 @@
From a3a0c1d59f72c5c367b2541a510e01d08c470266 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 22 Jun 2026 15:02:05 +0200
Subject: [PATCH 105/105] [9/9] targetcontentresolver: Make TODO notes
The current refactoring progressed significantly, but there are still
anti-patterns and bugs in the code, which cannot be easily resolved
without the second part of the refactoring. Due to lack of time, we
decided to stop here, considering the situation much better in
comparison to the previous state. Making notes in the code about
additional expected changes so we do not forget them :)
Co-authored-by: Matej Matuska <mmatuska@redhat.com>
Jira: RHEL-115867
Signed-off-by: Petr Stodulka <pstodulk@redhat.com>
---
.../libraries/targetcontentresolver.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
index 83460d99..f969d0f9 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
+++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/targetcontentresolver.py
@@ -142,6 +142,25 @@ def process():
source repos, installed package repos, and custom/blacklisted repo
configuration.
"""
+ # TODO: update pes_even_scanner to work just with PESIDs instead of repoids.
+ # The blocklisted events needs to be then evaluated / calculated after
+ # the setuptargetrepos calculations. Similarly we can update the blocklist
+ # library. The setuptargetrepos tells exactly what's going to be allowed
+ # (repoids) and based on that we can finish the calculations.
+ # This includes:
+ # * all reports will be generated (or directly called) from the orchestrator
+ # * all/most of messages should be produced in the orchestrator
+ # * we will need at least 2 public functions in pes_events_scanner
+ # # and repositoriesblocklist libs:
+ # # * these that we have right now
+ # # * additional ones for the final calculation / filtering based on
+ # # the real list of blocklisted repoids
+ # TL;DR; the workflow will be like this:
+ # * Make interim pes-events calculation (work just with PESIDS)
+ # * Make interim blocklist calculation
+ # * Calc expected & really blocked target repositories
+ # * take the result and finish the calculation
+ # * generate reports & messages
indata = InputData()
repomap_handler = _init_repomap_handler()
blocklisted_repoids = repositoriesblocklist.compute_blocklist(
--
2.54.0

View File

@ -52,7 +52,7 @@ py2_byte_compile "%1" "%2"}
Name: leapp-repository
Version: 0.24.0
Release: 2%{?dist}
Release: 3%{?dist}
Summary: Repositories for leapp
License: ASL 2.0
@ -110,7 +110,68 @@ Patch0041: 0041-python-tests-deps-update-version-requirements-per-py.patch
Patch0042: 0042-Drop-dependency-on-mock.patch
Patch0043: 0043-remove-single-quoting-from-remediation-commands-1520.patch
Patch0044: 0044-Update-leapp-data-data-stream-4.3-CTC-1.patch
# CTC 2 - 1
Patch0045: 0045-repofileutils-disable-ConfigParser-interpolation-whe.patch
Patch0046: 0046-Drop-six-dependency-and-simplify-config-parsing-to-P.patch
Patch0047: 0047-mounting-ignore-failures-when-callling-mount-a.patch
Patch0048: 0048-mount_unit_generator-do-not-make-nofail-mounts-requi.patch
Patch0049: 0049-leapp_resume.service-use-multi-user.target-instead-o.patch
Patch0050: 0050-LiveMode-update-do-upgrade.sh-to-reflect-upstream-ch.patch
Patch0051: 0051-LiveMode-improve-do-upgrade.sh-output-when-.noreboot.patch
Patch0052: 0052-multipathutil-handle-the-overrides-protocol-subsecti.patch
Patch0053: 0053-multipath-move-config_reader-Actor-and-8to9-Models-t.patch
Patch0054: 0054-multipath-Add-MultipathConfig9to10-and-MultipathConf.patch
Patch0055: 0055-multipath-Add-MultipathConfRead9to10-config_reader-a.patch
Patch0056: 0056-multipath-Add-MultipathConfCheck9to10-mpath_conf_che.patch
Patch0057: 0057-multipath-Add-MultipathUpgradeConfUpdate9to10-mpath_.patch
Patch0058: 0058-multipath-Add-bindings-wwids-prkeys-file-copying-to-.patch
Patch0059: 0059-multipath-Add-mpathfiles-library-and-use-it-in-el9to.patch
Patch0060: 0060-model-upgrade_cmdline-add-to_remove-field.patch
Patch0061: 0061-checkluks-emit-messages-only-once.patch
Patch0062: 0062-checkluks-remove-rd.luks.uuid-from-the-upgrade-entry.patch
Patch0063: 0063-conftest-exit-actor-context-when-switching-repos.patch
Patch0064: 0064-testutils-logger_mocked-accepts-kwargs.patch
Patch0065: 0065-conftest-Add-leapp_tmpdir-fixture-for-standardized-t.patch
Patch0066: 0066-Reorganize-DNF-libraries-into-dnflibs-package.patch
Patch0067: 0067-Update-imports-to-use-new-dnflibs-modules.patch
Patch0068: 0068-dnflibs-introduce-DNFError-exception.patch
Patch0069: 0069-dnflibs.dnfplugin-Drop-dead-code-_handle_transaction.patch
Patch0070: 0070-dnflibs.dnfplugin-Drop-deprecated-DNF_PLUGIN_PATH.patch
Patch0071: 0071-dnflibs.dnfplugin-Introduce-new-DNF-exceptions-and-t.patch
Patch0072: 0072-dnflibs-dnfmodule-Move-init-of-dnf.Base-to-dnflibs-a.patch
Patch0073: 0073-dnflibs.dnfconfig-Raise-proper-exceptions-and-add-te.patch
Patch0074: 0074-DOCSTRINGS-Document-DNF-related-exception-propagatio.patch
Patch0075: 0075-pulseaudiocheck-fix-for-users-with-None-home-directo.patch
Patch0076: 0076-fix-target_initramfs-always-regenerate-initramfs-153.patch
Patch0077: 0077-Add-AI-assistant-harness-with-AGENTS.md-and-task-ski.patch
Patch0078: 0078-el8toel9-networkdeprecations-warn-about-ifcfg-device.patch
Patch0079: 0079-Remove-enforcing-1-from-upgrade-and-target-kernel-bo.patch
Patch0080: 0080-Fix-utils-actor_path.py-crash-on-invalid-repo-paths.patch
Patch0081: 0081-fix-breadcrumbs-use-distro-aware-OS-names-for-source.patch
Patch0082: 0082-leapp_resume.service-use-install_exec_t-SELinux-cont.patch
Patch0083: 0083-config-rhui-Add-config-for-setting-rpm-gpg-keys-to-r.patch
Patch0084: 0084-Add-inhibitor-for-invalid-fstab-entries-on-pseudo-fi.patch
Patch0085: 0085-lib-version-Remove-deprecated-is_rhel_realtime-funct.patch
Patch0086: 0086-lib-kernel-Fix-rt-kernel-detection-on-centos.patch
Patch0087: 0087-Add-CLAUDE.md-and-YAML-frontmatter-to-skills-for-Cla.patch
Patch0088: 0088-Add-inhibitor-for-incorrect-var-run-symlink-configur.patch
Patch0089: 0089-checkvarrunsymlink-Log-warning-when-var-run-is-missi.patch
Patch0090: 0090-Add-scandnfcomps-actor-to-scan-installed-DNF-comps.patch
Patch0091: 0091-IPU-9-10-Fix-the-upgrade-of-Graphical-Server.patch
Patch0092: 0092-Bump-leapp-framework.patch
Patch0093: 0093-raid-mdadm-include-configuration-in-initramfs.patch
Patch0094: 0094-feat-upgrade_initramfs_gen-write-rd.md.uuids-into-cm.patch
Patch0095: 0095-Update-actions-checkout-action-to-v7.patch
Patch0096: 0096-Update-distro-values-for-rhui-jobs-follow-up-on-9to1.patch
Patch0097: 0097-1-9-Merge-and-refactor-of-repo-related-actors.patch
Patch0098: 0098-2-9-targetcontentresolver-Init-message-consumption-c.patch
Patch0099: 0099-3-9-targetcontentresolver-RepositoriesBlacklisted-us.patch
Patch0100: 0100-4-9-targetcontentresolver-repositoriesblacklist-rede.patch
Patch0101: 0101-5-9-repositoriesmapping-scan_repositories-load_repos.patch
Patch0102: 0102-6-9-targetcontentresolver-operate-with-RepoMapDataHa.patch
Patch0103: 0103-7-9-targetcontentresolver-setuptargetrepos-minor-upd.patch
Patch0104: 0104-8-9-targetcontentresolver-pes_events_scanner-refacto.patch
Patch0105: 0105-9-9-targetcontentresolver-Make-TODO-notes.patch
%description
%{summary}
@ -165,7 +226,7 @@ Requires: leapp-repository-dependencies = %{leapp_repo_deps}
# IMPORTANT: this is capability provided by the leapp framework rpm.
# Check that 'version' instead of the real framework rpm version.
Requires: leapp-framework >= 6.5
Requires: leapp-framework >= 6.6
# Since we provide sub-commands for the leapp utility, we expect the leapp
# tool to be installed as well.
@ -335,7 +396,67 @@ Requires: fapolicyd
%patch -P 0042 -p1
%patch -P 0043 -p1
%patch -P 0044 -p1
%patch -P 0045 -p1
%patch -P 0046 -p1
%patch -P 0047 -p1
%patch -P 0048 -p1
%patch -P 0049 -p1
%patch -P 0050 -p1
%patch -P 0051 -p1
%patch -P 0052 -p1
%patch -P 0053 -p1
%patch -P 0054 -p1
%patch -P 0055 -p1
%patch -P 0056 -p1
%patch -P 0057 -p1
%patch -P 0058 -p1
%patch -P 0059 -p1
%patch -P 0060 -p1
%patch -P 0061 -p1
%patch -P 0062 -p1
%patch -P 0063 -p1
%patch -P 0064 -p1
%patch -P 0065 -p1
%patch -P 0066 -p1
%patch -P 0067 -p1
%patch -P 0068 -p1
%patch -P 0069 -p1
%patch -P 0070 -p1
%patch -P 0071 -p1
%patch -P 0072 -p1
%patch -P 0073 -p1
%patch -P 0074 -p1
%patch -P 0075 -p1
%patch -P 0076 -p1
%patch -P 0077 -p1
%patch -P 0078 -p1
%patch -P 0079 -p1
%patch -P 0080 -p1
%patch -P 0081 -p1
%patch -P 0082 -p1
%patch -P 0083 -p1
%patch -P 0084 -p1
%patch -P 0085 -p1
%patch -P 0086 -p1
%patch -P 0087 -p1
%patch -P 0088 -p1
%patch -P 0089 -p1
%patch -P 0090 -p1
%patch -P 0091 -p1
%patch -P 0092 -p1
%patch -P 0093 -p1
%patch -P 0094 -p1
%patch -P 0095 -p1
%patch -P 0096 -p1
%patch -P 0097 -p1
%patch -P 0098 -p1
%patch -P 0099 -p1
%patch -P 0100 -p1
%patch -P 0101 -p1
%patch -P 0102 -p1
%patch -P 0103 -p1
%patch -P 0104 -p1
%patch -P 0105 -p1
%build
cp -a leapp*deps*el%{next_major_ver}.noarch.rpm repos/system_upgrade/%{repo_shortname}/files/bundled-rpms/
@ -431,6 +552,34 @@ fi
%changelog
- Thu Jul 25 2026 Karolina Kula <kkula@redhat.com> - 0.24.0-3
- Requires leapp-framework 6.6+
- Initial implementation of upgrades on systems with configured Software RAID
- Install the kernel variant matching the system memory page size on ARM systems
- Check and migrate the multipath configuration when possible
- Fix multipath device initialization race in the upgrade environment
- Detect misconfigured /var/run on the source system
- Detect misconfigured kernel & systemd API mountpoints in FSTAB
- Drop the inconsistent report about used CRB repositories
- Ensure SELinux is set to permissive mode during the upgrade even when enforcing=1 is set on the kernel cmdline
- Fix additional AVC SELinux warnings
- Fix mount failures for FSTAB entries with the `nofail` option specified
- Fix source and target distribution names in /etc/migration-results
- Fix target system initramfs containing outdated configuration files in some circumstances
- Fix the pulseaudio scan for users with no home directory
- Fix unwanted suspend mode after 15 minutes of inactivity after the upgrade on systems with graphical environment
- Fix upgrade crashing when dnf repofiles contain URL encoded characters
- Fix upgrade incorrectly resuming during SELinux relabeling
- Fix upgrades for systems with /boot/efi on a Software RAID
- Fix upgrades on systems with multiple LUKS devices
- Fix upgrades with the RealTime kernel on CentOS Stream
- Introduce rhui.obsolete_gpg_keys configuration option for the removal of obsoleted RPM GPG keys of RHUI Cloud providers
- LiveMode: Fix system version determination in the upgrade environment on CentOS Stream
- LiveMode: Fix upgrade getting stuck when the `.leapp_upgrade_failed` file exists
- Unify behaviour of upgrades on systems with enabled CRB repositories and explicitly required CRB repositories by user during the upgrade
- Resolves: RHEL-111860, RHEL-115867, RHEL-121205, RHEL-147438, RHEL-148697, RHEL-151509,
RHEL-154369, RHEL-154370, RHEL-156580, RHEL-161560, RHEL-161684, RHEL-174874
* Fri Apr 17 2026 Petr Stodulka <pstodulk@redhat.com> - 0.24.0-2
- Introduce new upgrade path 9.9 -> 10.3
- Requires leapp-framework 6.5+