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:
parent
5152897f93
commit
dc04575ed7
@ -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
|
||||
|
||||
157
0046-Drop-six-dependency-and-simplify-config-parsing-to-P.patch
Normal file
157
0046-Drop-six-dependency-and-simplify-config-parsing-to-P.patch
Normal 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
|
||||
|
||||
102
0047-mounting-ignore-failures-when-callling-mount-a.patch
Normal file
102
0047-mounting-ignore-failures-when-callling-mount-a.patch
Normal 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
|
||||
|
||||
142
0048-mount_unit_generator-do-not-make-nofail-mounts-requi.patch
Normal file
142
0048-mount_unit_generator-do-not-make-nofail-mounts-requi.patch
Normal 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
|
||||
|
||||
133
0049-leapp_resume.service-use-multi-user.target-instead-o.patch
Normal file
133
0049-leapp_resume.service-use-multi-user.target-instead-o.patch
Normal 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
|
||||
|
||||
334
0050-LiveMode-update-do-upgrade.sh-to-reflect-upstream-ch.patch
Normal file
334
0050-LiveMode-update-do-upgrade.sh-to-reflect-upstream-ch.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
324
0053-multipath-move-config_reader-Actor-and-8to9-Models-t.patch
Normal file
324
0053-multipath-move-config_reader-Actor-and-8to9-Models-t.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
1897
0055-multipath-Add-MultipathConfRead9to10-config_reader-a.patch
Normal file
1897
0055-multipath-Add-MultipathConfRead9to10-config_reader-a.patch
Normal file
File diff suppressed because it is too large
Load Diff
522
0056-multipath-Add-MultipathConfCheck9to10-mpath_conf_che.patch
Normal file
522
0056-multipath-Add-MultipathConfCheck9to10-mpath_conf_che.patch
Normal 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
|
||||
|
||||
659
0057-multipath-Add-MultipathUpgradeConfUpdate9to10-mpath_.patch
Normal file
659
0057-multipath-Add-MultipathUpgradeConfUpdate9to10-mpath_.patch
Normal 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
|
||||
|
||||
567
0058-multipath-Add-bindings-wwids-prkeys-file-copying-to-.patch
Normal file
567
0058-multipath-Add-bindings-wwids-prkeys-file-copying-to-.patch
Normal 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
|
||||
|
||||
228
0059-multipath-Add-mpathfiles-library-and-use-it-in-el9to.patch
Normal file
228
0059-multipath-Add-mpathfiles-library-and-use-it-in-el9to.patch
Normal 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
|
||||
|
||||
96
0060-model-upgrade_cmdline-add-to_remove-field.patch
Normal file
96
0060-model-upgrade_cmdline-add-to_remove-field.patch
Normal 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
|
||||
|
||||
81
0061-checkluks-emit-messages-only-once.patch
Normal file
81
0061-checkluks-emit-messages-only-once.patch
Normal 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
|
||||
|
||||
256
0062-checkluks-remove-rd.luks.uuid-from-the-upgrade-entry.patch
Normal file
256
0062-checkluks-remove-rd.luks.uuid-from-the-upgrade-entry.patch
Normal 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
|
||||
|
||||
114
0063-conftest-exit-actor-context-when-switching-repos.patch
Normal file
114
0063-conftest-exit-actor-context-when-switching-repos.patch
Normal 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
|
||||
|
||||
52
0064-testutils-logger_mocked-accepts-kwargs.patch
Normal file
52
0064-testutils-logger_mocked-accepts-kwargs.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
1748
0066-Reorganize-DNF-libraries-into-dnflibs-package.patch
Normal file
1748
0066-Reorganize-DNF-libraries-into-dnflibs-package.patch
Normal file
File diff suppressed because it is too large
Load Diff
256
0067-Update-imports-to-use-new-dnflibs-modules.patch
Normal file
256
0067-Update-imports-to-use-new-dnflibs-modules.patch
Normal 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
|
||||
|
||||
35
0068-dnflibs-introduce-DNFError-exception.patch
Normal file
35
0068-dnflibs-introduce-DNFError-exception.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
107
0070-dnflibs.dnfplugin-Drop-deprecated-DNF_PLUGIN_PATH.patch
Normal file
107
0070-dnflibs.dnfplugin-Drop-deprecated-DNF_PLUGIN_PATH.patch
Normal 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
|
||||
|
||||
903
0071-dnflibs.dnfplugin-Introduce-new-DNF-exceptions-and-t.patch
Normal file
903
0071-dnflibs.dnfplugin-Introduce-new-DNF-exceptions-and-t.patch
Normal 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
|
||||
|
||||
637
0072-dnflibs-dnfmodule-Move-init-of-dnf.Base-to-dnflibs-a.patch
Normal file
637
0072-dnflibs-dnfmodule-Move-init-of-dnf.Base-to-dnflibs-a.patch
Normal 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
|
||||
|
||||
363
0073-dnflibs.dnfconfig-Raise-proper-exceptions-and-add-te.patch
Normal file
363
0073-dnflibs.dnfconfig-Raise-proper-exceptions-and-add-te.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
393
0077-Add-AI-assistant-harness-with-AGENTS.md-and-task-ski.patch
Normal file
393
0077-Add-AI-assistant-harness-with-AGENTS.md-and-task-ski.patch
Normal 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
|
||||
|
||||
230
0078-el8toel9-networkdeprecations-warn-about-ifcfg-device.patch
Normal file
230
0078-el8toel9-networkdeprecations-warn-about-ifcfg-device.patch
Normal 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
|
||||
|
||||
441
0079-Remove-enforcing-1-from-upgrade-and-target-kernel-bo.patch
Normal file
441
0079-Remove-enforcing-1-from-upgrade-and-target-kernel-bo.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
119
0081-fix-breadcrumbs-use-distro-aware-OS-names-for-source.patch
Normal file
119
0081-fix-breadcrumbs-use-distro-aware-OS-names-for-source.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
380
0083-config-rhui-Add-config-for-setting-rpm-gpg-keys-to-r.patch
Normal file
380
0083-config-rhui-Add-config-for-setting-rpm-gpg-keys-to-r.patch
Normal 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
|
||||
|
||||
274
0084-Add-inhibitor-for-invalid-fstab-entries-on-pseudo-fi.patch
Normal file
274
0084-Add-inhibitor-for-invalid-fstab-entries-on-pseudo-fi.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
142
0086-lib-kernel-Fix-rt-kernel-detection-on-centos.patch
Normal file
142
0086-lib-kernel-Fix-rt-kernel-detection-on-centos.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
249
0088-Add-inhibitor-for-incorrect-var-run-symlink-configur.patch
Normal file
249
0088-Add-inhibitor-for-incorrect-var-run-symlink-configur.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
419
0090-Add-scandnfcomps-actor-to-scan-installed-DNF-comps.patch
Normal file
419
0090-Add-scandnfcomps-actor-to-scan-installed-DNF-comps.patch
Normal 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
|
||||
|
||||
165
0091-IPU-9-10-Fix-the-upgrade-of-Graphical-Server.patch
Normal file
165
0091-IPU-9-10-Fix-the-upgrade-of-Graphical-Server.patch
Normal 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
|
||||
|
||||
26
0092-Bump-leapp-framework.patch
Normal file
26
0092-Bump-leapp-framework.patch
Normal 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
|
||||
|
||||
491
0093-raid-mdadm-include-configuration-in-initramfs.patch
Normal file
491
0093-raid-mdadm-include-configuration-in-initramfs.patch
Normal 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
|
||||
|
||||
235
0094-feat-upgrade_initramfs_gen-write-rd.md.uuids-into-cm.patch
Normal file
235
0094-feat-upgrade_initramfs_gen-write-rd.md.uuids-into-cm.patch
Normal 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
|
||||
|
||||
53
0095-Update-actions-checkout-action-to-v7.patch
Normal file
53
0095-Update-actions-checkout-action-to-v7.patch
Normal 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
|
||||
|
||||
115
0096-Update-distro-values-for-rhui-jobs-follow-up-on-9to1.patch
Normal file
115
0096-Update-distro-values-for-rhui-jobs-follow-up-on-9to1.patch
Normal 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
|
||||
|
||||
2493
0097-1-9-Merge-and-refactor-of-repo-related-actors.patch
Normal file
2493
0097-1-9-Merge-and-refactor-of-repo-related-actors.patch
Normal file
File diff suppressed because it is too large
Load Diff
752
0098-2-9-targetcontentresolver-Init-message-consumption-c.patch
Normal file
752
0098-2-9-targetcontentresolver-Init-message-consumption-c.patch
Normal 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
|
||||
|
||||
309
0099-3-9-targetcontentresolver-RepositoriesBlacklisted-us.patch
Normal file
309
0099-3-9-targetcontentresolver-RepositoriesBlacklisted-us.patch
Normal 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
|
||||
|
||||
786
0100-4-9-targetcontentresolver-repositoriesblacklist-rede.patch
Normal file
786
0100-4-9-targetcontentresolver-repositoriesblacklist-rede.patch
Normal 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
|
||||
|
||||
288
0101-5-9-repositoriesmapping-scan_repositories-load_repos.patch
Normal file
288
0101-5-9-repositoriesmapping-scan_repositories-load_repos.patch
Normal 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
|
||||
|
||||
615
0102-6-9-targetcontentresolver-operate-with-RepoMapDataHa.patch
Normal file
615
0102-6-9-targetcontentresolver-operate-with-RepoMapDataHa.patch
Normal 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
|
||||
|
||||
262
0103-7-9-targetcontentresolver-setuptargetrepos-minor-upd.patch
Normal file
262
0103-7-9-targetcontentresolver-setuptargetrepos-minor-upd.patch
Normal 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
|
||||
|
||||
849
0104-8-9-targetcontentresolver-pes_events_scanner-refacto.patch
Normal file
849
0104-8-9-targetcontentresolver-pes_events_scanner-refacto.patch
Normal 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
|
||||
|
||||
52
0105-9-9-targetcontentresolver-Make-TODO-notes.patch
Normal file
52
0105-9-9-targetcontentresolver-Make-TODO-notes.patch
Normal 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
|
||||
|
||||
@ -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+
|
||||
|
||||
Loading…
Reference in New Issue
Block a user