Compare commits

..

No commits in common. "c8" and "c8-beta" have entirely different histories.
c8 ... c8-beta

112 changed files with 277 additions and 36462 deletions

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
SOURCES/deps-pkgs-13.tar.gz
SOURCES/leapp-repository-0.24.0.tar.gz
SOURCES/deps-pkgs-10.tar.gz
SOURCES/leapp-repository-0.20.0.tar.gz

View File

@ -1,2 +1,2 @@
3590b33b4a79ebe62f5cfa0eeca7efb41d526498 SOURCES/deps-pkgs-13.tar.gz
2271b92541564ebd07a038a8b45e5a33d5f8b9c9 SOURCES/leapp-repository-0.24.0.tar.gz
d520ada12294e4dd8837c81f92d4c184ab403d51 SOURCES/deps-pkgs-10.tar.gz
185bbb040dba48e1ea2d6c627133af594378afd4 SOURCES/leapp-repository-0.20.0.tar.gz

View File

@ -1,112 +0,0 @@
From 34aeae1e023e61345a1bb020b42231a79a0be4a8 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Thu, 26 Feb 2026 17:52:23 +0100
Subject: [PATCH 01/44] Remove legacy overlay solution leftovers
The solution was removed in 93429, however there are some leftovers.
---
.../libraries/userspacegen.py | 19 -------------------
.../common/libraries/dnfplugin.py | 15 ++++-----------
.../common/libraries/overlaygen.py | 6 ------
3 files changed, 4 insertions(+), 36 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
index 66843645..7dbadae2 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
@@ -178,26 +178,7 @@ def _import_gpg_keys(context, install_root_dir, target_major_version):
)
-def _handle_transaction_err_msg_size_old(err):
- # NOTE(pstodulk): This is going to be removed in future!
-
- article_section = 'Generic case'
- xfs_info = next(api.consume(XFSPresence), XFSPresence())
- 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_size(err):
- if get_env('LEAPP_OVL_LEGACY', '0') == '1':
- _handle_transaction_err_msg_size_old(err)
- return # not needed actually as the above function raises error, but for visibility
NO_SPACE_STR = 'more space needed on the'
# Disk Requirements:
diff --git a/repos/system_upgrade/common/libraries/dnfplugin.py b/repos/system_upgrade/common/libraries/dnfplugin.py
index d50dbbce..b91bcbe7 100644
--- a/repos/system_upgrade/common/libraries/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnfplugin.py
@@ -7,7 +7,6 @@ import shutil
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import dnfconfig, guards, mounting, overlaygen, rhsm, utils
-from leapp.libraries.common.config import get_env
from leapp.libraries.common.config.version import get_target_major_version, get_target_version
from leapp.libraries.common.gpg import is_nogpgcheck_set
from leapp.libraries.stdlib import api, CalledProcessError, config
@@ -166,16 +165,10 @@ def _handle_transaction_err_msg_old(stage, xfs_info, err):
raise StopActorExecutionError(message=message, details=details)
-def _handle_transaction_err_msg(stage, xfs_info, err, is_container=False):
- # ignore the fallback when the error is related to the container issue
- # e.g. installation of packages inside the container; so it's unrelated
- # to the upgrade transactions.
- if get_env('LEAPP_OVL_LEGACY', '0') == '1' and not is_container:
- _handle_transaction_err_msg_old(stage, xfs_info, err)
- return # not needed actually as the above function raises error, but for visibility
+def _handle_transaction_err_msg(err, is_container=False):
NO_SPACE_STR = 'more space needed on the'
- message = 'DNF execution failed with non zero exit code.'
if NO_SPACE_STR not in err.stderr:
+ message = 'DNF execution failed with non zero exit code.'
# if there was a problem reaching repos and proxy is configured in DNF/YUM configs, the
# proxy is likely the problem.
# NOTE(mmatuska): We can't consistently detect there was a problem reaching some repos,
@@ -308,7 +301,7 @@ def _transaction(context, stage, target_repoids, tasks, plugin_info, xfs_info,
)
except CalledProcessError as e:
api.current_logger().error('Cannot calculate, check, test, or perform the upgrade transaction.')
- _handle_transaction_err_msg(stage, xfs_info, e, is_container=False)
+ _handle_transaction_err_msg(e, is_container=False)
finally:
if stage == 'check':
backup_debug_data(context=context)
@@ -384,7 +377,7 @@ def install_initramdisk_requirements(packages, target_userspace_info, used_repos
api.current_logger().error(
'Cannot install packages in the target container required to build the upgrade initramfs.'
)
- _handle_transaction_err_msg('', None, e, is_container=True)
+ _handle_transaction_err_msg(e, is_container=True)
def perform_transaction_install(target_userspace_info, storage_info, used_repos, tasks, plugin_info, xfs_info):
diff --git a/repos/system_upgrade/common/libraries/overlaygen.py b/repos/system_upgrade/common/libraries/overlaygen.py
index f0d0ba1d..3091ab5b 100644
--- a/repos/system_upgrade/common/libraries/overlaygen.py
+++ b/repos/system_upgrade/common/libraries/overlaygen.py
@@ -594,12 +594,6 @@ def create_source_overlay(
in the OVERLAY_DO_NOT_MOUNT set. Such prepared OVL images are then composed
together to reflect the real host filesystem. In the end everything is cleaned.
- The new solution can be now problematic for system with too many partitions
- and loop devices. For such systems we keep for now the possibility of the
- fallback to an old solution, which has however number of issues that are
- fixed by the new design. To fallback to the old solution, set envar:
- LEAPP_OVL_LEGACY=1
-
Disk images created for OVL are formatted with XFS by default. In case of
problems, it's possible to switch to Ext4 FS using:
LEAPP_OVL_IMG_FS_EXT4=1
--
2.53.0

View File

@ -0,0 +1,251 @@
From 921c06892f7550a3a8e2b3fe941c6272bdacf88d Mon Sep 17 00:00:00 2001
From: mhecko <mhecko@redhat.com>
Date: Thu, 15 Feb 2024 09:56:27 +0100
Subject: [PATCH] rhui: do not bootstrap target client on aws
Bootstrapping target RHUI client now requires installing the entire
RHEL8 RPM stack. Threfore, do not try installing target client
and instead rely only on the files from our leapp-rhui-aws package.
---
.../cloud/checkrhui/libraries/checkrhui.py | 6 +-
.../libraries/userspacegen.py | 104 ++++++++++++++----
.../system_upgrade/common/models/rhuiinfo.py | 7 ++
3 files changed, 92 insertions(+), 25 deletions(-)
diff --git a/repos/system_upgrade/common/actors/cloud/checkrhui/libraries/checkrhui.py b/repos/system_upgrade/common/actors/cloud/checkrhui/libraries/checkrhui.py
index 84ab40e3..e1c158c7 100644
--- a/repos/system_upgrade/common/actors/cloud/checkrhui/libraries/checkrhui.py
+++ b/repos/system_upgrade/common/actors/cloud/checkrhui/libraries/checkrhui.py
@@ -142,7 +142,11 @@ def customize_rhui_setup_for_aws(rhui_family, setup_info):
target_version = version.get_target_major_version()
if target_version == '8':
- return # The rhel8 plugin is packed into leapp-rhui-aws as we need python2 compatible client
+ # RHEL8 rh-amazon-rhui-client depends on amazon-libdnf-plugin that depends
+ # essentially on the entire RHEL8 RPM stack, so we cannot just swap the clients
+ # The leapp-rhui-aws will provide all necessary files to access entire RHEL8 content
+ setup_info.bootstrap_target_client = False
+ return
amazon_plugin_copy_task = CopyFile(src='/usr/lib/python3.9/site-packages/dnf-plugins/amazon-id.py',
dst='/usr/lib/python3.6/site-packages/dnf-plugins/')
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
index d917bfd5..d60bc75f 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
@@ -853,9 +853,9 @@ def _get_rhui_available_repoids(context, cloud_repo):
return set(repoids)
-def get_copy_location_from_copy_in_task(context, copy_task):
+def get_copy_location_from_copy_in_task(context_basepath, copy_task):
basename = os.path.basename(copy_task.src)
- dest_in_container = context.full_path(copy_task.dst)
+ dest_in_container = os.path.join(context_basepath, copy_task.dst)
if os.path.isdir(dest_in_container):
return os.path.join(copy_task.dst, basename)
return copy_task.dst
@@ -871,7 +871,10 @@ def _get_rh_available_repoids(context, indata):
# If we are upgrading a RHUI system, check what repositories are provided by the (already installed) target clients
if indata and indata.rhui_info:
- files_provided_by_clients = _query_rpm_for_pkg_files(context, indata.rhui_info.target_client_pkg_names)
+ setup_info = indata.rhui_info.target_client_setup_info
+ target_content_access_files = set()
+ if setup_info.bootstrap_target_client:
+ target_content_access_files = _query_rpm_for_pkg_files(context, indata.rhui_info.target_client_pkg_names)
def is_repofile(path):
return os.path.dirname(path) == '/etc/yum.repos.d' and os.path.basename(path).endswith('.repo')
@@ -884,24 +887,33 @@ def _get_rh_available_repoids(context, indata):
yum_repos_d = context.full_path('/etc/yum.repos.d')
all_repofiles = {os.path.join(yum_repos_d, path) for path in os.listdir(yum_repos_d) if path.endswith('.repo')}
- client_repofiles = {context.full_path(path) for path in files_provided_by_clients if is_repofile(path)}
+ api.current_logger().debug('(RHUI Setup) All available repofiles: {0}'.format(' '.join(all_repofiles)))
+
+ target_access_repofiles = {
+ context.full_path(path) for path in target_content_access_files if is_repofile(path)
+ }
# Exclude repofiles used to setup the target rhui access as on some platforms the repos provided by
# the client are not sufficient to install the client into target userspace (GCP)
rhui_setup_repofile_tasks = [task for task in setup_tasks if task.src.endswith('repo')]
rhui_setup_repofiles = (
- get_copy_location_from_copy_in_task(context, copy_task) for copy_task in rhui_setup_repofile_tasks
+ get_copy_location_from_copy_in_task(context.base_dir, copy) for copy in rhui_setup_repofile_tasks
)
rhui_setup_repofiles = {context.full_path(repofile) for repofile in rhui_setup_repofiles}
- foreign_repofiles = all_repofiles - client_repofiles - rhui_setup_repofiles
+ foreign_repofiles = all_repofiles - target_access_repofiles - rhui_setup_repofiles
+
+ api.current_logger().debug(
+ 'The following repofiles are considered as unknown to'
+ ' the target RHUI content setup and will be ignored: {0}'.format(' '.join(foreign_repofiles))
+ )
# Rename non-client repofiles so they will not be recognized when running dnf repolist
for foreign_repofile in foreign_repofiles:
os.rename(foreign_repofile, '{0}.back'.format(foreign_repofile))
try:
- dnf_cmd = ['dnf', 'repolist', '--releasever', target_ver, '-v']
+ dnf_cmd = ['dnf', 'repolist', '--releasever', target_ver, '-v', '--enablerepo', '*']
repolist_result = context.call(dnf_cmd)['stdout']
repoid_lines = [line for line in repolist_result.split('\n') if line.startswith('Repo-id')]
rhui_repoids = {extract_repoid_from_line(line) for line in repoid_lines}
@@ -919,6 +931,9 @@ def _get_rh_available_repoids(context, indata):
for foreign_repofile in foreign_repofiles:
os.rename('{0}.back'.format(foreign_repofile), foreign_repofile)
+ api.current_logger().debug(
+ 'The following repofiles are considered as provided by RedHat: {0}'.format(' '.join(rh_repoids))
+ )
return rh_repoids
@@ -1086,7 +1101,7 @@ def _get_target_userspace():
return constants.TARGET_USERSPACE.format(get_target_major_version())
-def _create_target_userspace(context, packages, files, target_repoids):
+def _create_target_userspace(context, indata, packages, files, target_repoids):
"""Create the target userspace."""
target_path = _get_target_userspace()
prepare_target_userspace(context, target_path, target_repoids, list(packages))
@@ -1096,12 +1111,57 @@ def _create_target_userspace(context, packages, files, target_repoids):
_copy_files(target_context, files)
dnfplugin.install(_get_target_userspace())
+ # If we used only repofiles from leapp-rhui-<provider> then remove these as they provide
+ # duplicit definitions as the target clients already installed in the target container
+ if indata.rhui_info:
+ api.current_logger().debug(
+ 'Target container should have access to content. '
+ 'Removing repofiles from leapp-rhui-<provider> from the target..'
+ )
+ setup_info = indata.rhui_info.target_client_setup_info
+ if not setup_info.bootstrap_target_client:
+ target_userspace_path = _get_target_userspace()
+ for copy in setup_info.preinstall_tasks.files_to_copy_into_overlay:
+ dst_in_container = get_copy_location_from_copy_in_task(target_userspace_path, copy)
+ dst_in_container = dst_in_container.strip('/')
+ dst_in_host = os.path.join(target_userspace_path, dst_in_container)
+ if os.path.isfile(dst_in_host) and dst_in_host.endswith('.repo'):
+ api.current_logger().debug('Removing repofile: {0}'.format(dst_in_host))
+ os.remove(dst_in_host)
+
# and do not forget to set the rhsm into the container mode again
with mounting.NspawnActions(_get_target_userspace()) as target_context:
rhsm.set_container_mode(target_context)
-def install_target_rhui_client_if_needed(context, indata):
+def _apply_rhui_access_preinstall_tasks(context, rhui_setup_info):
+ if rhui_setup_info.preinstall_tasks:
+ api.current_logger().debug('Applying RHUI preinstall tasks.')
+ preinstall_tasks = rhui_setup_info.preinstall_tasks
+
+ for file_to_remove in preinstall_tasks.files_to_remove:
+ api.current_logger().debug('Removing {0} from the scratch container.'.format(file_to_remove))
+ context.remove(file_to_remove)
+
+ for copy_info in preinstall_tasks.files_to_copy_into_overlay:
+ api.current_logger().debug(
+ 'Copying {0} in {1} into the scratch container.'.format(copy_info.src, copy_info.dst)
+ )
+ context.makedirs(os.path.dirname(copy_info.dst), exists_ok=True)
+ context.copy_to(copy_info.src, copy_info.dst)
+
+
+def _apply_rhui_access_postinstall_tasks(context, rhui_setup_info):
+ if rhui_setup_info.postinstall_tasks:
+ api.current_logger().debug('Applying RHUI postinstall tasks.')
+ for copy_info in rhui_setup_info.postinstall_tasks.files_to_copy:
+ context.makedirs(os.path.dirname(copy_info.dst), exists_ok=True)
+ debug_msg = 'Copying {0} to {1} (inside the scratch container).'
+ api.current_logger().debug(debug_msg.format(copy_info.src, copy_info.dst))
+ context.call(['cp', copy_info.src, copy_info.dst])
+
+
+def setup_target_rhui_access_if_needed(context, indata):
if not indata.rhui_info:
return
@@ -1110,15 +1170,14 @@ def install_target_rhui_client_if_needed(context, indata):
_create_target_userspace_directories(userspace_dir)
setup_info = indata.rhui_info.target_client_setup_info
- if setup_info.preinstall_tasks:
- preinstall_tasks = setup_info.preinstall_tasks
+ _apply_rhui_access_preinstall_tasks(context, setup_info)
- for file_to_remove in preinstall_tasks.files_to_remove:
- context.remove(file_to_remove)
-
- for copy_info in preinstall_tasks.files_to_copy_into_overlay:
- context.makedirs(os.path.dirname(copy_info.dst), exists_ok=True)
- context.copy_to(copy_info.src, copy_info.dst)
+ if not setup_info.bootstrap_target_client:
+ # Installation of the target RHUI client is not possible and we bundle all necessary
+ # files into the leapp-rhui-<provider> packages.
+ api.current_logger().debug('Bootstrapping target RHUI client is disabled, leapp will rely '
+ 'only on files budled in leapp-rhui-<provider> package.')
+ return
cmd = ['dnf', '-y']
@@ -1149,16 +1208,13 @@ def install_target_rhui_client_if_needed(context, indata):
context.call(cmd, callback_raw=utils.logging_handler, stdin='\n'.join(dnf_transaction_steps))
- if setup_info.postinstall_tasks:
- for copy_info in setup_info.postinstall_tasks.files_to_copy:
- context.makedirs(os.path.dirname(copy_info.dst), exists_ok=True)
- context.call(['cp', copy_info.src, copy_info.dst])
+ _apply_rhui_access_postinstall_tasks(context, setup_info)
# Do a cleanup so there are not duplicit repoids
files_owned_by_clients = _query_rpm_for_pkg_files(context, indata.rhui_info.target_client_pkg_names)
for copy_task in setup_info.preinstall_tasks.files_to_copy_into_overlay:
- dest = get_copy_location_from_copy_in_task(context, copy_task)
+ dest = get_copy_location_from_copy_in_task(context.base_dir, copy_task)
can_be_cleaned_up = copy_task.src not in setup_info.files_supporting_client_operation
if dest not in files_owned_by_clients and can_be_cleaned_up:
context.remove(dest)
@@ -1184,10 +1240,10 @@ def perform():
target_iso = next(api.consume(TargetOSInstallationImage), None)
with mounting.mount_upgrade_iso_to_root_dir(overlay.target, target_iso):
- install_target_rhui_client_if_needed(context, indata)
+ setup_target_rhui_access_if_needed(context, indata)
target_repoids = _gather_target_repositories(context, indata, prod_cert_path)
- _create_target_userspace(context, indata.packages, indata.files, target_repoids)
+ _create_target_userspace(context, indata, indata.packages, indata.files, target_repoids)
# TODO: this is tmp solution as proper one needs significant refactoring
target_repo_facts = repofileutils.get_parsed_repofiles(context)
api.produce(TMPTargetRepositoriesFacts(repositories=target_repo_facts))
diff --git a/repos/system_upgrade/common/models/rhuiinfo.py b/repos/system_upgrade/common/models/rhuiinfo.py
index 3eaa4826..0a2e45af 100644
--- a/repos/system_upgrade/common/models/rhuiinfo.py
+++ b/repos/system_upgrade/common/models/rhuiinfo.py
@@ -36,6 +36,13 @@ class TargetRHUISetupInfo(Model):
files_supporting_client_operation = fields.List(fields.String(), default=[])
"""A subset of files copied in preinstall tasks that should not be cleaned up."""
+ bootstrap_target_client = fields.Boolean(default=True)
+ """
+ Swap the current RHUI client for the target one to facilitate access to the target content.
+
+ When False, only files from the leapp-rhui-<provider> will be used to access target content.
+ """
+
class RHUIInfo(Model):
"""
--
2.43.0

File diff suppressed because it is too large Load Diff

View File

@ -1,106 +0,0 @@
From b96c610bb27ab5f7ef1ad65a1a763ed2d31b6816 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Tue, 17 Feb 2026 10:23:18 +0100
Subject: [PATCH 03/44] Enable logrotate.timer during 8to9 IPU
On el8 logrotate uses cron, on el9 a systemd timer is used. This is not
automatically migrated during an IPU, this patch adds a actor that
does so.
Jira: RHEL-17361
---
.../actors/enablelogrotatetimer/actor.py | 22 +++++++++++++
.../libraries/enablelogrotatetimer.py | 13 ++++++++
.../tests/test_enablelogrotatetimer.py | 31 +++++++++++++++++++
3 files changed, 66 insertions(+)
create mode 100644 repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/actor.py
create mode 100644 repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/libraries/enablelogrotatetimer.py
create mode 100644 repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/tests/test_enablelogrotatetimer.py
diff --git a/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/actor.py b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/actor.py
new file mode 100644
index 00000000..bc3c7ec3
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/actor.py
@@ -0,0 +1,22 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import enablelogrotatetimer
+from leapp.models import SystemdServicesInfoTarget, SystemdServicesTasks
+from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag
+
+
+class EnableLogrotateTimer(Actor):
+ """
+ Enable logrotate.timer on the target system after upgrade
+
+ The logrotate.timer systemd timer unit replaces the traditional cron-based
+ logrotate execution used in RHEL 8. This actor ensures the timer is enabled
+ after the in-place upgrade is complete.
+ """
+
+ name = 'enable_logrotate_timer'
+ consumes = (SystemdServicesInfoTarget,)
+ produces = (SystemdServicesTasks,)
+ tags = (FinalizationPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ enablelogrotatetimer.process()
diff --git a/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/libraries/enablelogrotatetimer.py b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/libraries/enablelogrotatetimer.py
new file mode 100644
index 00000000..c9783ae9
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/libraries/enablelogrotatetimer.py
@@ -0,0 +1,13 @@
+from leapp.libraries.common.systemd import enable_unit
+from leapp.libraries.stdlib import api, CalledProcessError
+
+LOGROTATE_TIMER = 'logrotate.timer'
+
+
+def process():
+ try:
+ enable_unit(LOGROTATE_TIMER)
+ except CalledProcessError as e:
+ api.current_logger().error(
+ "Failed to enable {}: {}".format(LOGROTATE_TIMER, e)
+ )
diff --git a/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/tests/test_enablelogrotatetimer.py b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/tests/test_enablelogrotatetimer.py
new file mode 100644
index 00000000..f366bd6a
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/enablelogrotatetimer/tests/test_enablelogrotatetimer.py
@@ -0,0 +1,31 @@
+from leapp.libraries.actor import enablelogrotatetimer
+from leapp.libraries.common.testutils import logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+
+
+def test_success(monkeypatch):
+
+ def mock_enable_unit(unit):
+ assert unit == 'logrotate.timer'
+
+ monkeypatch.setattr(enablelogrotatetimer, "enable_unit", mock_enable_unit)
+ enablelogrotatetimer.process()
+
+
+def test_failed_to_enable(monkeypatch):
+
+ def mock_enable_unit(unit):
+ assert unit == 'logrotate.timer'
+ raise CalledProcessError(
+ "Failed to enable logrotate.titmer",
+ ["systemctl", "enable", "logrotate.timer"],
+ {
+ "exit_code": 1,
+ "stderr": "err",
+ },
+ )
+
+ monkeypatch.setattr(enablelogrotatetimer, "enable_unit", mock_enable_unit)
+ monkeypatch.setattr(api, "current_logger", logger_mocked())
+ enablelogrotatetimer.process()
+ assert "Failed to enable logrotate.timer" in api.current_logger.errmsg[0]
--
2.53.0

View File

@ -1,63 +0,0 @@
From 0e0db2860587d57193a52135ae88df8917d70538 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Wed, 11 Feb 2026 16:51:41 +0100
Subject: [PATCH 04/44] feat(net-naming-scheme): enable by default on CS 9to10
This was done for RHEL 9to10 in
91f37a3913c1608b24c9aa583ac3b13a08aace71.
Jira: RHEL-148291
---
.../libraries/emit_net_naming.py | 10 ++++------
.../tests/test_emit_net_naming_scheme.py | 9 ++-------
2 files changed, 6 insertions(+), 13 deletions(-)
diff --git a/repos/system_upgrade/common/actors/emit_net_naming_scheme/libraries/emit_net_naming.py b/repos/system_upgrade/common/actors/emit_net_naming_scheme/libraries/emit_net_naming.py
index 7b112ff0..ef2c0b79 100644
--- a/repos/system_upgrade/common/actors/emit_net_naming_scheme/libraries/emit_net_naming.py
+++ b/repos/system_upgrade/common/actors/emit_net_naming_scheme/libraries/emit_net_naming.py
@@ -1,5 +1,5 @@
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common.config import get_env, get_target_distro_id, version
+from leapp.libraries.common.config import get_env, version
from leapp.libraries.stdlib import api
from leapp.models import (
KernelCmdline,
@@ -49,11 +49,9 @@ def is_net_scheme_compatible_with_current_cmdline():
def emit_msgs_to_use_net_naming_schemes():
is_feature_enabled = get_env('LEAPP_DISABLE_NET_NAMING_SCHEMES', '0') != '1'
- is_net_naming_available = version.get_target_major_version() == "9" or (
- version.matches_target_version(">= 10.2")
- # TODO the net-naming-sysattrs pkg is not yet available on CS10, remove
- # this when it becomes
- and not get_target_distro_id() == "centos"
+ is_net_naming_available = (
+ version.get_target_major_version() == "9"
+ or version.matches_target_version(">= 10.2")
)
if not (is_feature_enabled and is_net_naming_available):
diff --git a/repos/system_upgrade/common/actors/emit_net_naming_scheme/tests/test_emit_net_naming_scheme.py b/repos/system_upgrade/common/actors/emit_net_naming_scheme/tests/test_emit_net_naming_scheme.py
index 9c1f91bb..d8eef404 100644
--- a/repos/system_upgrade/common/actors/emit_net_naming_scheme/tests/test_emit_net_naming_scheme.py
+++ b/repos/system_upgrade/common/actors/emit_net_naming_scheme/tests/test_emit_net_naming_scheme.py
@@ -95,13 +95,8 @@ def test_emit_msgs_to_use_net_naming_schemes(
pkg_name = emit_net_naming_lib.NET_NAMING_SYSATTRS_RPM_NAME[target_major]
produced_messages = api.produce.model_instances
- if (
- is_net_scheme_enabled
- # the package is available since 10.2
- and target_ver != "10.0"
- # TODO not yet available in CS 10, remove this when it is
- and not (target_distro == "centos" and target_major == "10")
- ):
+ # the package is available since 10.2
+ if is_net_scheme_enabled and target_ver != "10.0":
userspace_tasks = ensure_one_msg_of_type_produced(produced_messages, TargetUserSpaceUpgradeTasks)
assert userspace_tasks.install_rpms == [pkg_name]
--
2.53.0

View File

@ -1,46 +0,0 @@
From 26e4ca841b23907eda2de78288183285e6b54b1f Mon Sep 17 00:00:00 2001
From: karolinku <kkula@redhat.com>
Date: Tue, 3 Mar 2026 13:56:26 +0100
Subject: [PATCH 05/44] Leapp_resume.service: Silence AVC SELinux warnings
Wrap the ExecStart command in `/bin/sh -c` to avoid SELinux AVC denials.
The leapp_resume.service unit is inherently "malformed" from SELinux's
perspective: it directly executes /root/tmp_leapp_py3/leapp3, which is
labeled admin_home_t. When the system boots in Permissive mode (as it
does during the upgrade), SELinux logs AVC denials for this access
rather than blocking it. These AVCs are expected but noisy and
confusing.
By invoking the command through /bin/sh -c, the execution context is
evaluated against /bin/sh (which carries a proper shell_exec_t label),
avoiding the admin_home_t AVC altogether.
It is also confirmed with SELinux team that this issue could be
solved with also following solution (in case of any future issues):
```
ExecStartPre=chcon -t bin_t /root/tmp_leapp_py3/leapp3
ExecStart=/root/tmp_leapp_py3/leapp3 upgrade --resume
````
Jira: RHEL-149141
---
.../actors/createresumeservice/files/leapp_resume.service | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 39ac6112..859237a1 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
+++ b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
@@ -9,7 +9,7 @@ Wants=network-online.target
[Service]
Type=oneshot
# FIXME: this is temporary workaround for Python3
-ExecStart=/root/tmp_leapp_py3/leapp3 upgrade --resume
+ExecStart=/bin/sh -c "/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
TimeoutStartSec=infinity
--
2.53.0

View File

@ -1,138 +0,0 @@
From 9827568a1e8caeb3e96c0c40a3d7a741997391c6 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Tue, 17 Feb 2026 16:56:43 +0100
Subject: [PATCH 06/44] Add API to get a distro "report" name
Add a global variable DISTRO_REPORT_NAMES which can be directly be used
in format_map() or unpacked into format() calls.
There are also source and target fields which can be used in e.g.
fstrings.
Jira: RHEL-130948
---
.../system_upgrade/common/libraries/distro.py | 72 ++++++++++++++++++-
.../common/libraries/tests/test_distro.py | 19 +++++
2 files changed, 90 insertions(+), 1 deletion(-)
diff --git a/repos/system_upgrade/common/libraries/distro.py b/repos/system_upgrade/common/libraries/distro.py
index 34c48a57..1a5e3923 100644
--- a/repos/system_upgrade/common/libraries/distro.py
+++ b/repos/system_upgrade/common/libraries/distro.py
@@ -1,9 +1,10 @@
import json
import os
+from collections.abc import Mapping
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import efi, repofileutils, rhsm
-from leapp.libraries.common.config import get_target_distro_id
+from leapp.libraries.common.config import get_source_distro_id, get_target_distro_id
from leapp.libraries.common.config.architecture import ARCH_ACCEPTED, ARCH_X86_64
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api
@@ -234,6 +235,75 @@ def distro_id_to_pretty_name(distro_id):
}[distro_id]
+def _distro_id_to_report_name(distro_id):
+ """
+ Get the display name for the given distro id.
+
+ The display name should be used for user facing text, such as in reports.
+ For a real/full name see the :func:`distro_id_to_pretty_name` function.
+ """
+ return {
+ "rhel": "RHEL",
+ "centos": "CentOS Stream",
+ "almalinux": "AlmaLinux"
+ }[distro_id]
+
+
+class _DistroReportNames(Mapping):
+ """
+ A mapping of distro names to be used in reports.
+
+ In addition to the properties :attr:`source` and :attr:`target`, this class
+ can be used in :func:`format_map()` or unpacked to :func:`format` where it
+ exposes them as 'source_distro' and 'target_distro'.
+ """
+
+ @property
+ def source(self) -> str:
+ """
+ The source distro report name.
+
+ :type: str
+ """
+ return _distro_id_to_report_name(get_source_distro_id())
+
+ @property
+ def target(self) -> str:
+ """
+ The target distro report name.
+
+ :type: str
+ """
+ return _distro_id_to_report_name(get_target_distro_id())
+
+ def __getitem__(self, key):
+ if key == 'source_distro':
+ return self.source
+
+ if key == 'target_distro':
+ return self.target
+
+ raise KeyError(f"Key '{key}' not found in {self.__class__.__name__}")
+
+ def __len__(self):
+ return 2
+
+ def __iter__(self):
+ yield from ['source_distro', 'target_distro']
+
+
+DISTRO_REPORT_NAMES = _DistroReportNames()
+"""
+A mapping of distro "display" names for use in reports.
+
+Can be used as argument for format_map() or unpacked in format() calls.
+The format string placeholders 'source_distro' and 'target_distro' are then
+replaced by the respective distro names or abbreviations of them.
+
+See :class:`_DistroReportNames`.
+"""
+
+
def get_distro_efidir_canon_path(distro_id):
"""
Get canonical path to the distro EFI directory in the EFI mountpoint.
diff --git a/repos/system_upgrade/common/libraries/tests/test_distro.py b/repos/system_upgrade/common/libraries/tests/test_distro.py
index ec7d8f77..d266a9c6 100644
--- a/repos/system_upgrade/common/libraries/tests/test_distro.py
+++ b/repos/system_upgrade/common/libraries/tests/test_distro.py
@@ -235,3 +235,22 @@ def test_get_distro_repoids_invalid_repo(monkeypatch):
)
def test__get_distro_efidir_canon_path(distro, expect):
assert expect == distrolib.get_distro_efidir_canon_path(distro)
+
+
+def test_distro_report_names(monkeypatch):
+ current_actor = CurrentActorMocked(src_distro="centos", dst_distro="rhel")
+ monkeypatch.setattr(api, "current_actor", current_actor)
+
+ assert distrolib.DISTRO_REPORT_NAMES.source == "CentOS Stream"
+ assert distrolib.DISTRO_REPORT_NAMES.target == "RHEL"
+
+ expect = "CentOS Stream is upstream for RHEL"
+ template = "{source_distro} is upstream for {target_distro}"
+ assert expect == template.format_map(distrolib.DISTRO_REPORT_NAMES)
+ assert expect == template.format(**distrolib.DISTRO_REPORT_NAMES)
+
+ template = "{source_distro} is {what} for {target_distro}"
+ assert expect == template.format(what='upstream', **distrolib.DISTRO_REPORT_NAMES)
+
+ template = "{source_distro} is {what} for RHEL"
+ assert expect == template.format(what='upstream', **distrolib.DISTRO_REPORT_NAMES)
--
2.53.0

View File

@ -1,190 +0,0 @@
From d6e4afe5c51ac8809649ca395e2ffafc5ab76922 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Wed, 4 Mar 2026 22:45:18 +0100
Subject: [PATCH 07/44] livemode: copy upgrade kernel's hmac into /boot
Copying kernel's HMAC is necessary in order to boot with FIPS mode
enabled. Standard (old) upgrade process copies the kernel HMAC file
already, livemode did not in order to keep the initial implementation
simple. This change adds copying of kernel HMAC also for livemode.
Jira-ref: RHEL-129571
---
.../libraries/upgradeinitramfsgenerator.py | 50 +++++++----
.../unit_test_upgradeinitramfsgenerator.py | 84 ++++++++++++++++++-
2 files changed, 118 insertions(+), 16 deletions(-)
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 03447b7c..1a0dccd8 100644
--- a/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
+++ b/repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py
@@ -480,6 +480,16 @@ def prepare_boot_files_for_livemode(context):
copy_target_kernel_from_userspace_into_boot(context, target_kernel_ver, kernel_artifact_name)
+ uspace_kernel_hmac_path = context.full_path('/lib/modules/{}/.vmlinuz.hmac'.format(target_kernel_ver))
+ upgrade_kernel_hmac_dest = '/boot/.{}.hmac'.format(kernel_artifact_name)
+ create_upgrade_hmac_from_target_hmac(uspace_kernel_hmac_path, upgrade_kernel_hmac_dest, kernel_artifact_name)
+ api.current_logger().info(
+ 'Written hmac ({0}) for the upgrade kernel (based on {1})'.format(
+ upgrade_kernel_hmac_dest,
+ uspace_kernel_hmac_path
+ )
+ )
+
USERSPACE_ARTIFACTS_PATH = '/artifacts'
context.makedirs(USERSPACE_ARTIFACTS_PATH, exists_ok=True)
userspace_initramfs_dest = os.path.join(USERSPACE_ARTIFACTS_PATH, initramfs_artifact_name)
@@ -493,25 +503,35 @@ def prepare_boot_files_for_livemode(context):
return BootContent(kernel_path=host_kernel_dest,
initram_path=host_initramfs_dest,
- kernel_hmac_path='')
+ kernel_hmac_path=upgrade_kernel_hmac_dest)
+
+
+def _read_file(path):
+ with open(path) as in_file:
+ return in_file.read()
+
+
+def _write_file(path, data):
+ with open(path, 'w') as out_file:
+ out_file.write(data)
-def create_upgrade_hmac_from_target_hmac(original_hmac_path, upgrade_hmac_path, upgrade_kernel):
+def create_upgrade_hmac_from_target_hmac(original_hmac_path: str, upgrade_hmac_path: str, upgrade_kernel: str):
# Rename the kernel name stored in the HMAC file as the upgrade kernel is named differently and the HMAC file
# refers to the real target kernel
- with open(original_hmac_path) as original_hmac_file:
- hmac_file_lines = [line for line in original_hmac_file.read().split('\n') if line]
- if len(hmac_file_lines) > 1:
- details = ('Expected the target kernel HMAC file to containing only one HMAC line, '
- 'found {0}'.format(len(hmac_file_lines)))
- raise StopActorExecutionError('Failed to prepare HMAC file for upgrade kernel.',
- details={'details': details})
-
- # Keep only non-empty strings after splitting on space
- hmac, dummy_target_kernel_name = [fragment for fragment in hmac_file_lines[0].split(' ') if fragment]
-
- with open(upgrade_hmac_path, 'w') as upgrade_kernel_hmac_file:
- upgrade_kernel_hmac_file.write('{hmac} {kernel}\n'.format(hmac=hmac, kernel=upgrade_kernel))
+ original_hmac_file_content = _read_file(original_hmac_path)
+ hmac_file_lines = [line for line in original_hmac_file_content.split('\n') if line]
+ if len(hmac_file_lines) > 1:
+ details = ('Expected the target kernel HMAC file to containing only one HMAC line, '
+ 'found {0}'.format(len(hmac_file_lines)))
+ raise StopActorExecutionError('Failed to prepare HMAC file for upgrade kernel.',
+ details={'details': details})
+
+ # Keep only non-empty strings after splitting on space
+ hmac, dummy_target_kernel_name = [fragment for fragment in hmac_file_lines[0].split(' ') if fragment]
+
+ upgrade_hmac_content = '{hmac} {kernel}\n'.format(hmac=hmac, kernel=upgrade_kernel)
+ _write_file(upgrade_hmac_path, upgrade_hmac_content)
def copy_boot_files(context):
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 b96bf79f..165a4df0 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
@@ -116,7 +116,7 @@ class MockedContext:
self.called_copy_to.append((src, dst))
self.content.add(dst)
- def makedirs(self, path):
+ def makedirs(self, path, exists_ok=True):
self.called_makedirs.append(path)
def remove_tree(self, path):
@@ -402,3 +402,85 @@ def test_copy_modules_duplicate_skip(monkeypatch, kind):
assert context.content
assert len(context.called_copy_to) == 1
assert debugmsg in upgradeinitramfsgenerator.api.current_logger.dbgmsg
+
+
+def test_create_upgrade_hmac_from_target_hmac(monkeypatch):
+ upgrade_hmac_written = False
+
+ def _read_file_mock(path):
+ assert path == '/original-hmac'
+ return ('ff00a9674033eea61bec48d21a1d2c27eaac9bd6ed4997e31dd0d9307c7a4770eb81df7116c'
+ '4ace25d354a06dfdcd75e38f504f2ea7c1c4bdc95ea7083b701c0 vmlinuz-6.12.0-55.9.1.el10_0.x86_64')
+
+ def _write_file_mock(path, content):
+ assert path == '/boot/.vmlinuz-upgrade.x86_64.hmac'
+ expected_content = ('ff00a9674033eea61bec48d21a1d2c27eaac9bd6ed4997e31dd0d9307c7a4770eb81df7116c'
+ '4ace25d354a06dfdcd75e38f504f2ea7c1c4bdc95ea7083b701c0 '
+ 'vmlinuz-upgrade.x86_64\n')
+ assert content == expected_content
+ nonlocal upgrade_hmac_written
+ upgrade_hmac_written = True
+
+ monkeypatch.setattr(upgradeinitramfsgenerator, '_read_file', _read_file_mock)
+ monkeypatch.setattr(upgradeinitramfsgenerator, '_write_file', _write_file_mock)
+ upgradeinitramfsgenerator.create_upgrade_hmac_from_target_hmac(
+ '/original-hmac', '/boot/.vmlinuz-upgrade.x86_64.hmac', 'vmlinuz-upgrade.x86_64')
+
+ assert upgrade_hmac_written
+
+
+def test_prepare_boot_files_for_livemode(monkeypatch):
+ context_mock = MockedContext()
+
+ monkeypatch.setattr(upgradeinitramfsgenerator,
+ '_get_target_kernel_version',
+ lambda ctx: '6.18.3-100.fc42.x86_64')
+
+ monkeypatch.setattr(upgradeinitramfsgenerator,
+ 'get_boot_artifact_names',
+ lambda: ('vmlinuz-upgrade.x86_64', 'initramfs-upgrade.x86_64.img'))
+
+ upgrade_kernel_present = False
+ initramfs_generated = False
+ upgrade_initramfs_present = False
+ upgrade_kernel_hmac_present = False
+
+ def copy_target_kernel_mock(context, target_kernel_ver, kernel_artifact_name):
+ nonlocal upgrade_kernel_present
+ upgrade_kernel_present = True
+
+ def _generate_initramfs_mock(context, userspace_initramfs_dest, target_kernel_ver):
+ nonlocal initramfs_generated
+ initramfs_generated = True
+
+ def create_upgrade_hmac_from_target_hmac_mock(uspace_kernel_hmac_path,
+ upgrade_kernel_hmac_dest,
+ kernel_artifact_name):
+ assert upgrade_kernel_hmac_dest == '/boot/.vmlinuz-upgrade.x86_64.hmac'
+ nonlocal upgrade_kernel_hmac_present
+ upgrade_kernel_hmac_present = True
+
+ monkeypatch.setattr(upgradeinitramfsgenerator,
+ 'copy_target_kernel_from_userspace_into_boot',
+ copy_target_kernel_mock)
+
+ monkeypatch.setattr(upgradeinitramfsgenerator,
+ 'create_upgrade_hmac_from_target_hmac',
+ create_upgrade_hmac_from_target_hmac_mock)
+
+ monkeypatch.setattr(upgradeinitramfsgenerator,
+ '_generate_livemode_initramfs',
+ _generate_initramfs_mock)
+
+ boot_content = upgradeinitramfsgenerator.prepare_boot_files_for_livemode(context_mock)
+
+ upgrade_initramfs_present = context_mock.called_copy_from[0][1] == '/boot/initramfs-upgrade.x86_64.img'
+
+ assert upgrade_kernel_present
+ assert initramfs_generated
+ assert upgrade_initramfs_present
+ assert upgrade_kernel_hmac_present
+
+ assert boot_content.kernel_path == '/boot/vmlinuz-upgrade.x86_64'
+ assert boot_content.initram_path == '/boot/initramfs-upgrade.x86_64.img'
+ assert boot_content.kernel_hmac_path == '/boot/.vmlinuz-upgrade.x86_64.hmac'
--
2.53.0

View File

@ -1,131 +0,0 @@
From 0fc2075683ffe30b77e83c7d793815e9301fa41d Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Fri, 13 Mar 2026 10:03:19 +0100
Subject: [PATCH 08/44] fix(livemode): make live.dir is relative to device root
Booting from an squashfs image (from a local block dev) requires to
parameters:
- identifier of the device where the squashfs image resides
- path to the squashfs image on the given device (aka live.dir)
Therefore, live.dir needs to be set up with a path that is relative to
the device where the image resides. This patch fixes the current
behavior where the path is being taken as it is, i.e., relative to the
root of the source system.
Jira-ref: RHEL-104379
---
.../libraries/addupgradebootentry.py | 29 ++++++++++-----
.../tests/unit_test_addupgradebootentry.py | 36 +++++++++++--------
2 files changed, 43 insertions(+), 22 deletions(-)
diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
index b28ec57c..5b635a83 100644
--- a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
+++ b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
@@ -270,14 +270,26 @@ def local_os_stat(path):
return os.stat(path)
-def _get_device_uuid(path):
+def _split_path_on_mount_point(path):
+ """ Split a given path into a prefix where a device is mounted and suffix relative to the device root. """
+ mount_point = path
+ while not os.path.ismount(mount_point):
+ mount_point = os.path.dirname(mount_point)
+
+ relative_suffix = path[len(mount_point):]
+ if len(relative_suffix) == 0 or relative_suffix[0] != '/':
+ # relative_suffix is '' if the squashfs dir is set to root (/)
+ # relative_suffix does not start with '/' if the dir is located in /, i.e., /mydir
+ relative_suffix = '/{}'.format(relative_suffix)
+
+ return (mount_point, relative_suffix)
+
+
+def _get_device_uuid(mount_point):
"""
- Find the UUID of a device in which the given path is located.
+ Find the UUID of a device mounted at a given mount point.
"""
- while not os.path.ismount(path):
- path = os.path.dirname(path)
-
- needle_dev_id = local_os_stat(path).st_dev
+ needle_dev_id = local_os_stat(mount_point).st_dev
for uuid in os.listdir('/dev/disk/by-uuid'):
uuid_fullpath = os.path.join('/dev/disk/by-uuid/', uuid)
@@ -333,8 +345,9 @@ def construct_cmdline_args_for_livemode():
if livemode_config.url_to_load_squashfs_from:
args['root'] = 'live:{}'.format(livemode_config.url_to_load_squashfs_from)
else:
- args['root'] = 'live:UUID={}'.format(_get_device_uuid(dir_path_containing_liveimg))
- args['rd.live.dir'] = dir_path_containing_liveimg
+ dev_mount_point, dir_location_on_dev = _split_path_on_mount_point(dir_path_containing_liveimg)
+ args['root'] = 'live:UUID={}'.format(_get_device_uuid(dev_mount_point))
+ args['rd.live.dir'] = dir_location_on_dev
args['rd.live.squashimg'] = liveimg_filename
if livemode_config.dracut_network:
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 7341602b..79c05a5d 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
@@ -279,23 +279,31 @@ def test_get_rdlvm_arg_values(monkeypatch):
assert args == ('A', 'B')
+@pytest.mark.parametrize(
+ ('path', 'mount_point', 'suffix'),
+ [
+ ('/', '/', '/'),
+ ('/dir', '/', '/dir'),
+ ('/dir/squashfs', '/', '/dir/squashfs'),
+ ('/dir/squashfs', '/dir', '/squashfs'),
+ ]
+)
+def test_split_path_on_mount_point(monkeypatch, path, mount_point, suffix):
+ def is_mount_mock(_path):
+ return _path == mount_point
+
+ monkeypatch.setattr(os.path, 'ismount', is_mount_mock)
+
+ ret_mount_point, ret_suffix = addupgradebootentry._split_path_on_mount_point(path)
+ assert ret_mount_point == mount_point
+ assert ret_suffix == suffix
+
+
def test_get_device_uuid(monkeypatch):
"""
The file in question is /var/lib/file
Underlying partition /var is a device /dev/sda1 (dev_id=10) linked to from /dev/disk/by-uuid/MY_UUID1
"""
-
- execution_stats = {
- 'is_mount_call_count': 0
- }
-
- def is_mount_mock(path):
- execution_stats['is_mount_call_count'] += 1
- assert execution_stats['is_mount_call_count'] <= 3
- return path == '/var'
-
- monkeypatch.setattr(os.path, 'ismount', is_mount_mock)
-
StatResult = namedtuple('StatResult', ('st_dev', 'st_rdev'))
def stat_mock(path):
@@ -325,8 +333,8 @@ def test_get_device_uuid(monkeypatch):
monkeypatch.setattr(os, 'readlink', readlink_mock)
- path = '/var/lib/file'
- uuid = addupgradebootentry._get_device_uuid(path)
+ mount_point = '/var'
+ uuid = addupgradebootentry._get_device_uuid(mount_point)
assert uuid == 'MY_UUID1'
--
2.53.0

View File

@ -1,61 +0,0 @@
From 3a9cb366b7487bb5cc57e7650447af327c343b0d Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Wed, 18 Mar 2026 09:58:13 +0100
Subject: [PATCH 09/44] fix(livemode): change location of source system tree
Before this patch, the generated squashfs image mounts the source system
tree at /run/initramfs/live, which is the location used by dmsquash-live
to mount the block device that holds the squashfs image. This device,
however, might be arbitrary, e.g., it could be hosting /var of the
source system. Therefore, the squashfs system would attempt to mount
multiple things at /run/initramfs/live (/ and /var of the source
system), creating problems. This patch changes the location of the
source system tree to '/run/upgrade'.
Jira-ref: RHEL-104379
---
.../files/do-upgrade.sh | 4 ++--
.../libraries/prepareliveimage.py | 12 ++++++++++--
2 files changed, 12 insertions(+), 4 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 4b2f9a1f..51ebddb5 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
@@ -28,8 +28,8 @@ export LEAPPHOME=/root/tmp_leapp_py3
export LEAPP3_BIN=$LEAPPHOME/leapp3
# this was initially a dracut script, hence $NEWROOT.
-# the rootfs is mounted on /run/initramfs/live when booted with dmsquash-live
-export NEWROOT=/run/initramfs/live
+# the rootfs is mounted on /run/upgrade when booted with dmsquash-live
+export NEWROOT=/run/upgrade
NSPAWN_OPTS="--capability=all --bind=/dev --bind=/dev/pts --bind=/proc --bind=/run/udev --bind=/run/lock"
[ -d /dev/mapper ] && NSPAWN_OPTS="$NSPAWN_OPTS --bind=/dev/mapper"
diff --git a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/libraries/prepareliveimage.py b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/libraries/prepareliveimage.py
index 2587bf89..96dadadf 100644
--- a/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/libraries/prepareliveimage.py
+++ b/repos/system_upgrade/common/actors/livemode/modify_userspace_for_livemode/libraries/prepareliveimage.py
@@ -18,8 +18,16 @@ LEAPP_CONSOLE_SERVICE_FILE = 'console.service'
LEAPP_STRACE_SERVICE_FILE = 'upgrade-strace.service'
""" Service that executes the upgrade while strace-ing the corresponding Leapp's process tree. """
-SOURCE_ROOT_MOUNT_LOCATION = '/run/initramfs/live'
-""" Controls where the source system's root will be mounted inside the upgrade image. """
+SOURCE_ROOT_MOUNT_LOCATION = '/run/upgrade'
+"""
+Controls where the source system's root will be mounted inside the upgrade image.
+
+Note: This cannot be set to /run/initramfs/live as the path is used by
+ dmsquash-live by default to mount the block device that holds the squashfs
+ image. As this device can be arbitrary (e.g., it can be mounted as /var on the
+ source system), using /run/initramfs/live would cause mounting problems - we
+ would attempt to mount / and also (for example) /var on the same location.
+"""
def create_fstab_mounting_current_root_elsewhere(context, host_fstab):
--
2.53.0

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +0,0 @@
From f6838df00e3be7d8beec99bd9448ed47c7720853 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 2 Mar 2026 18:25:09 +0100
Subject: [PATCH 11/44] checkblacklistca: Respect distro name in reports
Jira: RHEL-130950
---
.../libraries/checkblacklistca.py | 28 +++++++++++++------
.../tests/component_test_checkblacklistca.py | 9 ++++++
2 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/checkblacklistca/libraries/checkblacklistca.py b/repos/system_upgrade/el8toel9/actors/checkblacklistca/libraries/checkblacklistca.py
index 53b912b8..635b3640 100644
--- a/repos/system_upgrade/el8toel9/actors/checkblacklistca/libraries/checkblacklistca.py
+++ b/repos/system_upgrade/el8toel9/actors/checkblacklistca/libraries/checkblacklistca.py
@@ -1,4 +1,5 @@
from leapp import reporting
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import BlackListCA, BlackListError
@@ -50,9 +51,14 @@ def process():
reporting.Title('Distrusted CA certificates will be moved from blacklist to blocklist'),
reporting.Summary(
'The directories which store user and administrator supplied '
- 'distrusted certificates have change names from blacklist in '
- 'RHEL8 to blocklist in RHEL9. As a result {} and '
- '{} will be deleted.'.format(reportString, deleteString)),
+ 'distrusted certificates were renamed from blacklist in '
+ '{source_distro} 8 to blocklist in {target_distro} 9. '
+ 'As a result {report_string} and {delete_string} will be deleted.'.format(
+ report_string=reportString,
+ delete_string=deleteString,
+ **DISTRO_REPORT_NAMES,
+ )
+ ),
reporting.Severity(reporting.Severity.INFO),
reporting.Groups([reporting.Groups.SECURITY]),
reporting.Groups([reporting.Groups.AUTHENTICATION])
@@ -63,11 +69,17 @@ def process():
reporting.Summary(
'The directories which stores user and administrator supplied '
'distrusted certificates has change names from blacklist in '
- 'RHEL8 to blocklist in RHEL9. But we are unable to access the '
- 'RHEL8 directory {} because {}. You can clear this error by '
- 'correcting the condition, or by moving the contents to {} '
- 'and removing {} completely'
- '. '.format(ble.sourceDir, ble.error, ble.targetDir, ble.sourceDir)),
+ '{source_distro} 8 to blocklist in {target_distro} 9. '
+ 'But we are unable to access the {source_distro} 8 directory '
+ '{source_dir} because {error}. You can clear this error by '
+ 'correcting the condition, or by moving the contents to '
+ '{target_dir} and removing {source_dir} completely'.format(
+ source_dir=ble.sourceDir,
+ error=ble.error,
+ target_dir=ble.targetDir,
+ **DISTRO_REPORT_NAMES,
+ )
+ ),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.SECURITY]),
reporting.Groups([reporting.Groups.INHIBITOR]),
diff --git a/repos/system_upgrade/el8toel9/actors/checkblacklistca/tests/component_test_checkblacklistca.py b/repos/system_upgrade/el8toel9/actors/checkblacklistca/tests/component_test_checkblacklistca.py
index 2fc27501..af7e6305 100644
--- a/repos/system_upgrade/el8toel9/actors/checkblacklistca/tests/component_test_checkblacklistca.py
+++ b/repos/system_upgrade/el8toel9/actors/checkblacklistca/tests/component_test_checkblacklistca.py
@@ -1,7 +1,16 @@
+import pytest
+
+from leapp.libraries.common import distro
from leapp.models import BlackListCA, BlackListError, Report
from leapp.utils.report import is_inhibitor
+@pytest.fixture(autouse=True)
+def common_mocks(monkeypatch):
+ monkeypatch.setattr(distro, 'get_source_distro_id', lambda: 'rhel')
+ monkeypatch.setattr(distro, 'get_target_distro_id', lambda: 'rhel')
+
+
def test_actor_execution_empty(current_actor_context):
current_actor_context.feed()
current_actor_context.run()
--
2.53.0

View File

@ -1,49 +0,0 @@
From a2a3dfb9b14f9582b4c895bda28a3ecb604ea737 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 2 Mar 2026 18:34:34 +0100
Subject: [PATCH 12/44] blsgrubcfgonppc64: Respect distro name in reports
Jira: RHEL-130950
---
.../libraries/blsgrubcfgonppc64.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/checkblsgrubcfgonppc64/libraries/blsgrubcfgonppc64.py b/repos/system_upgrade/el8toel9/actors/checkblsgrubcfgonppc64/libraries/blsgrubcfgonppc64.py
index d723df65..58d15e25 100644
--- a/repos/system_upgrade/el8toel9/actors/checkblsgrubcfgonppc64/libraries/blsgrubcfgonppc64.py
+++ b/repos/system_upgrade/el8toel9/actors/checkblsgrubcfgonppc64/libraries/blsgrubcfgonppc64.py
@@ -1,6 +1,7 @@
from leapp import reporting
from leapp.libraries.common import grub
from leapp.libraries.common.config import architecture
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import DefaultGrubInfo, FirmwareFacts, GrubCfgBios
@@ -31,8 +32,9 @@ def process():
'Leapp cannot continue with upgrade on "ppc64le" bare metal systems'
),
reporting.Summary(
- 'In-place upgrade to RHEL 9 is not supported on POWER8 and POWER9 bare metal systems. '
- 'For more information, refer to the following article: {}'.format(URL)
+ f'In-place upgrade to {DISTRO_REPORT_NAMES.target} 9 is not'
+ ' supported on POWER8 and POWER9 bare metal systems. For more'
+ ' information, refer to the attached article.'
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups(['inhibitor']),
@@ -54,8 +56,9 @@ def process():
'On "ppc64le" systems with BLS enabled, the GRUB configuration is not '
'properly converted after the upgrade and Leapp has to run "grub2-mkconfig" '
'-o /boot/grub2/grub.cfg command in order to fix an issue with booting into '
- 'the RHEL 8 kernel instead of RHEL 9.'
-
+ 'the {source_distro} 8 kernel instead of {target_distro} 9.'.format_map(
+ DISTRO_REPORT_NAMES
+ )
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.BOOT]),
--
2.53.0

View File

@ -1,38 +0,0 @@
From b43ed18a6e4cea273def17c83c0c8d1742ba5145 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 16:16:17 +0100
Subject: [PATCH 13/44] checkselinux: Respect distro name in report
Jira: RHEL-130950
---
.../common/actors/checkselinux/libraries/checkselinux.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py b/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
index 2ef914ac..dbd79adf 100644
--- a/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
+++ b/repos/system_upgrade/common/actors/checkselinux/libraries/checkselinux.py
@@ -1,5 +1,6 @@
from leapp import reporting
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
@@ -20,10 +21,10 @@ def process():
reporting.create_report([
reporting.Title('LEAPP detected SELinux disabled in "/etc/selinux/config"'),
reporting.Summary(
- 'On RHEL 9, disabling SELinux in "/etc/selinux/config" is no longer possible. '
- 'This way, the system starts with SELinux enabled but with no policy loaded. LEAPP '
+ 'On {target_distro} 9, disabling SELinux in "/etc/selinux/config" is no longer possible. '
+ 'This way, the system starts with SELinux enabled but with no policy loaded. Leapp '
'will automatically disable SELinux using "SELINUX=0" kernel command line parameter. '
- 'However, Red Hat strongly recommends to have SELinux enabled'
+ 'However, it is strongly recommended to have SELinux enabled'.format_map(DISTRO_REPORT_NAMES)
),
reporting.Severity(reporting.Severity.INFO),
reporting.Groups([reporting.Groups.SELINUX]),
--
2.53.0

View File

@ -1,92 +0,0 @@
From 60e54a82da37bb0d2f1bee1a35d1d7fa01cf3df7 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 16:44:04 +0100
Subject: [PATCH 14/44] checkosrelease: Respect distro name in report
Jira: RHEL-130950
---
.../common/actors/checkosrelease/actor.py | 2 +-
.../actors/checkosrelease/libraries/checkosrelease.py | 10 +++++++---
.../actors/checkosrelease/tests/test_checkosrelease.py | 4 +++-
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkosrelease/actor.py b/repos/system_upgrade/common/actors/checkosrelease/actor.py
index 7747eb9b..8c60d968 100644
--- a/repos/system_upgrade/common/actors/checkosrelease/actor.py
+++ b/repos/system_upgrade/common/actors/checkosrelease/actor.py
@@ -6,7 +6,7 @@ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
class CheckOSRelease(Actor):
"""
- Check if the current RHEL minor version is supported. If not, inhibit the upgrade process.
+ Check if the current distro version is supported. If not, inhibit the upgrade process.
This check can be skipped by using the LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE environment variable.
"""
diff --git a/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py b/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
index 1ee6e6ab..1ab89a8d 100644
--- a/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
+++ b/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
@@ -2,6 +2,7 @@ import os
from leapp import reporting
from leapp.libraries.common.config import version
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
COMMON_REPORT_TAGS = [reporting.Groups.SANITY]
FMT_LIST_SEPARATOR = '\n - '
@@ -14,7 +15,9 @@ def skip_check():
if os.getenv('LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE'):
reporting.create_report([
reporting.Title('Skipped OS release check'),
- reporting.Summary('Source RHEL release check skipped via LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE env var.'),
+ reporting.Summary(
+ 'Source system release check skipped via LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE env variable.'
+ ),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups(COMMON_REPORT_TAGS)
] + related)
@@ -24,7 +27,7 @@ def skip_check():
def check_os_version():
- """ Check the RHEL minor version and inhibit the upgrade if it does not match the supported ones """
+ """ Check the distro version and inhibit the upgrade if it does not match the supported ones """
if not version.is_supported_version():
supported_releases = []
for rel in version.SUPPORTED_VERSIONS:
@@ -33,7 +36,8 @@ def check_os_version():
current_release = ' '.join(version.current_version()).upper()
reporting.create_report([
reporting.Title(
- 'The installed OS version is not supported for the in-place upgrade to the target RHEL version'
+ 'The installed OS version is not supported for the in-place upgrade'
+ ' to the target {target_distro} version'.format_map(DISTRO_REPORT_NAMES)
),
reporting.Summary(
'The supported OS releases for the upgrade process:'
diff --git a/repos/system_upgrade/common/actors/checkosrelease/tests/test_checkosrelease.py b/repos/system_upgrade/common/actors/checkosrelease/tests/test_checkosrelease.py
index 1ca8a1d7..c1c43065 100644
--- a/repos/system_upgrade/common/actors/checkosrelease/tests/test_checkosrelease.py
+++ b/repos/system_upgrade/common/actors/checkosrelease/tests/test_checkosrelease.py
@@ -3,7 +3,8 @@ import os
from leapp import reporting
from leapp.libraries.actor import checkosrelease
from leapp.libraries.common.config import version
-from leapp.libraries.common.testutils import create_report_mocked, produce_mocked
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
from leapp.utils.report import is_inhibitor
@@ -26,6 +27,7 @@ def test_no_skip_check(monkeypatch):
def test_not_supported_release(monkeypatch):
+ monkeypatch.setattr(api, "current_actor", CurrentActorMocked())
monkeypatch.setattr(version, "is_supported_version", lambda: False)
monkeypatch.setattr(version, "get_source_major_version", lambda: '8')
monkeypatch.setattr(version, "current_version", lambda: ('bad', '8'))
--
2.53.0

View File

@ -1,113 +0,0 @@
From ce379f96f128b60d93e02f95351e2f718940d2f2 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 23 Feb 2026 12:45:59 +0100
Subject: [PATCH 15/44] rocecheck: Remove outadated check for source version
Jira: RHEL-130950
---
.../actors/rocecheck/libraries/rocecheck.py | 35 ++-----------------
.../rocecheck/tests/unit_test_rocecheck.py | 22 ------------
2 files changed, 3 insertions(+), 54 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
index 7549feb8..0ed2046a 100644
--- a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
+++ b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
@@ -1,6 +1,6 @@
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common.config import architecture, version
+from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import KernelCmdline, RoceDetected
@@ -37,36 +37,8 @@ def _fmt_list(items):
return ''.join([FMT_LIST_SEPARATOR.format(i) for i in items])
-def _report_old_version(roce):
+def _report_wrong_setup(roce):
roce_nics = roce.roce_nics_connected + roce.roce_nics_connecting
- reporting.create_report([
- reporting.Title('A newer version of RHEL 8 is required for the upgrade with RoCE.'),
- reporting.Summary(
- 'The RHEL 9 system uses different network schemes for NIC names'
- ' than RHEL 8.'
- ' RHEL {version} does not provide functionality to be able'
- ' to set the system configuration in a way the network interface'
- ' names used by RoCE are persistent on both (RHEL 8 and RHEL 9)'
- ' systems.'
- ' The in-place upgrade from the current version of RHEL to RHEL 9'
- ' will break the RoCE network configuration.'
- '\n\nRoCE detected on following NICs:{nics}'
- .format(
- version=version.get_source_version(),
- nics=_fmt_list(roce_nics)
- )
- ),
- reporting.Remediation(hint=(
- 'Update the system to RHEL 8.8 or newer using DNF and then reboot'
- ' the system prior the in-place upgrade to RHEL 9.'
- )),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([
- reporting.Groups.INHIBITOR,
- reporting.Groups.ACCESSIBILITY,
- reporting.Groups.SANITY,
- ]),
- ])
def _report_wrong_setup(roce):
@@ -128,7 +100,6 @@ def process():
# No used RoCE detected - nothing to do
api.current_logger().debug('Skipping RoCE checks: No RoCE card detected.')
return
- if version.matches_source_version('<= 8.6'):
- _report_old_version(roce)
+
if not is_kernel_arg_set():
_report_wrong_setup(roce)
diff --git a/repos/system_upgrade/el8toel9/actors/rocecheck/tests/unit_test_rocecheck.py b/repos/system_upgrade/el8toel9/actors/rocecheck/tests/unit_test_rocecheck.py
index b5511d17..70e4a7b3 100644
--- a/repos/system_upgrade/el8toel9/actors/rocecheck/tests/unit_test_rocecheck.py
+++ b/repos/system_upgrade/el8toel9/actors/rocecheck/tests/unit_test_rocecheck.py
@@ -52,7 +52,6 @@ def test_roce_noibmz(monkeypatch, arch):
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch))
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
- monkeypatch.setattr(rocecheck, '_report_old_version', mocked_do_not_call_me)
monkeypatch.setattr(rocecheck, '_report_wrong_setup', mocked_do_not_call_me)
monkeypatch.setattr(rocecheck, 'is_kernel_arg_set', mocked_do_not_call_me)
monkeypatch.setattr(rocecheck.api, 'consume', mocked_do_not_call_me)
@@ -76,27 +75,6 @@ def test_roce_ok(monkeypatch, msgs, version):
assert not reporting.create_report.called
-@pytest.mark.parametrize('msgs', (
- [_kernel_cmdline(['net.naming-scheme=rhel-8.7']), _roce(['eno'], [])],
- [_kernel_cmdline(['net.naming-scheme=rhel-8.7']), _roce([], ['eno'])],
- [_kernel_cmdline(['net.naming-scheme=rhel-8.6']), _roce(['eno'], [])],
- [_kernel_cmdline(['net.naming-scheme=rhel-8.6']), _roce(['eno', 'eno1'], ['enp'])],
- [_kernel_cmdline(['foo=bar']), _roce(['eno'], [])],
- [_kernel_cmdline(), _roce(['eno'], [])],
-))
-@pytest.mark.parametrize('version', ['8.0', '8.3', '8.6'])
-def test_roce_old_rhel(monkeypatch, msgs, version):
- curr_actor_mocked = CurrentActorMocked(arch=architecture.ARCH_S390X, src_ver=version, msgs=msgs)
- monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
- monkeypatch.setattr(reporting, "create_report", create_report_mocked())
- rocecheck.process()
- assert reporting.create_report.called
- assert any(
- 'version of RHEL' in report['title']
- for report in reporting.create_report.reports
- )
-
-
# NOTE: what about the situation when net.naming-scheme is configured multiple times???
@pytest.mark.parametrize('msgs', (
[_kernel_cmdline(['net.naming-scheme=rhel-8.6']), _roce(['eno'], [])],
--
2.53.0

View File

@ -1,114 +0,0 @@
From 18fa991962cb1198173f0ad08a34de27467c32fe Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 23 Feb 2026 13:59:35 +0100
Subject: [PATCH 16/44] rocecheck: Respect distro name in report
Jira: RHEL-130950
---
.../actors/rocecheck/libraries/rocecheck.py | 81 ++++++++++---------
1 file changed, 44 insertions(+), 37 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
index 0ed2046a..5014a8db 100644
--- a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
+++ b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
@@ -1,6 +1,7 @@
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common.config import architecture
+from leapp.libraries.common.config import architecture, get_target_distro_id
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import KernelCmdline, RoceDetected
@@ -40,45 +41,51 @@ def _fmt_list(items):
def _report_wrong_setup(roce):
roce_nics = roce.roce_nics_connected + roce.roce_nics_connecting
+ summary = (
+ 'The {target_distro} 9 system uses different network schemes for NIC names'
+ ' than {source_distro} 8.'
+ ' The below listed RoCE NICs need to be reconfigured to the new'
+ ' interface naming scheme in order to prevent loss of network'
+ ' access to your system via these interfaces after the upgrade.'
+ ' For more information, see: {url}'
+ '\n\nRoCE detected on the following NICs:{nics}'
+ ).format(
+ nics=_fmt_list(roce_nics),
+ url=DOC_URL,
+ **DISTRO_REPORT_NAMES,
+ )
+ remmediation_hint = (
+ 'Prerequisite for upgrading to {target_distro} {target_version}:'
+ 'In {source_distro} 8, all RoCE cards must be configured with the interface'
+ ' names they should have in {target_distro} {target_version}.\n'
+ 'For more information, see chapter 1.4 of the RHEL 8 Product'
+ ' Documentation (see the attached link) and follow these steps:\n'
+ '1.) determine the current interface device names of the RoCE'
+ ' cards that are in "connected to" or in "connecting" state\n'
+ '2.) determine if UID uniqueness is set for these cards\n'
+ '3.) compute new interface device names from the UID or the'
+ ' function ID, respectively\n'
+ '4.) change the network interface device names in ifcfg'
+ ' files\n'
+ '5.) set the kernel parameter net.naming-scheme=rhel-8.7 in the'
+ ' effective .conf file in /boot/loader/entries\n'
+ '6.) adjust other settings that rely on the interface device names'
+ ' (e.g. firewall) by changing the interface device names'
+ ' accordingly\n'
+ '7.) run `zipl -V` and reboot the system\n'
+ '8.) check your network connectivity\n'
+ '\n'
+ 'Caution: Creating an incorrect configuration might cause the loss'
+ ' of your network connection after reboot!'
+ ).format(
+ target_version="9" if get_target_distro_id() == "centos" else "9.x",
+ **DISTRO_REPORT_NAMES,
+ )
-def _report_wrong_setup(roce):
- roce_nics = roce.roce_nics_connected + roce.roce_nics_connecting
reporting.create_report([
reporting.Title('Invalid RoCE configuration for the in-place upgrade'),
- reporting.Summary(
- 'The RHEL 9 system uses different network schemes for NIC names'
- ' than RHEL 8.'
- ' The below listed RoCE NICs need to be reconfigured to the new'
- ' interface naming scheme in order to prevent loss of network'
- ' access to your system via these interfaces after the upgrade.'
- ' For more information, see: {url}'
- '\n\nRoCE detected on the following NICs:{nics}'
- .format(nics=_fmt_list(roce_nics), url=DOC_URL)
- ),
- reporting.Remediation(hint=(
- 'Prerequisite for upgrading to RHEL9.x:'
- 'In RHEL 8, all RoCE cards must be configured with the interface'
- ' names they should have in RHEL9.x.\n'
- 'For more information, see chapter 1.4 of the RHEL8 Product'
- ' Documentation (see the attached link) and follow these steps:\n'
- '1.) determine the current interface device names of the RoCE'
- ' cards that are in "connected to" or in "connecting" state\n'
- '2.) determine if UID uniqueness is set for these cards\n'
- '3.) compute new interface device names from the UID or the'
- ' function ID, respectively\n'
- '4.) change the network interface device names in ifcfg'
- ' files\n'
- '5.) set the kernel parameter net.naming-scheme=rhel-8.7 in the'
- ' effective .conf file in /boot/loader/entries\n'
- '6.) adjust other settings that rely on the interface device names'
- ' (e.g. firewall) by changing the interface device names'
- ' accordingly\n'
- '7.) run `zipl -V` and reboot the system\n'
- '8.) check your network connectivity\n'
- '\n'
- 'Caution: Creating an incorrect configuration might cause the loss'
- ' of your network connection after reboot!'
- )),
+ reporting.Summary(summary),
+ reporting.Remediation(hint=remmediation_hint),
reporting.ExternalLink(
title='Predictable network interface device names on the System z platform',
url=DOC_URL),
--
2.53.0

View File

@ -1,101 +0,0 @@
From 71aa340ff3c2b5b9cd25e0aa731ace1b87e46d7f Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 23 Feb 2026 14:15:18 +0100
Subject: [PATCH 17/44] opensshpermitrootlogincheck: Remove 7to8 related code
Jira: RHEL-130950
---
.../opensshpermitrootlogincheck/actor.py | 64 +------------------
1 file changed, 2 insertions(+), 62 deletions(-)
diff --git a/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py b/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
index 98d329ab..3aedfd5e 100644
--- a/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
+++ b/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
@@ -1,7 +1,7 @@
from leapp import reporting
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.actor.opensshpermitrootlogincheck import global_value, semantics_changes
+from leapp.libraries.actor.opensshpermitrootlogincheck import global_value
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api
from leapp.models import OpenSshConfig, Report
@@ -46,73 +46,13 @@ class OpenSshPermitRootLoginCheck(Actor):
'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
)
- if get_source_major_version() == '7':
- self.process7to8(config)
- elif get_source_major_version() == '8':
+ if get_source_major_version() == '8':
self.process8to9(config)
elif int(get_source_major_version()) >= 9:
pass
else:
api.current_logger().warning('Unknown source major version: {}'.format(get_source_major_version()))
- @staticmethod
- def process7to8(config):
- # when the config was not modified, we can pass this check and let the
- # rpm handle the configuration file update
- if not config.modified:
- return
-
- # When the configuration does not contain *any* PermitRootLogin directive and
- # the configuration file was locally modified, it will not get updated by
- # RPM and the user might be locked away from the server with new default
- if not config.permit_root_login:
- create_report([
- reporting.Title('Possible problems with remote login using root account'),
- reporting.Summary(
- 'OpenSSH configuration file does not explicitly state '
- 'the option PermitRootLogin in sshd_config file, '
- 'which will default in RHEL8 to "prohibit-password".'
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups(COMMON_REPORT_TAGS),
- reporting.Remediation(
- hint='If you depend on remote root logins using passwords, consider '
- 'setting up a different user for remote administration or adding '
- '"PermitRootLogin yes" to sshd_config. '
- 'If this change is ok for you, add explicit '
- '"PermitRootLogin prohibit-password" to your sshd_config '
- 'to ignore this inhibitor'
- ),
- reporting.Groups([reporting.Groups.INHIBITOR])
- ] + COMMON_RESOURCES)
- return
-
- # Check if there is at least one PermitRootLogin other than "no"
- # in match blocks (other than Match All).
- # This usually means some more complicated setup depending on the
- # default value being globally "yes" and being overwritten by this
- # match block
- if semantics_changes(config):
- create_report([
- reporting.Title('OpenSSH configured to allow root login'),
- reporting.Summary(
- 'OpenSSH is configured to deny root logins in match '
- 'blocks, but not explicitly enabled in global or '
- '"Match all" context. This update changes the '
- 'default to disable root logins using passwords '
- 'so your server might get inaccessible.'
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups(COMMON_REPORT_TAGS),
- reporting.Remediation(
- hint='Consider using different user for administrative '
- 'logins or make sure your configuration file '
- 'contains the line "PermitRootLogin yes" '
- 'in global context if desired.'
- ),
- reporting.Groups([reporting.Groups.INHIBITOR])
- ] + COMMON_RESOURCES)
-
@staticmethod
def process8to9(config):
# RHEL8 default sshd configuration file is not modified: It will get replaced by rpm and
--
2.53.0

View File

@ -1,60 +0,0 @@
From d5edd261a729f0a83aa9642bf3655acf63f8808e Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 23 Feb 2026 14:15:38 +0100
Subject: [PATCH 18/44] opensshpermitrootlogincheck: Respect target distro name
in report
Jira: RHEL-130950
---
.../actors/opensshpermitrootlogincheck/actor.py | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py b/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
index 3aedfd5e..93ee5021 100644
--- a/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
+++ b/repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py
@@ -3,6 +3,7 @@ from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.opensshpermitrootlogincheck import global_value
from leapp.libraries.common.config.version import get_source_major_version
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import OpenSshConfig, Report
from leapp.reporting import create_report
@@ -62,12 +63,12 @@ class OpenSshPermitRootLoginCheck(Actor):
create_report([
reporting.Title('Possible problems with remote login using root account'),
reporting.Summary(
- 'OpenSSH configuration file will get updated to RHEL9 '
+ 'OpenSSH configuration file will get updated to {target_distro} 9 '
'version, no longer allowing root login with password. '
'It is a good practice to use non-root administrative '
'user and non-password authentications, but if you rely '
'on the remote root login, this change can lock you out '
- 'of this system.'
+ 'of this system.'.format_map(DISTRO_REPORT_NAMES)
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups(COMMON_REPORT_TAGS),
@@ -93,11 +94,13 @@ class OpenSshPermitRootLoginCheck(Actor):
create_report([
reporting.Title('Remote root logins globally allowed using password'),
reporting.Summary(
- 'RHEL9 no longer allows remote root logins, but the '
- 'server configuration explicitly overrides this default. '
- 'The configuration file will not be updated and root is '
- 'still going to be allowed to login with password. '
- 'This is not recommended and considered as a security risk.'
+ '{target_distro} 9 no longer allows remote root logins, but '
+ 'the server configuration explicitly overrides this default. '
+ 'The configuration file will not be updated and root is still'
+ 'going to be allowed to login with password. This is not '
+ 'recommended and considered as a security risk. '.format_map(
+ DISTRO_REPORT_NAMES
+ )
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups(COMMON_REPORT_TAGS),
--
2.53.0

View File

@ -1,120 +0,0 @@
From 61dc43540b15c71a8a2c2d5705c5952de059b6d8 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 2 Mar 2026 18:05:37 +0100
Subject: [PATCH 19/44] checkifcfg_ifcfg: Respect distro name in report
Jira: RHEL-130950
---
.../checkifcfg/libraries/checkifcfg_ifcfg.py | 37 +++++++++++++------
.../checkifcfg/tests/unit_test_ifcfg.py | 9 ++++-
2 files changed, 32 insertions(+), 14 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py b/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py
index ed666350..79ede81e 100644
--- a/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py
+++ b/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py
@@ -1,6 +1,7 @@
import os
from leapp import reporting
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import IfCfg, InstalledRPM, RpmTransactionTasks
@@ -8,6 +9,12 @@ from leapp.models import IfCfg, InstalledRPM, RpmTransactionTasks
FMT_LIST_SEPARATOR = '\n - '
+def _format_files_list(files):
+ return "".join(
+ ["{}{}".format(FMT_LIST_SEPARATOR, f) for f in files]
+ )
+
+
def process():
TRUE_VALUES = ['yes', 'true', '1']
TYPE_MAP = {
@@ -74,12 +81,15 @@ def process():
if bad_type_files:
title = 'Network configuration for unsupported device types detected'
- summary = ('RHEL 9 does not support the legacy network-scripts'
- ' package that was deprecated in RHEL 8 in favor of'
- ' NetworkManager. Files for device types that are not'
- ' supported by NetworkManager are present in the system.'
- ' Files with the problematic configuration:{}').format(
- ''.join(['{}{}'.format(FMT_LIST_SEPARATOR, bfile) for bfile in bad_type_files])
+ summary = (
+ "{target_distro} 9 does not support the legacy network-scripts"
+ " package that was deprecated in {source_distro} 8 in favor of"
+ " NetworkManager. Files for device types that are not"
+ " supported by NetworkManager are present in the system."
+ " Files with the problematic configuration:{bad_files}".format(
+ bad_files=_format_files_list(bad_type_files),
+ **DISTRO_REPORT_NAMES,
+ )
)
remediation = ('Consult the nm-settings-ifcfg-rh(5) manual for'
' valid types of ifcfg files. Remove configuration'
@@ -104,12 +114,15 @@ def process():
if not_controlled_files:
title = 'Network configuration with disabled NetworkManager support detected'
- summary = ('RHEL 9 does not support the legacy network-scripts'
- ' package that was deprecated in RHEL 8 in favor of'
- ' NetworkManager. Configuration present in the system'
- ' prohibit NetworkManager from loading it.'
- ' Files with the problematic configuration:{}').format(
- ''.join(['{}{}'.format(FMT_LIST_SEPARATOR, bfile) for bfile in not_controlled_files])
+ summary = (
+ '{target_distro} 9 does not support the legacy network-scripts'
+ ' package that was deprecated in {source_distro} 8 in favor of'
+ ' NetworkManager. Configuration present in the system'
+ ' prohibit NetworkManager from loading it.'
+ ' Files with the problematic configuration:{bad_files}'
+ ).format(
+ bad_files=_format_files_list(not_controlled_files),
+ **DISTRO_REPORT_NAMES,
)
remediation = ('Ensure the ifcfg files comply with format described in'
' nm-settings-ifcfg-rh(5) manual and remove the'
diff --git a/repos/system_upgrade/el8toel9/actors/checkifcfg/tests/unit_test_ifcfg.py b/repos/system_upgrade/el8toel9/actors/checkifcfg/tests/unit_test_ifcfg.py
index ddabedf2..02ffc65c 100644
--- a/repos/system_upgrade/el8toel9/actors/checkifcfg/tests/unit_test_ifcfg.py
+++ b/repos/system_upgrade/el8toel9/actors/checkifcfg/tests/unit_test_ifcfg.py
@@ -1,3 +1,4 @@
+from leapp.libraries.common import distro
from leapp.models import IfCfg, IfCfgProperty, InstalledRPM, RPM, RpmTransactionTasks
from leapp.reporting import Report
from leapp.utils.report import is_inhibitor
@@ -85,10 +86,12 @@ def test_ifcfg_good_type(current_actor_context):
assert rpm_transaction.to_install == ["NetworkManager"]
-def test_ifcfg_not_controlled(current_actor_context):
+def test_ifcfg_not_controlled(monkeypatch, current_actor_context):
"""
Report if there's a NM_CONTROLLED=no file.
"""
+ monkeypatch.setattr(distro, 'get_source_distro_id', lambda: 'rhel')
+ monkeypatch.setattr(distro, 'get_target_distro_id', lambda: 'rhel')
current_actor_context.feed(IfCfg(
filename="/NM/ifcfg-eth0",
@@ -105,10 +108,12 @@ def test_ifcfg_not_controlled(current_actor_context):
assert "disabled NetworkManager" in report_fields['title']
-def test_ifcfg_unknown_type(current_actor_context):
+def test_ifcfg_unknown_type(monkeypatch, current_actor_context):
"""
Report if there's configuration for a type we don't recognize.
"""
+ monkeypatch.setattr(distro, 'get_source_distro_id', lambda: 'rhel')
+ monkeypatch.setattr(distro, 'get_target_distro_id', lambda: 'rhel')
current_actor_context.feed(IfCfg(
filename="/NM/ifcfg-pigeon0",
--
2.53.0

View File

@ -1,155 +0,0 @@
From 747e4467aafe7c97b2a02b67527af70083a89e91 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 2 Mar 2026 18:11:02 +0100
Subject: [PATCH 20/44] opensslproviders: Use distro name in the leapp comment
Jira: RHEL-130950
---
.../libraries/add_provider.py | 13 ++--
.../tests/test_add_provider.py | 66 ++++++++++++-------
2 files changed, 53 insertions(+), 26 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/opensslproviders/libraries/add_provider.py b/repos/system_upgrade/el8toel9/actors/opensslproviders/libraries/add_provider.py
index 91462f18..3bf4cbf8 100644
--- a/repos/system_upgrade/el8toel9/actors/opensslproviders/libraries/add_provider.py
+++ b/repos/system_upgrade/el8toel9/actors/opensslproviders/libraries/add_provider.py
@@ -2,13 +2,13 @@ import re
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
# The openssl configuration file
# TODO copied from opensslconfigscanner/libraries/readconf.py
CONFIG = '/etc/pki/tls/openssl.cnf'
-LEAPP_COMMENT = '# Modified by leapp during upgrade to RHEL 9\n'
APPEND_STRING = (
'[provider_sect]\n'
'default = default_sect\n'
@@ -22,6 +22,11 @@ APPEND_STRING = (
)
+def _get_leapp_comment():
+ # cannot access DISTRO_REPORT_NAMES at top level
+ return f"# Modified by leapp during upgrade to {DISTRO_REPORT_NAMES.target} 9\n"
+
+
def _add_lines(lines, add):
"""
Add lines to the list of lines. Breaking possible newlines onto separate items
@@ -76,12 +81,12 @@ def _modify_file(f, fail_on_error=True):
lines = f.readlines()
lines = _replace(lines, r"openssl_conf\s*=\s*default_modules",
"openssl_conf = openssl_init",
- LEAPP_COMMENT, True, fail_on_error)
+ _get_leapp_comment(), True, fail_on_error)
lines = _replace(lines, r"\[\s*default_modules\s*\]",
"[openssl_init]\n"
"providers = provider_sect",
- LEAPP_COMMENT, True, fail_on_error)
- lines = _append(lines, APPEND_STRING, LEAPP_COMMENT)
+ _get_leapp_comment(), True, fail_on_error)
+ lines = _append(lines, APPEND_STRING, _get_leapp_comment())
f.seek(0)
f.write(''.join(lines))
diff --git a/repos/system_upgrade/el8toel9/actors/opensslproviders/tests/test_add_provider.py b/repos/system_upgrade/el8toel9/actors/opensslproviders/tests/test_add_provider.py
index 78f2e9c6..c6e31c29 100644
--- a/repos/system_upgrade/el8toel9/actors/opensslproviders/tests/test_add_provider.py
+++ b/repos/system_upgrade/el8toel9/actors/opensslproviders/tests/test_add_provider.py
@@ -3,12 +3,14 @@ import pytest
from leapp.libraries.actor.add_provider import (
_add_lines,
_append,
+ _get_leapp_comment,
_modify_file,
_replace,
APPEND_STRING,
- LEAPP_COMMENT,
NotFoundException
)
+from leapp.libraries.common.testutils import CurrentActorMocked
+from leapp.libraries.stdlib import api
testdata = (
([], 'one', ['one\n']),
@@ -80,32 +82,52 @@ class MockFile:
testdata = (
- ('', ''),
- ('openssl_conf=default_modules\n',
- '{}# openssl_conf=default_modules\nopenssl_conf = openssl_init\n'.format(LEAPP_COMMENT)),
- ('openssl_conf = default_modules\n',
- '{}# openssl_conf = default_modules\nopenssl_conf = openssl_init\n'.format(LEAPP_COMMENT)),
- ('openssl_conf = default_modules\n',
- '{}# openssl_conf = default_modules\nopenssl_conf = openssl_init\n'.format(LEAPP_COMMENT)),
- (' openssl_conf = default_modules \n',
- '{}# openssl_conf = default_modules \nopenssl_conf = openssl_init\n'.format(LEAPP_COMMENT)),
- ('[default_modules]\n',
- '{}# [default_modules]\n[openssl_init]\nproviders = provider_sect\n'.format(LEAPP_COMMENT)),
- ('[ default_modules ]\n',
- '{}# [ default_modules ]\n[openssl_init]\nproviders = provider_sect\n'.format(LEAPP_COMMENT)),
- (' [ default_modules ] \n',
- '{}# [ default_modules ] \n[openssl_init]\nproviders = provider_sect\n'.format(LEAPP_COMMENT)),
- ('openssl_conf=default_modules\n[default_modules]\n',
- '{c}# openssl_conf=default_modules\nopenssl_conf = openssl_init\n'
- '{c}# [default_modules]\n[openssl_init]\nproviders = provider_sect\n'.format(c=LEAPP_COMMENT)),
+ ("", ""),
+ (
+ "openssl_conf=default_modules\n",
+ "{c}# openssl_conf=default_modules\nopenssl_conf = openssl_init\n",
+ ),
+ (
+ "openssl_conf = default_modules\n",
+ "{c}# openssl_conf = default_modules\nopenssl_conf = openssl_init\n",
+ ),
+ (
+ "openssl_conf = default_modules\n",
+ "{c}# openssl_conf = default_modules\nopenssl_conf = openssl_init\n",
+ ),
+ (
+ " openssl_conf = default_modules \n",
+ "{c}# openssl_conf = default_modules \nopenssl_conf = openssl_init\n",
+ ),
+ (
+ "[default_modules]\n",
+ "{c}# [default_modules]\n[openssl_init]\nproviders = provider_sect\n",
+ ),
+ (
+ "[ default_modules ]\n",
+ "{c}# [ default_modules ]\n[openssl_init]\nproviders = provider_sect\n",
+ ),
+ (
+ " [ default_modules ] \n",
+ "{c}# [ default_modules ] \n[openssl_init]\nproviders = provider_sect\n",
+ ),
+ (
+ "openssl_conf=default_modules\n[default_modules]\n",
+ "{c}# openssl_conf=default_modules\nopenssl_conf = openssl_init\n"
+ "{c}# [default_modules]\n[openssl_init]\nproviders = provider_sect\n",
+ ),
)
@pytest.mark.parametrize('file_content,expected', testdata)
-def test_modify_file(file_content, expected):
- f = MockFile(file_content)
+def test_modify_file(monkeypatch, file_content, expected):
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+
+ f = MockFile(file_content.format(c=_get_leapp_comment()))
# Test separate replaces and do not fail if pattern is not found
_modify_file(f, False)
- assert f.content == "{}{}{}\n".format(expected, LEAPP_COMMENT, APPEND_STRING)
+ assert f.content == "{}{}{}\n".format(
+ expected.format(c=_get_leapp_comment()), _get_leapp_comment(), APPEND_STRING
+ )
--
2.53.0

View File

@ -1,75 +0,0 @@
From 35ea535848223e32bd01d726a838abd2ae84fa3e Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Mon, 2 Mar 2026 18:30:51 +0100
Subject: [PATCH 21/44] opensslconfigcheck: Dont make the recommendation as
redhat if not rhel target
Jira: RHEL-130950
---
.../opensslconfigcheck/libraries/opensslconfigcheck.py | 9 +++++++--
.../tests/component_test_opensslconfigcheck.py | 7 +++++--
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/libraries/opensslconfigcheck.py b/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/libraries/opensslconfigcheck.py
index 07c1b22f..a686a7f2 100644
--- a/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/libraries/opensslconfigcheck.py
+++ b/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/libraries/opensslconfigcheck.py
@@ -1,4 +1,5 @@
from leapp import reporting
+from leapp.libraries.common.config import get_target_distro_id
from leapp.libraries.stdlib import api
@@ -228,14 +229,18 @@ def check_crypto_policies(config):
path=("default_modules", "ssl_conf", "ssl_module",
"system_default", "crypto_policy", ".include"),
value="/etc/crypto-policies/back-ends/opensslcnf.config"):
+
reporting.create_report([
reporting.Title('The OpenSSL configuration is missing the crypto policies integration'),
+
reporting.Summary(
'The OpenSSL configuration file `/etc/pki/tls/openssl.cnf` does not contain the '
'directive to include the system-wide crypto policies. This is not recommended '
- 'by Red Hat and can lead to decreasing overall system security and inconsistent '
+ '{} can lead to decreasing overall system security and inconsistent '
'behavior between applications. If you need to adjust the crypto policies to your '
- 'needs, it is recommended to use custom crypto policies.'
+ 'needs, it is recommended to use custom crypto policies.'.format(
+ "by Red Hat and" if get_target_distro_id() == "rhel" else "as it"
+ )
),
reporting.Severity(reporting.Severity.MEDIUM),
reporting.Groups([
diff --git a/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/tests/component_test_opensslconfigcheck.py b/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/tests/component_test_opensslconfigcheck.py
index d3363def..892cb7f1 100644
--- a/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/tests/component_test_opensslconfigcheck.py
+++ b/repos/system_upgrade/el8toel9/actors/opensslconfigcheck/tests/component_test_opensslconfigcheck.py
@@ -1,3 +1,4 @@
+from leapp.libraries.actor import opensslconfigcheck
from leapp.models import OpenSslConfig, OpenSslConfigBlock, OpenSslConfigPair, Report
@@ -12,7 +13,8 @@ def test_actor_execution_empty(current_actor_context):
assert not current_actor_context.consume(Report)
-def test_actor_execution_empty_modified(current_actor_context):
+def test_actor_execution_empty_modified(current_actor_context, monkeypatch):
+ monkeypatch.setattr(opensslconfigcheck, 'get_target_distro_id', lambda: 'rhel')
current_actor_context.feed(
OpenSslConfig(
blocks=[],
@@ -25,7 +27,8 @@ def test_actor_execution_empty_modified(current_actor_context):
assert 'missing the crypto policies integration' in r[0].report['title']
-def test_actor_execution_default_modified(current_actor_context):
+def test_actor_execution_default_modified(current_actor_context, monkeypatch):
+ monkeypatch.setattr(opensslconfigcheck, 'get_target_distro_id', lambda: 'rhel')
current_actor_context.feed(
OpenSslConfig(
openssl_conf='default_modules',
--
2.53.0

View File

@ -1,73 +0,0 @@
From 367654a5eeaa1a13d857ff04acdc1e7890550fc1 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 17:35:53 +0100
Subject: [PATCH 22/44] checkmicroarchitecture: Respect distro name in report
Also add stable report key, the title used for key computation was using
target major ver == 8:
'Current x86-64 microarchitecture is unsupported in RHEL8'
Jira: RHEL-130950
---
.../libraries/checkmicroarchitecture.py | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py b/repos/system_upgrade/common/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py
index a063b534..3011f906 100644
--- a/repos/system_upgrade/common/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py
+++ b/repos/system_upgrade/common/actors/checkmicroarchitecture/libraries/checkmicroarchitecture.py
@@ -3,6 +3,7 @@ from collections import namedtuple
from leapp import reporting
from leapp.libraries.common.config.architecture import ARCH_X86_64, matches_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 CPUInfo
@@ -13,11 +14,11 @@ X86_64_V3_FLAGS = ['avx2', 'bmi1', 'bmi2', 'f16c', 'fma', 'abm', 'movbe', 'xsave
MicroarchInfo = namedtuple('MicroarchInfo', ('required_flags', 'extra_report_fields', 'microarch_ver'))
-def _inhibit_upgrade(missing_flags, target_rhel, microarch_ver, extra_report_fields=None):
- title = 'Current x86-64 microarchitecture is unsupported in {0}'.format(target_rhel)
+def _inhibit_upgrade(missing_flags, target_distro, microarch_ver, extra_report_fields=None):
+ title = 'Current x86-64 microarchitecture is unsupported in the target OS'
summary = ('{0} has a higher CPU requirement than older versions, it now requires a CPU '
'compatible with {1} instruction set or higher.\n\n'
- 'Missings flags detected are: {2}\n').format(target_rhel, microarch_ver, ', '.join(missing_flags))
+ 'Missings flags detected are: {2}\n').format(target_distro, microarch_ver, ', '.join(missing_flags))
report_fields = [
reporting.Title(title),
@@ -28,7 +29,8 @@ def _inhibit_upgrade(missing_flags, target_rhel, microarch_ver, extra_report_fie
reporting.Remediation(hint=('If a case of using virtualization, virtualization platforms often allow '
'configuring a minimum denominator CPU model for compatibility when migrating '
'between different CPU models. Ensure that minimum requirements are not below '
- 'that of {0}\n').format(target_rhel)),
+ 'that of {0}\n').format(target_distro)),
+ reporting.Key('0f48cdbe5aa2584e2ca7f4eb470b9b79da9e515d')
]
if extra_report_fields:
@@ -69,14 +71,15 @@ def process():
microarch_info = rhel_major_to_microarch_reqs.get(get_target_major_version())
if not microarch_info:
- api.current_logger().info('No known microarchitecture requirements are known for target RHEL%s.',
- get_target_major_version())
+ api.current_logger().info(
+ 'No known microarchitecture requirements are known'
+ ' for target {target_distro} {version}.'.format(**DISTRO_REPORT_NAMES, version=get_target_major_version()))
return
missing_flags = [flag for flag in microarch_info.required_flags if flag not in cpuinfo.flags]
api.current_logger().debug('Required flags missing: %s', missing_flags)
if missing_flags:
_inhibit_upgrade(missing_flags,
- 'RHEL{0}'.format(get_target_major_version()),
+ '{target_distro} {version}'.format(**DISTRO_REPORT_NAMES, version=get_target_major_version()),
microarch_info.microarch_ver,
extra_report_fields=microarch_info.extra_report_fields)
--
2.53.0

View File

@ -1,88 +0,0 @@
From 0e250cdc4672263d753209c420c15a33f9ae90eb Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 17:45:16 +0100
Subject: [PATCH 23/44] checkmemory: Respect distro name in reports
Also add stable key to the report, the distro key was computed as if for
7->8 upgrade, the title:
'Minimum memory requirements for RHEL 8 are not met'
as the reports for 8->9 and 9->10 are unified with the above report, the
following keys are no longer used:
Minimum memory requirements for RHEL 9 are not met -> e3066f6fae135718b3158597145554c4f22b9372
Minimum memory requirements for RHEL 10 are not met -> ccf14c3ac899f3cdc3123dadac399cafe28ce2ef
Jira: RHEL-130950
---
.../checkmemory/libraries/checkmemory.py | 33 ++++++++++---------
.../checkmemory/tests/test_checkmemory.py | 2 +-
2 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkmemory/libraries/checkmemory.py b/repos/system_upgrade/common/actors/checkmemory/libraries/checkmemory.py
index 040b404b..cdf8faa6 100644
--- a/repos/system_upgrade/common/actors/checkmemory/libraries/checkmemory.py
+++ b/repos/system_upgrade/common/actors/checkmemory/libraries/checkmemory.py
@@ -1,6 +1,6 @@
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
-from leapp.libraries.common.config import architecture, version
+from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import MemoryInfo
@@ -32,23 +32,24 @@ def process():
minimum_req_error = _check_memory(memoryinfo)
if minimum_req_error:
- title = 'Minimum memory requirements for RHEL {} are not met'.format(version.get_target_major_version())
+ title = "Minimum memory requirements for the target OS are not met"
summary = 'Memory detected: {} MiB, required: {} MiB'.format(
int(minimum_req_error['detected'] / 1024),
int(minimum_req_error['minimal_req'] / 1024),
)
reporting.create_report([
- reporting.Title(title),
- reporting.Summary(summary),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.SANITY, reporting.Groups.INHIBITOR]),
- reporting.ExternalLink(
- url='https://access.redhat.com/solutions/7014179',
- title='Leapp upgrade fail with error"Minimum memory requirements '
- 'for RHEL 8 are not met"Upgrade cannot proceed'
- ),
- reporting.ExternalLink(
- url='https://access.redhat.com/articles/rhel-limits',
- title='Red Hat Enterprise Linux Technology Capabilities and Limits'
- ),
- ])
+ reporting.Title(title),
+ reporting.Summary(summary),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.SANITY, reporting.Groups.INHIBITOR]),
+ reporting.ExternalLink(
+ url='https://access.redhat.com/solutions/7014179',
+ title='Leapp upgrade fail with error "Minimum memory requirements '
+ 'for RHEL 8 are not met"Upgrade cannot proceed'
+ ),
+ reporting.ExternalLink(
+ url='https://access.redhat.com/articles/rhel-limits',
+ title='Red Hat Enterprise Linux Technology Capabilities and Limits'
+ ),
+ reporting.Key('be50646b45beb8304c13daf5380d836a4be8e1cc'),
+ ])
diff --git a/repos/system_upgrade/common/actors/checkmemory/tests/test_checkmemory.py b/repos/system_upgrade/common/actors/checkmemory/tests/test_checkmemory.py
index 79158dc6..38ddfa33 100644
--- a/repos/system_upgrade/common/actors/checkmemory/tests/test_checkmemory.py
+++ b/repos/system_upgrade/common/actors/checkmemory/tests/test_checkmemory.py
@@ -21,7 +21,7 @@ def test_check_memory_high(monkeypatch):
def test_report(monkeypatch):
- title_msg = 'Minimum memory requirements for RHEL 9 are not met'
+ title_msg = 'Minimum memory requirements for the target OS are not met'
monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
monkeypatch.setattr(api, 'consume', lambda x: iter([MemoryInfo(mem_total=129)]))
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
--
2.53.0

View File

@ -1,126 +0,0 @@
From 73b2a3f76e6f808c381a29253ef7e1cd5bfd4f69 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 18:11:43 +0100
Subject: [PATCH 24/44] targetuserspacecreator: Respect distro name in report
Also add stable report key for no base repos inhibitor. The key was
computed with 'Cannot find required basic RHEL target repositories.' as
the title.
Jira: RHEL-130950
---
.../libraries/userspacegen.py | 34 ++++++++++---------
.../tests/unit_test_targetuserspacecreator.py | 10 +++---
2 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
index 7dbadae2..2475aad4 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
@@ -844,9 +844,8 @@ def _inhibit_if_no_base_repos(distro_repoids):
no_baseos = all("baseos" not in ri for ri in distro_repoids)
no_appstream = all("appstream" not in ri for ri in distro_repoids)
if no_baseos or no_appstream:
- reporting.create_report([
- # TODO: Make the report distro agnostic
- reporting.Title('Cannot find required basic RHEL target repositories.'),
+ report = [
+ reporting.Title('Cannot find required basic target OS repositories.'),
reporting.Summary(
'This can happen when a repository ID was entered incorrectly either while using the --enablerepo'
' option of leapp or in a third party actor that produces a CustomTargetRepositoryMessage.'
@@ -854,17 +853,6 @@ def _inhibit_if_no_base_repos(distro_repoids):
reporting.Groups([reporting.Groups.REPOSITORY]),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.INHIBITOR]),
- reporting.Remediation(hint=(
- 'It is required to have RHEL repositories on the system'
- ' provided by the subscription-manager unless the --no-rhsm'
- ' option is specified. You might be missing a valid SKU for'
- ' the target system or have a failed network connection.'
- ' Check whether your system is attached to a valid SKU that is'
- ' providing RHEL {} repositories.'
- ' If you are using Red Hat Satellite, read the upgrade documentation'
- ' to set up Satellite and the system properly.'
-
- ).format(target_major_version)),
reporting.ExternalLink(
url='https://access.redhat.com/solutions/5392811',
title='RHEL 7 to RHEL 8 LEAPP Upgrade Failing When Using Red Hat Satellite'
@@ -874,8 +862,22 @@ def _inhibit_if_no_base_repos(distro_repoids):
# https://red.ht/preparing-for-upgrade-to-rhel9
# https://red.ht/preparing-for-upgrade-to-rhel10
url='https://red.ht/preparing-for-upgrade-to-rhel{}'.format(target_major_version),
- title='Preparing for the upgrade')
- ])
+ title='Preparing for the upgrade'),
+ reporting.Key('f5770a56e540f27d370da7b697cb4a2e81e2c30d'),
+ ]
+ if get_target_distro_id() == 'rhel':
+ report.append(reporting.Remediation(hint=(
+ 'It is required to have RHEL repositories on the system'
+ ' provided by the subscription-manager unless the --no-rhsm'
+ ' option is specified. You might be missing a valid SKU for'
+ ' the target system or have a failed network connection.'
+ ' Check whether your system is attached to a valid SKU that is'
+ ' providing RHEL {} repositories.'
+ ' If you are using Red Hat Satellite, read the upgrade documentation'
+ ' to set up Satellite and the system properly.'
+ .format(target_major_version)))
+ )
+ reporting.create_report(report)
raise StopActorExecution()
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py b/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
index e8853979..b49eff56 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
@@ -1103,7 +1103,7 @@ def test_gather_target_repositories_none_available(monkeypatch):
reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
inhibitors = [m for m in reports if 'INHIBITOR' in m.get('flags', ())]
assert len(inhibitors) == 1
- assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'
+ assert inhibitors[0].get('title', '') == 'Cannot find required basic target OS repositories.'
@suppress_deprecation(models.RHELTargetRepository)
@@ -1166,7 +1166,7 @@ def test_gather_target_repositories_baseos_appstream_not_available(monkeypatch):
reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
inhibitors = [m for m in reports if 'inhibitor' in m.get('groups', ())]
assert len(inhibitors) == 1
- assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'
+ assert inhibitors[0].get('title', '') == 'Cannot find required basic target OS repositories.'
# Now test the case when either of AppStream and BaseOs is not available, upgrade should be inhibited
mocked_produce = produce_mocked()
monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
@@ -1181,7 +1181,7 @@ def test_gather_target_repositories_baseos_appstream_not_available(monkeypatch):
reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
inhibitors = [m for m in reports if 'inhibitor' in m.get('groups', ())]
assert len(inhibitors) == 1
- assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'
+ assert inhibitors[0].get('title', '') == 'Cannot find required basic target OS repositories.'
mocked_produce = produce_mocked()
monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
monkeypatch.setattr(userspacegen.api.current_actor(), 'produce', mocked_produce)
@@ -1195,7 +1195,7 @@ def test_gather_target_repositories_baseos_appstream_not_available(monkeypatch):
reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
inhibitors = [m for m in reports if 'inhibitor' in m.get('groups', ())]
assert len(inhibitors) == 1
- assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'
+ assert inhibitors[0].get('title', '') == 'Cannot find required basic target OS repositories.'
def test__get_distro_available_repoids_norhsm_norhui(monkeypatch):
@@ -1254,7 +1254,7 @@ def test__get_distro_available_repoids_nobaserepos_inhibit(
# TODO adjust the asserts when the report is made distro agnostic
assert reporting.create_report.called == 1
report = reporting.create_report.reports[0]
- assert "Cannot find required basic RHEL target repositories" in report["title"]
+ assert "Cannot find required basic target OS repositories" in report["title"]
assert reporting.Groups.INHIBITOR in report["groups"]
--
2.53.0

View File

@ -1,241 +0,0 @@
From 8e0aad41201e080482fbc279fd2d58bc11c20e91 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 18:46:16 +0100
Subject: [PATCH 25/44] checkdetecteddevicesanddirvers: Respect distro name in
reports
Also add stable keys to all reports, the keys were generated as if on
7->8 upgrade (target major version == 8), the exact titles used for
computation:
- 'Leapp detected loaded kernel drivers which have been removed in RHEL 8. Upgrade cannot proceed.'
- 'Leapp detected devices which are no longer supported in RHEL 8. Upgrade cannot proceed.'
- 'Leapp detected a processor which is no longer supported in RHEL 8. Upgrade cannot proceed.'
- 'Leapp detected loaded kernel drivers which are no longer maintained in RHEL 8.'
- 'Leapp detected devices which are no longer maintained in RHEL 8'
- 'Leapp detected a processor which is no longer maintained in RHEL 8.'
The following report titles and keys are no longer used as they've been
unified with the reports above and now also use the respective keys:
Leapp detected loaded kernel drivers which have been removed in RHEL 9. Upgrade cannot proceed.
- 7ae2961239eec9905e2580fa6309a589b1dca953
Leapp detected devices which are no longer supported in RHEL 9. Upgrade cannot proceed.
- 0e042eba4d14bd1f1ae691db6389621f778f21ad
Leapp detected a processor which is no longer supported in RHEL 9. Upgrade cannot proceed.
- f79672290945c09de12a14b28906b98d5c8ed68a
Leapp detected loaded kernel drivers which are no longer maintained in RHEL 9.
- b03c306f274b33b4cf3c7cd3764366c599681481
Leapp detected devices which are no longer maintained in RHEL 9
- 65ac6710649c928288162900e8df8316ac8e1bda
Leapp detected a processor which is no longer maintained in RHEL 9.
- 93f6cf039f01095a59ebaf581ced33316e034ef8
Leapp detected loaded kernel drivers which have been removed in RHEL 10. Upgrade cannot proceed.
- 48e04852631245aa4afee9adcdc7c2375b8cffb8
Leapp detected devices which are no longer supported in RHEL 10. Upgrade cannot proceed.
- fa9da5bf370c8477eb4f8366b68ffac2aaae6374
Leapp detected a processor which is no longer supported in RHEL 10. Upgrade cannot proceed.
- f6199d8526ee41c3e89b4ed11b3f8ee90cfc6f9f
Leapp detected loaded kernel drivers which are no longer maintained in RHEL 10.
- fbb11ae5828d624c4e4c91e73d766c8e27b066d9
Leapp detected devices which are no longer maintained in RHEL 10
- 93f67baabd93c00625fce5ad47edbf19972e41a0
Leapp detected a processor which is no longer maintained in RHEL 10.
- 9dde4bcd8b458bd6803462216785df3a1476c1f8
Jira: RHEL-130950
---
.../libraries/checkdddd.py | 70 +++++++++++--------
1 file changed, 41 insertions(+), 29 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py b/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
index 1f01adde..22a39c29 100644
--- a/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
+++ b/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
@@ -3,6 +3,7 @@ from enum import IntEnum
from leapp import reporting
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import DetectedDeviceOrDriver
@@ -22,17 +23,19 @@ def create_inhibitors(inhibiting_entries):
if drivers:
reporting.create_report([
reporting.Title(
- 'Leapp detected loaded kernel drivers which have been removed '
- 'in RHEL {}. Upgrade cannot proceed.'.format(get_target_major_version())
+ 'Leapp detected loaded kernel drivers that have been removed in'
+ ' the target OS. Upgrade cannot proceed.'
),
reporting.Summary(
(
- 'Support for the following RHEL {source} device drivers has been removed in RHEL {target}:\n'
+ 'Support for the following {source_distro} {source_version} device drivers has'
+ ' been removed in {target_distro} {target_version}:\n'
' - {drivers}\n'
).format(
drivers='\n - '.join([entry.driver_name for entry in drivers]),
- target=get_target_major_version(),
- source=get_source_major_version(),
+ target_version=get_target_major_version(),
+ source_version=get_source_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.ExternalLink(
@@ -52,52 +55,57 @@ def create_inhibitors(inhibiting_entries):
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.DRIVERS]),
reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.INHIBITOR])
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Key('f08a07da902958defa4f5c2699fae9ec2eb67c5b'),
])
devices = inhibiting_entries.get(MessagingClass.DEVICES)
if devices:
reporting.create_report([
reporting.Title(
- 'Leapp detected devices which are no longer supported in RHEL {}. Upgrade cannot proceed.'.format(
- get_target_major_version())
+ 'Leapp detected devices no longer supported by the target OS.'
+ ' Upgrade cannot proceed.'
),
reporting.Summary(
(
- 'Support for the following devices has been removed in RHEL {target}:\n'
+ 'Support for the following devices has been removed in {target_distro} {version}:\n'
' - {devices}\n'
).format(
devices='\n - '.join(['{name} ({pci})'.format(name=entry.device_name,
pci=entry.device_id) for entry in devices]),
- target=get_target_major_version(),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL]),
reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.INHIBITOR])
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Key('ccfc28592c82123649fc824c6c1c89cabfceae7c'),
])
cpus = inhibiting_entries.get(MessagingClass.CPUS)
if cpus:
reporting.create_report([
reporting.Title(
- 'Leapp detected a processor which is no longer supported in RHEL {}. Upgrade cannot proceed.'.format(
- get_target_major_version())
+ 'Leapp detected a processor no longer supported by the target OS.'
+ ' Upgrade cannot proceed.'
),
reporting.Summary(
(
- 'Support for the following processors has been removed in RHEL {target}:\n'
+ 'Support for the following processors has been removed in {target_distro} {version}:\n'
' - {processors}\n'
).format(
processors='\n - '.join([entry.device_name for entry in cpus]),
- target=get_target_major_version(),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.INHIBITOR])
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Key('e3e9e4d2566733e2f843db9823c8568b9b6922f9'),
])
@@ -109,65 +117,69 @@ def create_warnings(unmaintained_entries):
if drivers:
reporting.create_report([
reporting.Title(
- 'Leapp detected loaded kernel drivers which are no longer maintained in RHEL {}.'.format(
- get_target_major_version())
+ 'Leapp detected loaded kernel drivers no longer maintained in the target OS'
),
reporting.Summary(
(
- 'The following RHEL {source} device drivers are no longer maintained RHEL {target}:\n'
+ 'The following {source_distro} {source_version} device drivers are no longer'
+ ' maintained {target_distro} {target_version}:\n'
' - {drivers}\n'
).format(
drivers='\n - '.join([entry.driver_name for entry in drivers]),
- target=get_target_major_version(),
- source=get_source_major_version(),
+ target_version=get_target_major_version(),
+ source_version=get_source_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.DRIVERS]),
reporting.Severity(reporting.Severity.HIGH),
+ reporting.Key('0ff2413fd3cb0358736bf9be597f4dbdf58f2c4d'),
])
devices = unmaintained_entries.get(MessagingClass.DEVICES)
if devices:
reporting.create_report([
reporting.Title(
- 'Leapp detected devices which are no longer maintained in RHEL {}'.format(
- get_target_major_version())
+ 'Leapp detected devices no longer maintained in the target OS'
),
reporting.Summary(
(
- 'The support for the following devices has been removed in RHEL {target} and '
+ 'The support for the following devices has been removed in {target_distro} {version} and '
'are no longer maintained:\n - {devices}\n'
).format(
devices='\n - '.join(['{name} ({pci})'.format(name=entry.device_name,
pci=entry.device_id) for entry in devices]),
- target=get_target_major_version(),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL]),
reporting.Severity(reporting.Severity.HIGH),
+ reporting.Key('261e3e55a3a80346f2fcc2a1e59c64f7a4caa263'),
])
cpus = unmaintained_entries.get(MessagingClass.CPUS)
if cpus:
reporting.create_report([
reporting.Title(
- 'Leapp detected a processor which is no longer maintained in RHEL {}.'.format(
- get_target_major_version())
+ 'Leapp detected a processor no longer maintained in the target OS'
),
reporting.Summary(
(
- 'The following processors are no longer maintained in RHEL {target}:\n'
+ 'The following processors are no longer maintained in {target_distro} {version}:\n'
' - {processors}\n'
).format(
processors='\n - '.join([entry.device_name for entry in cpus]),
- target=get_target_major_version(),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
)
),
reporting.Audience('sysadmin'),
reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.BOOT]),
reporting.Severity(reporting.Severity.HIGH),
+ reporting.Key('61eb181bbc56328fbe03b5229d25a8ea5ebdc7a2'),
])
--
2.53.0

View File

@ -1,140 +0,0 @@
From e9fa8aac4b32baaaf171c1cd6cd25f88a78d36a0 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Mon, 2 Mar 2026 19:01:02 +0100
Subject: [PATCH 26/44] reportleftoverpackages: Respect distro name in reports
The actors is also moved from the reportleftoverpackages actor
subdirectory to the root actor directory.
Also add stable report keys to the reports. The keys were computed using
titles:
- 'Leftover RHEL packages have been removed'
- 'Some RHEL packages have not been upgraded'
Jira: RHEL-130950
---
.../{reportleftoverpackages => }/actor.py | 8 ++++----
.../libraries/reportleftoverpackages.py | 17 +++++++++++------
.../tests/test_reportleftoverpackages.py | 9 ++++++---
3 files changed, 21 insertions(+), 13 deletions(-)
rename repos/system_upgrade/common/actors/reportleftoverpackages/{reportleftoverpackages => }/actor.py (61%)
rename repos/system_upgrade/common/actors/reportleftoverpackages/{reportleftoverpackages => }/libraries/reportleftoverpackages.py (73%)
rename repos/system_upgrade/common/actors/reportleftoverpackages/{reportleftoverpackages => }/tests/test_reportleftoverpackages.py (86%)
diff --git a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/actor.py b/repos/system_upgrade/common/actors/reportleftoverpackages/actor.py
similarity index 61%
rename from repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/actor.py
rename to repos/system_upgrade/common/actors/reportleftoverpackages/actor.py
index 58573451..d0640774 100644
--- a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/actor.py
+++ b/repos/system_upgrade/common/actors/reportleftoverpackages/actor.py
@@ -7,11 +7,11 @@ from leapp.tags import IPUWorkflowTag, RPMUpgradePhaseTag
class ReportLeftoverPackages(Actor):
"""
- Collect messages about leftover RHEL packages from older major versions and generate a report.
+ Generate a report about leftover distribution packages from older major versions.
- Depending on execution of previous actors,
- generated report contains information that there are still old RHEL packages
- present on the system, which makes it unsupported or lists packages that have been removed.
+ Generated report informs about old distribution packages present on the system,
+ which makes it unsupported, and lists packages that have been removed if there
+ were any.
"""
name = 'report_leftover_packages'
diff --git a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/libraries/reportleftoverpackages.py b/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
similarity index 73%
rename from repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/libraries/reportleftoverpackages.py
rename to repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
index 51bda931..cd45ac87 100644
--- a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/libraries/reportleftoverpackages.py
+++ b/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
@@ -1,4 +1,5 @@
from leapp import reporting
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
from leapp.libraries.stdlib import api
from leapp.models import LeftoverPackages, RemovedPackages
@@ -11,15 +12,16 @@ def process():
leftover_pkgs_to_remove = ['-'.join([pkg.name, pkg.version, pkg.release]) for pkg in leftover_packages.items]
if removed_packages and removed_packages.items:
- title = 'Leftover RHEL packages have been removed'
+ title = 'Leftover packages from the original OS have been removed'
removed = ['-'.join([pkg.name, pkg.version, pkg.release]) for pkg in removed_packages.items]
summary = (
- 'Following packages have been removed:{sep}{list}\n'
+ 'Following {source_distro} packages have been removed:{sep}{list}\n'
'Dependent packages may have been removed as well, please check that you are not missing '
'any packages.'
.format(
sep=FMT_LIST_SEPARATOR,
- list=FMT_LIST_SEPARATOR.join(removed)
+ list=FMT_LIST_SEPARATOR.join(removed),
+ **DISTRO_REPORT_NAMES
)
)
reporting.create_report([
@@ -27,23 +29,26 @@ def process():
reporting.Summary(summary),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.SANITY]),
+ reporting.Key('5afbad560709afa4e40a160e40dfd44788ba9c3b'),
] + [reporting.RelatedResource('package', pkg.name) for pkg in removed_packages.items])
return
if leftover_packages and leftover_packages.items:
summary = (
- 'Following RHEL packages have not been upgraded:{sep}{list}\n'
+ 'Following {source_distro} packages have not been upgraded:{sep}{list}\n'
'Please remove these packages to keep your system in supported state.'
.format(
sep=FMT_LIST_SEPARATOR,
- list=FMT_LIST_SEPARATOR.join(leftover_pkgs_to_remove)
+ list=FMT_LIST_SEPARATOR.join(leftover_pkgs_to_remove),
+ **DISTRO_REPORT_NAMES
)
)
reporting.create_report([
- reporting.Title('Some RHEL packages have not been upgraded'),
+ reporting.Title('Some packages from the original OS have not been upgraded'),
reporting.Summary(summary),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.SANITY]),
+ reporting.Key('d424c3132ed78a8632b5c73d919909e453107c06'),
] + [reporting.RelatedResource('package', pkg.name) for pkg in leftover_packages.items])
else:
api.current_logger().info('No leftover packages, skipping...')
diff --git a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/tests/test_reportleftoverpackages.py b/repos/system_upgrade/common/actors/reportleftoverpackages/tests/test_reportleftoverpackages.py
similarity index 86%
rename from repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/tests/test_reportleftoverpackages.py
rename to repos/system_upgrade/common/actors/reportleftoverpackages/tests/test_reportleftoverpackages.py
index ff493c57..9502bfd2 100644
--- a/repos/system_upgrade/common/actors/reportleftoverpackages/reportleftoverpackages/tests/test_reportleftoverpackages.py
+++ b/repos/system_upgrade/common/actors/reportleftoverpackages/tests/test_reportleftoverpackages.py
@@ -27,7 +27,10 @@ def test_no_removed_packages_leftover_present(monkeypatch):
reportleftoverpackages.process()
assert reporting.create_report.called == 1
- assert 'Some RHEL packages have not been upgraded' in reporting.create_report.report_fields['title']
+ assert (
+ 'Some packages from the original OS have not been upgraded'
+ in reporting.create_report.report_fields['title']
+ )
assert 'Following RHEL packages have not been upgraded' in reporting.create_report.report_fields['summary']
summary = 'Please remove these packages to keep your system in supported state.'
assert summary in reporting.create_report.report_fields['summary']
@@ -44,6 +47,6 @@ def test_removed_packages(monkeypatch):
reportleftoverpackages.process()
assert reporting.create_report.called == 1
- assert 'Leftover RHEL packages have been removed' in reporting.create_report.report_fields['title']
- assert 'Following packages have been removed' in reporting.create_report.report_fields['summary']
+ assert 'Leftover packages from the original OS have been removed' in reporting.create_report.report_fields['title']
+ assert 'Following RHEL packages have been removed' in reporting.create_report.report_fields['summary']
assert 'rpm-1.0-1.el7' in reporting.create_report.report_fields['summary']
--
2.53.0

View File

@ -1,32 +0,0 @@
From 9f0cbbcb5a75f77b226cf385b6179f93fbdf9bc0 Mon Sep 17 00:00:00 2001
From: Sergii Golovatiuk <sgolovat@redhat.com>
Date: Wed, 18 Mar 2026 17:19:38 +0100
Subject: [PATCH 27/44] fix(overlaygen): exclude hugetlbfs from overlay mounts
hugetlbfs should not be mounted in the overlay environment during the
upgrade. It's not a regular mount point. This patch adds it to the
OVERLAY_DO_NOT_MOUNT exclusion list.
Resolves: RHEL-156902
Signed-off-by: Sergii Golovatiuk <sgolovat@redhat.com>
---
repos/system_upgrade/common/libraries/overlaygen.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/repos/system_upgrade/common/libraries/overlaygen.py b/repos/system_upgrade/common/libraries/overlaygen.py
index 3091ab5b..9fdaadf9 100644
--- a/repos/system_upgrade/common/libraries/overlaygen.py
+++ b/repos/system_upgrade/common/libraries/overlaygen.py
@@ -10,7 +10,7 @@ from leapp.libraries.common.config import get_env
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api, CalledProcessError, run
-OVERLAY_DO_NOT_MOUNT = ('tmpfs', 'devtmpfs', 'devpts', 'sysfs', 'proc', 'cramfs', 'sysv', 'vfat')
+OVERLAY_DO_NOT_MOUNT = ('tmpfs', 'devtmpfs', 'devpts', 'sysfs', 'proc', 'cramfs', 'sysv', 'vfat', 'hugetlbfs')
# NOTE(pstodulk): what about using more closer values and than just multiply
# the final result by magical constant?... this number is most likely going to
--
2.53.0

View File

@ -1,121 +0,0 @@
From 4c303cb3d30f891aa5d5d3ff4b3675deb41c6385 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Mon, 16 Mar 2026 23:10:49 +0100
Subject: [PATCH 28/44] biosdevname: Fix check of naming scheme and tests
The original check matched also NIC names with suffixes that do not
correspond to biosdevname naming scheme (e.g. "em0net"). Make the
check strict for the whole ^string$ and update tests to cover the
scenario.
Also I made small refactorings in tests
* so in case of failure it's visible what input data caused the
failure
* minimize changes needed to do with planned update of Interface model
---
.../biosdevname/libraries/biosdevname.py | 4 +-
.../biosdevname/tests/test_biosdevname.py | 73 ++++++++-----------
2 files changed, 31 insertions(+), 46 deletions(-)
diff --git a/repos/system_upgrade/common/actors/biosdevname/libraries/biosdevname.py b/repos/system_upgrade/common/actors/biosdevname/libraries/biosdevname.py
index a6b4a242..e2f4b1d1 100644
--- a/repos/system_upgrade/common/actors/biosdevname/libraries/biosdevname.py
+++ b/repos/system_upgrade/common/actors/biosdevname/libraries/biosdevname.py
@@ -27,8 +27,8 @@ def is_vendor_dell():
def all_interfaces_biosdevname(interfaces):
# Biosdevname supports two naming schemes
- emx = re.compile('em[0-9]+')
- pxpy = re.compile('p[0-9]+p[0-9]+')
+ emx = re.compile('^em[0-9]+$')
+ pxpy = re.compile('^p[0-9]+p[0-9]+$')
for i in interfaces:
if emx.match(i.name) is None and pxpy.match(i.name) is None:
diff --git a/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py b/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
index 427eea54..3aa8c614 100644
--- a/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
+++ b/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
@@ -59,50 +59,35 @@ def test_is_vendor_is_not_dell(monkeypatch):
assert not biosdevname.is_vendor_dell()
-def test_all_interfaces_biosdevname(monkeypatch):
- pci_info = PCIAddress(domain="domain", function="function", bus="bus", device="device")
-
- interfaces = [
- Interface(
- name="eth0", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- )
- ]
- assert not biosdevname.all_interfaces_biosdevname(interfaces)
- interfaces = [
- Interface(
- name="em0", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- )
- ]
- assert biosdevname.all_interfaces_biosdevname(interfaces)
- interfaces = [
- Interface(
- name="p0p22", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- )
- ]
- assert biosdevname.all_interfaces_biosdevname(interfaces)
-
- interfaces = [
- Interface(
- name="p1p2", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- ),
- Interface(
- name="em2", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- ),
- ]
- assert biosdevname.all_interfaces_biosdevname(interfaces)
-
- interfaces = [
- Interface(
- name="p1p2", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- ),
- Interface(
- name="em2", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- ),
- Interface(
- name="eth0", mac="mac", vendor="dell", pci_info=pci_info, devpath="path", driver="drv"
- ),
- ]
- assert not biosdevname.all_interfaces_biosdevname(interfaces)
+def _gen_ifaces_by_names(names):
+ pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
+ interfaces = []
+ for nic_name in names:
+ interfaces.append(Interface(
+ name=nic_name,
+ devpath="path",
+ driver="drv",
+ mac="52:54:00:0b:4a:6d",
+ pci_info=pci,
+ vendor="dell",
+ ))
+ return interfaces
+
+
+@pytest.mark.parametrize(("interface_names", "expected_result"), (
+ (["eth0"], False),
+ (["preem0"], False),
+ (["em0post"], False),
+ (["prep0p22"], False),
+ (["p0p22post"], False),
+ (["em0"], True),
+ (["p0p22"], True),
+ (["em2", "p1p22"], True),
+ (["p1p2", "em2", "eth0"], False)
+))
+def test_all_interfaces_biosdevname(interface_names, expected_result):
+ interfaces = _gen_ifaces_by_names(interface_names)
+ assert biosdevname.all_interfaces_biosdevname(interfaces) == expected_result
def test_enable_biosdevname(monkeypatch):
--
2.53.0

View File

@ -1,340 +0,0 @@
From 8d8774c34518a6269d56f100c2fc312ec0bbbde1 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Fri, 13 Mar 2026 16:15:20 +0100
Subject: [PATCH 29/44] persistentnetnamesdisable: refactoring + fix ethX
detection
The original check of ethX interfaces was buggy as it detected all
interfaces with the eth[0-9] substring:
* eth0
* preeth0
* eth0post
...
The check has been corrected to search just for ethX kernel names,
skipping NIC names with additional prefixes or suffixes. Added test
for the ethX_count function. Tests have been slightly refactored to
minimize planned changes in the Interface model.
Also the actor has been originally executed in incorrect phase.
Moving the actor to CheckPhase where it should be. Also I refactorred
the actor a little bit, moving the code to the private library to
follow current best practices.
Jira: RHEL-3370
---
.../actors/persistentnetnamesdisable/actor.py | 63 +----------
.../libraries/persistentnetnamesdisable.py | 75 +++++++++++++
.../tests/test_persistentnetnamesdisable.py | 104 ++++++++++++++----
3 files changed, 163 insertions(+), 79 deletions(-)
create mode 100644 repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
index b0182982..15c43141 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
@@ -1,11 +1,8 @@
-import re
-
-from leapp import reporting
from leapp.actors import Actor
-from leapp.libraries.common.config.version import get_target_major_version
+from leapp.libraries.actor import persistentnetnamesdisable
from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
-from leapp.reporting import create_report, Report
-from leapp.tags import FactsPhaseTag, IPUWorkflowTag
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
class PersistentNetNamesDisable(Actor):
@@ -16,57 +13,7 @@ class PersistentNetNamesDisable(Actor):
name = 'persistentnetnamesdisable'
consumes = (PersistentNetNamesFacts,)
produces = (KernelCmdlineArg, Report)
- tags = (FactsPhaseTag, IPUWorkflowTag)
-
- @staticmethod
- def ethX_count(interfaces):
- ethX = re.compile('eth[0-9]+')
- count = 0
-
- for i in interfaces:
- if ethX.match(i.name):
- count = count + 1
- return count
-
- @staticmethod
- def single_eth0(interfaces):
- return len(interfaces) == 1 and interfaces[0].name == 'eth0'
-
- def disable_persistent_naming(self):
- self.log.info("Single eth0 network interface detected. Appending 'net.ifnames=0' to RHEL-8 kernel commandline")
- self.produce(KernelCmdlineArg(**{'key': 'net.ifnames', 'value': '0'}))
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
def process(self):
- interfaces = next(self.consume(PersistentNetNamesFacts)).interfaces
-
- if self.single_eth0(interfaces):
- self.disable_persistent_naming()
- elif len(interfaces) > 1 and self.ethX_count(interfaces) > 0:
- report_entries = [
- reporting.Title('Unsupported network configuration'),
- reporting.Summary(
- 'Detected multiple physical network interfaces where one or more use kernel naming (e.g. eth0). '
- 'Upgrade process can not continue because stability of names can not be guaranteed. '
- ),
- reporting.ExternalLink(
- title='How to Perform an In-Place Upgrade when Using Kernel-Assigned NIC Names',
- url='https://access.redhat.com/solutions/4067471'
- ),
- reporting.Remediation(
- hint='Rename all ethX network interfaces following the attached KB solution article.'
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.NETWORK]),
- reporting.Groups([reporting.Groups.INHIBITOR])
- ]
-
- if get_target_major_version() == '9':
- report_entries.append(
- reporting.ExternalLink(
- title='RHEL 8 to RHEL 9: inplace upgrade fails at '
- '"Network configuration for unsupported device types detected"',
- url='https://access.redhat.com/solutions/7009239'
- )
- )
-
- create_report(report_entries)
+ persistentnetnamesdisable.process()
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
new file mode 100644
index 00000000..0d1a90e2
--- /dev/null
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
@@ -0,0 +1,75 @@
+import re
+
+from leapp import reporting
+from leapp.libraries.common.config.version import get_target_major_version
+from leapp.libraries.stdlib import api
+from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
+from leapp.reporting import create_report
+
+
+def ethX_count(interfaces):
+ """
+ Count how many network interfaces with ethX naming is present.
+ """
+ ethX = re.compile('^eth[0-9]+$')
+ count = 0
+
+ for i in interfaces:
+ if ethX.match(i.name):
+ count = count + 1
+ return count
+
+
+def single_eth0(interfaces):
+ return len(interfaces) == 1 and interfaces[0].name == 'eth0'
+
+
+def disable_persistent_naming():
+ api.current_logger().info(
+ "Single eth0 network interface detected."
+ " Appending 'net.ifnames=0' for the target system kernel commandline"
+ )
+ api.produce(KernelCmdlineArg(**{'key': 'net.ifnames', 'value': '0'}))
+
+
+def report_ethX_ifaces():
+ report_entries = [
+ reporting.Title('Unsupported network configuration'),
+ reporting.Summary(
+ 'Detected multiple physical network interfaces where one or more'
+ ' use kernel naming (e.g. eth0). Upgrade process cannot continue'
+ ' because stability of names can not be guaranteed.'
+ ),
+ reporting.ExternalLink(
+ title='How to Perform an In-Place Upgrade when Using Kernel-Assigned NIC Names',
+ url='https://access.redhat.com/solutions/4067471'
+ ),
+ reporting.Remediation(
+ hint='Rename all ethX network interfaces following the attached KB solution article.'
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.NETWORK]),
+ reporting.Groups([reporting.Groups.INHIBITOR])
+ ]
+
+ if get_target_major_version() == '9':
+ report_entries.append(
+ reporting.ExternalLink(
+ title='RHEL 8 to RHEL 9: inplace upgrade fails at '
+ '"Network configuration for unsupported device types detected"',
+ url='https://access.redhat.com/solutions/7009239'
+ )
+ )
+
+ create_report(report_entries)
+
+
+def process():
+ interfaces = next(api.consume(PersistentNetNamesFacts)).interfaces
+
+ if single_eth0(interfaces):
+ disable_persistent_naming()
+ return
+
+ if len(interfaces) > 1 and ethX_count(interfaces):
+ report_ethX_ifaces()
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
index 95b695c0..2369e80f 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
@@ -1,17 +1,53 @@
import pytest
-from leapp.libraries.common.config import version
+from leapp.libraries.actor import persistentnetnamesdisable
from leapp.models import Interface, KernelCmdlineArg, PCIAddress, PersistentNetNamesFacts
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor
+def _gen_ifaces_by_names(names):
+ pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
+ interfaces = []
+ for nic_name in names:
+ interfaces.append(Interface(
+ name=nic_name,
+ devpath="/devices/platform/usb/cdc-wdm0",
+ driver="pcieport",
+ mac="52:54:00:0b:4a:6d",
+ pci_info=pci,
+ vendor="redhat",
+ ))
+ return interfaces
+
+
+@pytest.mark.parametrize(('interfaces', 'exp_result'), (
+ (_gen_ifaces_by_names(['eno1', 'eno2', 'myfoo00', 'nicname']), 0),
+ (_gen_ifaces_by_names(['preeth0', 'eth2post', 'preeth0post']), 0),
+ (_gen_ifaces_by_names(['eth0']), 1),
+ (_gen_ifaces_by_names(['eth0', 'eth1', 'eth01', 'eth4980']), 4),
+ (_gen_ifaces_by_names(['myeth0', 'eth0', 'something']), 1),
+))
+def test_ethX_count(interfaces, exp_result):
+ """
+ Test the correct detection of ethX interfaces.
+
+ It tests the bug causing https://issues.redhat.com/browse/RHEL-3370
+ """
+ assert persistentnetnamesdisable.ethX_count(interfaces) == exp_result
+
+
def test_actor_single_eth0(current_actor_context):
pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
- interface = [Interface(name="eth0", mac="52:54:00:0b:4a:6d", vendor="redhat",
- driver="pcieport", pci_info=pci,
- devpath="/devices/platform/usb/cdc-wdm0")]
+ interface = [Interface(
+ name="eth0",
+ mac="52:54:00:0b:4a:6d",
+ vendor="redhat",
+ driver="pcieport",
+ pci_info=pci,
+ devpath="/devices/platform/usb/cdc-wdm0"
+ )]
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
current_actor_context.run()
assert not current_actor_context.consume(Report)
@@ -21,15 +57,25 @@ def test_actor_single_eth0(current_actor_context):
'target_version', ['9', '10']
)
def test_actor_more_ethX(monkeypatch, current_actor_context, target_version):
- monkeypatch.setattr(version, 'get_target_major_version', lambda: target_version)
+ monkeypatch.setattr(persistentnetnamesdisable, 'get_target_major_version', lambda: target_version)
pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
- interface = [Interface(name="eth0", mac="52:54:00:0b:4a:6d", vendor="redhat",
- driver="pcieport", pci_info=pci1,
- devpath="/devices/platform/usb/cdc-wdm0"),
- Interface(name="eth1", mac="52:54:00:0b:4a:6a", vendor="redhat",
- driver="serial", pci_info=pci2,
- devpath="/devices/hidraw/hidraw0")]
+ interface = [
+ Interface(
+ name="eth0",
+ mac="52:54:00:0b:4a:6d",
+ vendor="redhat",
+ driver="pcieport",
+ pci_info=pci1,
+ devpath="/devices/platform/usb/cdc-wdm0"),
+ Interface(
+ name="eth1",
+ mac="52:54:00:0b:4a:6a",
+ vendor="redhat",
+ driver="serial",
+ pci_info=pci2,
+ devpath="/devices/hidraw/hidraw0")
+ ]
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
current_actor_context.run()
@@ -50,9 +96,15 @@ def test_actor_more_ethX(monkeypatch, current_actor_context, target_version):
def test_actor_single_int_not_ethX(current_actor_context):
pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
- interface = [Interface(name="tap0", mac="52:54:00:0b:4a:60", vendor="redhat",
- driver="pcieport", pci_info=pci,
- devpath="/devices/platform/usb/cdc-wdm0")]
+ interface = [
+ Interface(
+ name="tap0",
+ mac="52:54:00:0b:4a:60",
+ vendor="redhat",
+ driver="pcieport",
+ pci_info=pci,
+ devpath="/devices/platform/usb/cdc-wdm0")
+ ]
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
current_actor_context.run()
assert not current_actor_context.consume(Report)
@@ -62,15 +114,25 @@ def test_actor_single_int_not_ethX(current_actor_context):
'target_version', ['9', '10']
)
def test_actor_ethX_and_not_ethX(monkeypatch, current_actor_context, target_version):
- monkeypatch.setattr(version, 'get_target_major_version', lambda: target_version)
+ monkeypatch.setattr(persistentnetnamesdisable, 'get_target_major_version', lambda: target_version)
pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
- interface = [Interface(name="virbr0", mac="52:54:00:0b:4a:6d", vendor="redhat",
- driver="pcieport", pci_info=pci1,
- devpath="/devices/platform/usb/cdc-wdm0"),
- Interface(name="eth0", mac="52:54:00:0b:4a:6a", vendor="redhat",
- driver="serial", pci_info=pci2,
- devpath="/devices/hidraw/hidraw0")]
+ interface = [
+ Interface(
+ name="virbr0",
+ mac="52:54:00:0b:4a:6d",
+ vendor="redhat",
+ driver="pcieport",
+ pci_info=pci1,
+ devpath="/devices/platform/usb/cdc-wdm0"),
+ Interface(
+ name="eth0",
+ mac="52:54:00:0b:4a:6a",
+ vendor="redhat",
+ driver="serial",
+ pci_info=pci2,
+ devpath="/devices/hidraw/hidraw0")
+ ]
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
current_actor_context.run()
assert current_actor_context.consume(Report)
--
2.53.0

View File

@ -1,518 +0,0 @@
From cdbb91156d1b87883523eed476c9a5d16b4b4e20 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Wed, 21 Feb 2024 20:55:54 +0100
Subject: [PATCH 30/44] Fix scan of net interfaces: non-PCI, conflicting,
broken
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Non-PCI network devices (present usually on IBM Z architecture)
does not have ID_VENDOR_ID attribute and also there is nothing
we could put into Interface.pci_info about them.
For such devices:
* set empty string for Interface.vendor field
* and None value for pci_info.
The Interface model has been updated, allowing None value for
pci_info field.
Also some interfaces do not have to be "managed" by udev or can
provide incomplete data. This can happen e.g. in situations when
udev want to assign a NIC name that is already used by another
network interface. Such an interface stays with kernel name (ethX)
and udev DB contains only elementary information about it. This
led originally to a skip of the interface during system scanning
by leapp actors and such systems bypassed checks for presence of
ethX NIC names later. Usually such interfaces have been renamed after
the upgrade (either to a new non-ethX name, or they had a different
ethX value) which led to another issues.
The new solution requires just bare-minimum details to be always
present (like NIC name and device path; if missing, the skip is used
again). For other values we set an empty strings if missing.
Tests have been updated and extended to cover new changes, using
mainly real input data collected from affected systems.
Jira: RHEL-22371, RHEL-72140
Co-authored-by: Michal Hečko <michal.sk.com@gmail.com>
---
.../tests/test_persistentnetnames.py | 5 +
.../tests/test_persistentnetnamesinitramfs.py | 6 +
.../common/libraries/persistentnetnames.py | 35 ++-
.../tests/test_persistentnetnames_library.py | 243 ++++++++++++++++--
.../common/models/persistentnetnamesfacts.py | 57 +++-
5 files changed, 314 insertions(+), 32 deletions(-)
diff --git a/repos/system_upgrade/common/actors/persistentnetnames/tests/test_persistentnetnames.py b/repos/system_upgrade/common/actors/persistentnetnames/tests/test_persistentnetnames.py
index ffea5983..a47eeabe 100644
--- a/repos/system_upgrade/common/actors/persistentnetnames/tests/test_persistentnetnames.py
+++ b/repos/system_upgrade/common/actors/persistentnetnames/tests/test_persistentnetnames.py
@@ -32,6 +32,11 @@ class interfaces_mocked:
@pytest.mark.parametrize('count', [0, 1, 8, 256])
def test_run(monkeypatch, current_actor_context, count):
+ """
+ Basic test of interface scanner actor
+
+ Full testing of underlying function is covered in tests of common library.
+ """
monkeypatch.setattr(persistentnetnames, 'interfaces', interfaces_mocked(count))
current_actor_context.run()
assert len(current_actor_context.consume(PersistentNetNamesFacts)[0].interfaces) == count
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesinitramfs/tests/test_persistentnetnamesinitramfs.py b/repos/system_upgrade/common/actors/persistentnetnamesinitramfs/tests/test_persistentnetnamesinitramfs.py
index f149502b..5605af4b 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesinitramfs/tests/test_persistentnetnamesinitramfs.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesinitramfs/tests/test_persistentnetnamesinitramfs.py
@@ -32,6 +32,12 @@ class interfaces_mocked:
@pytest.mark.parametrize('count', [0, 1, 8, 256])
def test_run(monkeypatch, current_actor_context, count):
+ """
+ Basic functionality test.
+
+ The full testing of the underlying scanner function is covered by the common
+ library.
+ """
monkeypatch.setattr(persistentnetnames, 'interfaces', interfaces_mocked(count))
current_actor_context.run()
assert len(current_actor_context.consume(PersistentNetNamesFactsInitramfs)[0].interfaces) == count
diff --git a/repos/system_upgrade/common/libraries/persistentnetnames.py b/repos/system_upgrade/common/libraries/persistentnetnames.py
index 7fdf7eaa..48dee5f8 100644
--- a/repos/system_upgrade/common/libraries/persistentnetnames.py
+++ b/repos/system_upgrade/common/libraries/persistentnetnames.py
@@ -41,16 +41,41 @@ def interfaces():
try:
attrs['name'] = dev.sys_name
attrs['devpath'] = dev.device_path
- attrs['driver'] = dev['ID_NET_DRIVER']
- attrs['vendor'] = dev['ID_VENDOR_ID']
- attrs['pci_info'] = PCIAddress(**pci_info(dev['ID_PATH']))
- attrs['mac'] = dev.attributes.get('address')
+
+ # can be missing when the interface is not "managed" by udev
+ # TODO(pstodulk): check the MAC, I think that that one should be
+ # actually always in DB.
+ attrs['driver'] = dev.get('ID_NET_DRIVER', '')
+ attrs['mac'] = dev.attributes.get('address', '')
if isinstance(attrs['mac'], bytes):
attrs['mac'] = attrs['mac'].decode()
+
+ # pci info is not provided for cards that do not use PCI bus,
+ # also it can be missing if the interface is not "managed" by udev.
+ # Also vendor can be provided only for cards on PCI bus.
+ attrs['vendor'] = dev.get('ID_VENDOR_ID', '')
+ attrs['pci_info'] = None # default for non-PCI card
+ if dev.get('ID_PATH', '').startswith('pci-'):
+ attrs['pci_info'] = PCIAddress(**pci_info(dev['ID_PATH']))
except Exception as e: # pylint: disable=broad-except
# FIXME(msekleta): We should probably handle errors more granularly
# Maybe we should inhibit upgrade process at this point
- api.current_logger().warning('Failed to gather information about network interface: %s', e)
+ net_name = attrs.get('name', 'unknown-interface')
+ api.current_logger().warning(
+ 'Failed to gather information about network interface %s: "%s"',
+ net_name, str(e)
+ )
+ # NOTE(pstodulk): skipping the interface as it's really broken
+ # or unknown from the code in leapp-repository pov and another
+ # processing would lead now to a broken upgrades.
+ # Other values that are usually expected but can be missing
+ # in some situations are covered and do not raise this exception.
continue
+ if not all([attrs['driver'], attrs['mac']]):
+ api.current_logger().warning(
+ 'Information in udev about %s network interface is incomplete.',
+ attrs['name']
+ )
+
yield Interface(**attrs)
diff --git a/repos/system_upgrade/common/libraries/tests/test_persistentnetnames_library.py b/repos/system_upgrade/common/libraries/tests/test_persistentnetnames_library.py
index 74aa08fa..b45863d9 100644
--- a/repos/system_upgrade/common/libraries/tests/test_persistentnetnames_library.py
+++ b/repos/system_upgrade/common/libraries/tests/test_persistentnetnames_library.py
@@ -1,57 +1,252 @@
+import pytest
+
from leapp.libraries.common import persistentnetnames
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api
+from leapp.models import PCIAddress
-class AttributesTest:
- def __init__(self):
- self.attributes = {
- 'address': b'fa:16:3e:cd:26:5a'
- }
+class AttributesMocked:
+ def __init__(self, attributes):
+ self._attributes = attributes
- def get(self, attribute):
- if attribute in self.attributes:
- return self.attributes[attribute]
- raise KeyError
+ def get(self, key, default=None):
+ return self._attributes.get(key, default)
-class DeviceTest:
- def __init__(self):
- self.dict_data = {
- 'ID_NET_DRIVER': 'virtio_net',
- 'ID_VENDOR_ID': '0x1af4',
- 'ID_PATH': 'pci-0000:00:03.0',
- }
+class DeviceMocked:
+ def __init__(self, properties, attributes):
+ self.dict_data = properties
+ self._attributes = attributes
def __getitem__(self, key):
+ return self.dict_data[key]
+
+ def get(self, key, default=None):
if key in self.dict_data:
return self.dict_data[key]
- raise KeyError
+ return default
@property
def sys_name(self):
- return 'eth'
+ return self.dict_data['INTERFACE']
@property
def device_path(self):
- return '/devices/pci0000:00/0000:00:03.0/virtio0/net/eth0'
+ return self.dict_data['DEVPATH']
@property
def attributes(self):
- return AttributesTest()
+ return AttributesMocked(self._attributes)
+
+@pytest.mark.parametrize('input_mac', ('fa:16:3e:cd:26:5a', b'fa:16:3e:cd:26:5a'))
+def test_getting_interfaces_complete_good(monkeypatch, input_mac):
+ """
+ Detailed parsing of physical net interface with complete data
+ """
+ def mocked_physical_interfaces():
+ properties = {
+ 'CURRENT_TAGS': ':systemd:',
+ 'DEVPATH': '/devices/pci0000:17/0000:17:02.0/0000:19:00.0/net/eno3',
+ 'ID_BUS': 'pci',
+ 'ID_MM_CANDIDATE': '1',
+ 'ID_MODEL_FROM_DATABASE': 'NetXtreme BCM5720 Gigabit Ethernet PCIe',
+ 'ID_MODEL_ID': '0x165f',
+ 'ID_NET_DRIVER': 'tg3',
+ 'ID_NET_LABEL_ONBOARD': 'NIC3',
+ 'ID_NET_LINK_FILE': '/usr/lib/systemd/network/99-default.link',
+ 'ID_NET_NAME': 'eno3',
+ 'ID_NET_NAME_MAC': 'enx34735a9920fe',
+ 'ID_NET_NAME_ONBOARD': 'eno3',
+ 'ID_NET_NAME_PATH': 'enp25s0f0',
+ 'ID_NET_NAMING_SCHEME': 'rhel-9.0',
+ 'ID_OUI_FROM_DATABASE': 'Dell Inc.',
+ 'ID_PATH': 'pci-0000:19:00.0',
+ 'ID_PATH_TAG': 'pci-0000_19_00_0',
+ 'ID_PCI_CLASS_FROM_DATABASE': 'Network controller',
+ 'ID_PCI_SUBCLASS_FROM_DATABASE': 'Ethernet controller',
+ 'ID_VENDOR_FROM_DATABASE': 'Broadcom Inc. and subsidiaries',
+ 'ID_VENDOR_ID': '0x14e4',
+ 'IFINDEX': '4',
+ 'INTERFACE': 'eno3',
+ 'SUBSYSTEM': 'net',
+ 'SYSTEMD_ALIAS': '/sys/subsystem/net/devices/eno3',
+ 'TAGS': ':systemd:',
+ 'USEC_INITIALIZED': '16690226'
+ }
+ attributes = {
+ 'address': input_mac
+ }
+ return [DeviceMocked(properties, attributes)]
+
+ monkeypatch.setattr(persistentnetnames, 'physical_interfaces', mocked_physical_interfaces)
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ interface = next(persistentnetnames.interfaces())
-def provide_test_interfaces():
- return [DeviceTest()]
+ assert interface.name == 'eno3'
+ assert interface.devpath == '/devices/pci0000:17/0000:17:02.0/0000:19:00.0/net/eno3'
+ assert interface.driver == 'tg3'
+ assert interface.vendor == '0x14e4'
+ assert interface.mac == 'fa:16:3e:cd:26:5a'
+ assert interface.pci_info == PCIAddress(
+ domain='0000',
+ bus='19',
+ device='00',
+ function='0'
+ )
-def test_getting_interfaces(monkeypatch):
- monkeypatch.setattr(persistentnetnames, 'physical_interfaces', provide_test_interfaces)
+def test_getting_interfaces_complete_good_roce(monkeypatch):
+ """
+ ROCE net interface parsing
+ """
+ def mocked_physical_interfaces():
+ properties = {
+ 'CURRENT_TAGS': ':systemd:',
+ 'DEVPATH': '/devices/pci0201:00/0201:00:00.0/net/eno513',
+ 'ID_BUS': 'pci',
+ 'ID_MODEL_FROM_DATABASE': 'ConnectX Family mlx5Gen Virtual Function',
+ 'ID_MODEL_ID': '0x101e',
+ 'ID_NET_DRIVER': 'mlx5_core',
+ 'ID_NET_LINK_FILE': '/etc/systemd/network/10-anaconda-ifname-eno513.link',
+ 'ID_NET_NAME': 'eno513',
+ 'ID_NET_NAME_MAC': 'enx2219aef66069',
+ 'ID_NET_NAME_ONBOARD': 'eno513',
+ 'ID_NET_NAME_PATH': 'enP513p0s0',
+ 'ID_NET_NAME_SLOT': 'ens5912',
+ 'ID_NET_NAMING_SCHEME': 'rhel-9.0',
+ 'ID_PATH': 'pci-0201:00:00.0',
+ 'ID_PATH_TAG': 'pci-0201_00_00_0',
+ 'ID_PCI_CLASS_FROM_DATABASE': 'Network controller',
+ 'ID_PCI_SUBCLASS_FROM_DATABASE': 'Ethernet controller',
+ 'ID_VENDOR_FROM_DATABASE': 'Mellanox Technologies',
+ 'ID_VENDOR_ID': '0x15b3',
+ 'IFINDEX': '2',
+ 'INTERFACE': 'eno513',
+ 'SUBSYSTEM': 'net',
+ 'SYSTEMD_ALIAS': '/sys/subsystem/net/devices/eno513',
+ 'TAGS': ':systemd:',
+ 'USEC_INITIALIZED': '26747014'
+ }
+ attributes = {
+ 'address': b'22:19:ae:f6:60:69'
+ }
+
+ return [DeviceMocked(properties, attributes)]
+
+ monkeypatch.setattr(persistentnetnames, 'physical_interfaces', mocked_physical_interfaces)
monkeypatch.setattr(api, 'produce', produce_mocked())
+
interface = next(persistentnetnames.interfaces())
+
assert interface.name
assert interface.devpath
assert interface.driver
assert interface.vendor
+ assert interface.mac
assert interface.pci_info
+
+
+@pytest.mark.parametrize(('properties', 'attributes'), (
+ (
+ {
+ # artificial data
+ 'CURRENT_TAGS': ':systemd:',
+ 'DEVPATH': '/devices/whatever/net/eno3',
+ 'ID_MM_CANDIDATE': '1',
+ 'ID_MODEL_FROM_DATABASE': 'Foo',
+ 'ID_MODEL_ID': '0x0000',
+ 'ID_NET_DRIVER': 'tg3',
+ 'ID_NET_LABEL_ONBOARD': 'NIC3',
+ 'ID_NET_LINK_FILE': '/usr/lib/systemd/network/99-default.link',
+ 'ID_NET_NAME': 'eno3',
+ 'ID_NET_NAME_MAC': 'enx34735a9920fe',
+ 'ID_NET_NAME_ONBOARD': 'eno3',
+ 'ID_NET_NAME_PATH': 'enp25s0f0',
+ 'ID_NET_NAMING_SCHEME': 'rhel-9.0',
+ 'ID_OUI_FROM_DATABASE': 'Dell Inc.',
+ 'IFINDEX': '4',
+ 'INTERFACE': 'eno3',
+ 'SUBSYSTEM': 'net',
+ 'SYSTEMD_ALIAS': '/sys/subsystem/net/devices/eno3',
+ 'TAGS': ':systemd:',
+ 'USEC_INITIALIZED': '16690226'
+ }, {
+ 'address': b'fa:16:3e:cd:26:5a'
+ }
+ ), (
+ {
+ 'CURRENT_TAGS': ':systemd:',
+ 'DEVPATH': '/devices/css0/0.0.0001/0.0.0001/virtio1/net/enc1',
+ 'ID_NET_DRIVER': 'virtio_net',
+ 'ID_NET_LINK_FILE': '/usr/lib/systemd/network/99-default.link',
+ 'ID_NET_NAME': 'enc1',
+ 'ID_NET_NAME_MAC': 'enx001738010124',
+ 'ID_NET_NAME_PATH': 'enc1',
+ 'ID_NET_NAMING_SCHEME': 'rhel-9.0',
+ 'ID_OUI_FROM_DATABASE': 'International Business Machines',
+ 'ID_PATH': 'ccw-0.0.0001',
+ 'ID_PATH_TAG': 'ccw-0_0_0001',
+ 'IFINDEX': '2',
+ 'INTERFACE': 'enc1',
+ 'SUBSYSTEM': 'net',
+ 'SYSTEMD_ALIAS': '/sys/subsystem/net/devices/enc1',
+ 'TAGS': ':systemd:',
+ 'USEC_INITIALIZED': '3423981'
+ }, {
+ 'address': b'00:17:38:01:01:24'
+ }
+ )
+))
+def test_getting_interfaces_nonpci_good(monkeypatch, properties, attributes):
+ """
+ Processing of net interface with incomplete data
+ """
+ def mocked_physical_interfaces():
+ return [DeviceMocked(properties, attributes)]
+
+ monkeypatch.setattr(persistentnetnames, 'physical_interfaces', mocked_physical_interfaces)
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ interface = next(persistentnetnames.interfaces())
+
+ assert interface.name
+ assert interface.devpath
+ assert interface.driver
+ assert not interface.vendor
+ assert interface.mac
+ assert not interface.pci_info
+
+
+def test_getting_interfaces_incomplete_udev_conflict(monkeypatch):
+ """
+ Test parsing of conflicting interface.
+
+ Such interface is not managed by udev and the information we could get
+ about it is limited.
+ """
+ def mocked_physical_interfaces():
+ properties = {
+ 'DEVPATH': '/devices/pci0000:ae/0000:ae:00.0/0000:af:00.0/0000:b0:04.0/0000:b2:00.1/net/eth3',
+ 'IFINDEX': '9',
+ 'INTERFACE': 'eth3',
+ 'SUBSYSTEM': 'net'
+ }
+ attributes = {
+ 'address': b'fa:16:3e:cd:26:5a'
+ }
+ return [DeviceMocked(properties, attributes)]
+
+ monkeypatch.setattr(persistentnetnames, 'physical_interfaces', mocked_physical_interfaces)
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+
+ interface = next(persistentnetnames.interfaces())
+
+ assert interface.name
+ assert interface.devpath
+ assert not interface.driver
+ assert not interface.vendor
assert interface.mac
+ assert not interface.pci_info
diff --git a/repos/system_upgrade/common/models/persistentnetnamesfacts.py b/repos/system_upgrade/common/models/persistentnetnamesfacts.py
index 395b26f0..3c71cdcd 100644
--- a/repos/system_upgrade/common/models/persistentnetnamesfacts.py
+++ b/repos/system_upgrade/common/models/persistentnetnamesfacts.py
@@ -4,7 +4,10 @@ from leapp.topics import SystemInfoTopic
class PCIAddress(Model):
"""
- TODO: tbd
+ Network Interface PCI address.
+
+ This model should not be produced nor consumed by actors directly.
+ It's part of the Interface model.
"""
topic = SystemInfoTopic
@@ -16,16 +19,49 @@ class PCIAddress(Model):
class Interface(Model):
"""
- TODO: tbd - Interface or NetworkInterface?
+ Physical network interface
+
+ Contains information about a network interface collected from udev.
+ Data can be incomplete in case of issues or when the interface is not
+ managed by udev.
+
+ This model should not be produced or consumed by actors directly.
+ See PersistentNetNamesFacts or PersistentNetNamesFactsInitramfs.
"""
topic = SystemInfoTopic
name = fields.String()
+ """
+ Name of the interface.
+ """
+
devpath = fields.String()
+ """
+ Path to the device.
+ """
+
driver = fields.String()
+ """
+ Network interface driver identifier.
+ """
+
vendor = fields.String()
- pci_info = fields.Model(PCIAddress)
+ """
+ Numeric identifier of the hardware vendor on PCI bus.
+ """
+
+ pci_info = fields.Nullable(fields.Model(PCIAddress))
+ """
+ Parsed PCI address of the network interface.
+
+ The value is None if the network interface is not connected via PCI or it is not managed
+ by udev.
+ """
+
mac = fields.String()
+ """
+ MAC address of the network interface.
+ """
class PersistentNetNamesFacts(Model):
@@ -34,6 +70,9 @@ class PersistentNetNamesFacts(Model):
"""
topic = SystemInfoTopic
interfaces = fields.List(fields.Model(Interface))
+ """
+ List of network interfaces with information collected from udev.
+ """
class PersistentNetNamesFactsInitramfs(PersistentNetNamesFacts):
@@ -49,8 +88,17 @@ class RenamedInterface(Model):
"""
topic = SystemInfoTopic
+ # TODO(pstodulk) deprecate these fields and replace them by new ones
+ # or deprecate the model completely.
rhel7_name = fields.String()
+ """
+ Original interface name.
+ """
+
rhel8_name = fields.String()
+ """
+ New interface name.
+ """
class RenamedInterfaces(Model):
@@ -63,3 +111,6 @@ class RenamedInterfaces(Model):
topic = SystemInfoTopic
renamed = fields.List(fields.Model(RenamedInterface))
+ """
+ The list of renamed interfaces.
+ """
--
2.53.0

View File

@ -1,398 +0,0 @@
From c60f657713f130d3accee7ceb303d7286b6180d5 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Wed, 18 Mar 2026 17:57:56 +0100
Subject: [PATCH 31/44] persistentnetnamesconfig: Deprecate legacy
functionality
The legacy solution to make NIC names persistent during the upgrade
is buggy and usually it's not used nowadays. Creation of link files
in the legacy solution breaks bonding configuration and also does not
handle interfaces in case of InfiniBand and RDMA.
At this point, we plan to use of net naming scheme mandatory during
the upgrade (or at least the only solution for persistent NIC names).
Mark following symbols as deprecated:
* LEAPP_DISABLE_NET_NAMING_SCHEMES (envar)
* LEAPP_NO_NETWORK_RENAMING (envar)
* RenamedInterfaces (model)
* RenamedInterface (model)
Also the RenamedInterface model had fields `rhel7_name` & `rhel8_name`,
which has not actually correspond to the real state. So the fields
have been renamed instantly to `original_name` & `new_name`.
Also dropping the produce of InitrdInclude message in the actor as
the model is deprecated for a long time.
We could possibly still think about reporting changed interface names,
but such a report should happen in the last (FirstBoot) phase when
the networking should be already fully initialized. Such a report
would be just a check whether net naming scheme works as expected.
But it's not considered at this very moment.
---
docs/source/configuring-ipu/envars.md | 20 +++++++-
.../libraries-and-api/deprecations-list.md | 6 ++-
.../actors/persistentnetnamesconfig/actor.py | 19 ++++---
.../libraries/persistentnetnamesconfig.py | 27 +++++-----
.../tests/test_persistentnetnamesconfig.py | 50 ++++++++++---------
.../common/models/persistentnetnamesfacts.py | 21 ++++++--
6 files changed, 93 insertions(+), 50 deletions(-)
diff --git a/docs/source/configuring-ipu/envars.md b/docs/source/configuring-ipu/envars.md
index 72d00634..9fa37f4f 100644
--- a/docs/source/configuring-ipu/envars.md
+++ b/docs/source/configuring-ipu/envars.md
@@ -6,11 +6,21 @@ Below is a list of the general and development variables available.
### General variables
#### LEAPP_DISABLE_NET_NAMING_SCHEMES
-On RHEL 8 to 9 upgrades, by default, net.naming-scheme is used to make network interface names immutable during the upgrade. In this case an extra RPM named `rhel-net-naming-sysattrs` is installed to the target system and target userspace container, providing the definitions of the "profiles" for net.naming-scheme.
+The `net.naming-scheme` kernel command line option is used by default to make
+network interface names immutable during the upgrade.
+In this case an extra RPM named `rhel-net-naming-sysattrs` (or `net-naming-sysattrs`)
+is installed to the target system and target userspace container, providing
+the definitions of the "profiles" for `net.naming-scheme`.
-If set to `0`, the "legacy" mechanism is used where leapp writes .link files to prevent interfaces being renamed
+If set to `0`, the legacy mechanism is used where leapp writes .link files to prevent interfaces being renamed
after booting to post-upgrade system.
+```{warning}
+The variable is deprecated as it is a part of the legacy solution for handling
+NIC names during the upgrade. Current supported solution allows only using
+the `net.naming-scheme`.
+```
+
#### LEAPP_ENABLE_REPOS
Specify repositories (repoids) split by comma, that should be used during the in-place upgrade to the target system. Its overwritten automatically in case the --enablerepo option is used. Its recommended to use the --enablerepo option instead of the envar.
@@ -26,6 +36,12 @@ If set to `1`, Leapp does not register the system into Red Hat Lightspeed automa
#### LEAPP_NO_NETWORK_RENAMING
If set to `1`, the actor responsible to handle NICs names ends without doing anything. The actor usually creates UDEV rules to preserve original NICs in case they are changed. However, in some cases its not wanted and it leads in malfunction network configuration (e.g. in case the bonding is configured on the system). Its expected that NICs have to be handled manually if needed.
+```{warning}
+The variable is deprecated as it is a part of the legacy solution for
+handling NIC names during the upgrade. Current supported solution allows only
+using of the `net.naming-scheme`.
+```
+
##### LEAPP_NO_RHSM
If set to `1`, Leapp does not use Red Hat Subscription Management for the upgrade. Its equivalent to the --no-rhsm leapp option.
diff --git a/docs/source/libraries-and-api/deprecations-list.md b/docs/source/libraries-and-api/deprecations-list.md
index 679bf489..a074ebc1 100644
--- a/docs/source/libraries-and-api/deprecations-list.md
+++ b/docs/source/libraries-and-api/deprecations-list.md
@@ -13,7 +13,11 @@ framework, see {ref}`deprecation:list of the deprecated functionality in leapp`.
Only the versions in which a deprecation has been made are listed.
## Next release <span style="font-size:0.5em; font-weight:normal">(till TODO date)</span>
-- Note: nothing new deprecated yet
+- Environment variables
+ - **`LEAPP_DISABLE_NET_NAMING_SCHEMES`** - The `net.naming-scheme` kernel argument provides much better alternative to the legacy solution enabled using this variable. Hence the legacy solution is deprecated together with this environment variable.
+ - **`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.
## v0.24.0 <span style="font-size:0.5em; font-weight:normal">(till September 2026)</span>
- Shared libraries
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesconfig/actor.py b/repos/system_upgrade/common/actors/persistentnetnamesconfig/actor.py
index 2689d837..7a21b43a 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesconfig/actor.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesconfig/actor.py
@@ -1,7 +1,6 @@
from leapp.actors import Actor
from leapp.libraries.actor import persistentnetnamesconfig
from leapp.models import (
- InitrdIncludes,
PersistentNetNamesFacts,
PersistentNetNamesFactsInitramfs,
RenamedInterfaces,
@@ -11,21 +10,27 @@ from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation
-@suppress_deprecation(InitrdIncludes)
+@suppress_deprecation(RenamedInterfaces)
class PersistentNetNamesConfig(Actor):
"""
Generate udev persistent network naming configuration
- This actor generates systemd-udevd link files for each physical ethernet interface present on RHEL-7
- in case we notice that interface name differs on RHEL-8. Link file configuration will assign RHEL-7 version of
- a name. Actors produces list of interfaces which changed name between RHEL-7 and RHEL-8.
+ NOTE: This actor is deprecated and currently performs described actions
+ only if LEAPP_NO_NETWORK_RENAMING != 1 and LEAPP_DISABLE_NET_NAMING_SCHEMES == 1.
+
+ This actor generates systemd-udevd link files for each physical network
+ interface present on the original system if the interface name differs
+ on the target OS. Link file configuration will assign original name that has
+ been detected on the source OS.
+
+ Also produce list of interfaces which changed names during the upgrade
+ process.
"""
name = 'persistentnetnamesconfig'
consumes = (PersistentNetNamesFacts, PersistentNetNamesFactsInitramfs)
- produces = (RenamedInterfaces, InitrdIncludes, TargetInitramfsTasks)
+ produces = (RenamedInterfaces, TargetInitramfsTasks)
tags = (ApplicationsPhaseTag, IPUWorkflowTag)
- initrd_files = []
def process(self):
persistentnetnamesconfig.process()
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesconfig/libraries/persistentnetnamesconfig.py b/repos/system_upgrade/common/actors/persistentnetnamesconfig/libraries/persistentnetnamesconfig.py
index 189cd4d0..0618f090 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesconfig/libraries/persistentnetnamesconfig.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesconfig/libraries/persistentnetnamesconfig.py
@@ -2,10 +2,9 @@ import errno
import os
import re
-from leapp.libraries.common.config import get_env, version
+from leapp.libraries.common.config import get_env
from leapp.libraries.stdlib import api
from leapp.models import (
- InitrdIncludes,
PersistentNetNamesFacts,
PersistentNetNamesFactsInitramfs,
RenamedInterface,
@@ -37,18 +36,21 @@ def generate_link_file(interface):
return link_file
-@suppress_deprecation(InitrdIncludes)
+@suppress_deprecation(RenamedInterfaces, RenamedInterface)
def process():
are_net_schemes_enabled = get_env('LEAPP_DISABLE_NET_NAMING_SCHEMES', '0') != '1'
- is_upgrade_8to9 = version.get_target_major_version() == '9'
- if are_net_schemes_enabled and is_upgrade_8to9:
- # For 8>9 we are using net.naming_scheme kernel arg by default - do not generate link files
+ if are_net_schemes_enabled:
msg = ('Skipping generation of .link files renaming NICs as net.naming-scheme '
- '{LEAPP_DISABLE_NET_NAMING_SCHEMES != 1} is enabled and upgrade is 8>9')
- api.current_logger().info(msg)
+ '{LEAPP_DISABLE_NET_NAMING_SCHEMES != 1} is enabled.')
+ api.current_logger().debug(msg)
return
+ api.current_logger().warning(
+ 'LEAPP_DISABLE_NET_NAMING_SCHEMES=1 - Using deprecated handling'
+ ' of network interface names by creating link files.'
+ )
+
if get_env('LEAPP_NO_NETWORK_RENAMING', '0') == '1':
api.current_logger().info(
'Skipping handling of possibly renamed network interfaces: leapp executed with LEAPP_NO_NETWORK_RENAMING=1'
@@ -80,13 +82,13 @@ def process():
)
continue
- if source_name != target_name and get_env('LEAPP_NO_NETWORK_RENAMING', '0') != '1':
+ if source_name != target_name:
api.current_logger().warning('Detected interface rename {} -> {}.'.format(source_name, target_name))
- if re.search('eth[0-9]+', iface.name) is not None:
+ if re.search('^eth[0-9]+$', iface.name) is not None:
api.current_logger().warning('Interface named using eth prefix, refusing to generate link file')
- renamed_interfaces.append(RenamedInterface(**{'rhel7_name': source_name,
- 'rhel8_name': target_name}))
+ renamed_interfaces.append(RenamedInterface(**{'original_name': source_name,
+ 'new_name': target_name}))
continue
initrd_files.append(generate_link_file(iface))
@@ -108,7 +110,6 @@ def process():
api.current_logger().warning(msg)
api.produce(RenamedInterfaces(renamed=renamed_interfaces))
- api.produce(InitrdIncludes(files=initrd_files))
# TODO: cover actor by tests in future. I am skipping writing of tests
# now as some refactoring and bugfixing related to this actor
# is planned already.
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py b/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
index c584c7ea..b107612b 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
@@ -7,7 +7,8 @@ from leapp.libraries.actor import persistentnetnamesconfig
from leapp.libraries.common.config import mock_configs
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.models import (
- InitrdIncludes,
+ EnvVar,
+ IPUConfig,
Interface,
PCIAddress,
PersistentNetNamesFacts,
@@ -19,6 +20,19 @@ from leapp.models import (
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
CUR_DIR = ""
+CONFIG_DISABLED_NAMING_SCHEMES = IPUConfig(
+ leapp_env_vars=[
+ EnvVar(name='LEAPP_DEVEL', value='0'),
+ EnvVar(name='LEAPP_DISABLE_NET_NAMING_SCHEMES', value='1'),
+ ],
+ os_release=mock_configs.CONFIG.os_release,
+ version=mock_configs.CONFIG.version,
+ architecture=mock_configs.CONFIG.architecture,
+ kernel=mock_configs.CONFIG.kernel,
+ supported_upgrade_paths=mock_configs.CONFIG.supported_upgrade_paths,
+ distro=mock_configs.CONFIG.distro
+)
+
@pytest.fixture
def adjust_cwd():
@@ -56,12 +70,10 @@ def test_identical(current_actor_context):
interfaces = generate_interfaces(4)
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
- current_actor_context.run(config_model=mock_configs.CONFIG)
+ current_actor_context.run(config_model=CONFIG_DISABLED_NAMING_SCHEMES)
renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
- initrd_files = current_actor_context.consume(InitrdIncludes)[0]
t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
- assert initrd_files.files == t_initrafms_tasks.include_files
assert not renamed_interfaces.renamed
assert not t_initrafms_tasks.include_files
@@ -73,12 +85,10 @@ def test_renamed_single_noneth(monkeypatch, current_actor_context):
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
interfaces[0].name = 'n4'
current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
- current_actor_context.run(config_model=mock_configs.CONFIG)
+ current_actor_context.run(config_model=CONFIG_DISABLED_NAMING_SCHEMES)
renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
- initrd_files = current_actor_context.consume(InitrdIncludes)[0]
t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
- assert initrd_files.files == t_initrafms_tasks.include_files
assert not renamed_interfaces.renamed
assert len(t_initrafms_tasks.include_files) == 1
assert '/etc/systemd/network/10-leapp-n0.link' in t_initrafms_tasks.include_files
@@ -92,12 +102,10 @@ def test_renamed_swap_noneth(monkeypatch, current_actor_context):
interfaces[0].name = 'n3'
interfaces[3].name = 'n0'
current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
- current_actor_context.run(config_model=mock_configs.CONFIG)
+ current_actor_context.run(config_model=CONFIG_DISABLED_NAMING_SCHEMES)
renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
- initrd_files = current_actor_context.consume(InitrdIncludes)[0]
t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
- assert initrd_files.files == t_initrafms_tasks.include_files
assert not renamed_interfaces.renamed
assert len(t_initrafms_tasks.include_files) == 2
assert '/etc/systemd/network/10-leapp-n0.link' in t_initrafms_tasks.include_files
@@ -113,15 +121,13 @@ def test_renamed_single_eth(monkeypatch, current_actor_context):
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
interfaces[0].name = 'eth4'
current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
- current_actor_context.run(config_model=mock_configs.CONFIG)
+ current_actor_context.run(config_model=CONFIG_DISABLED_NAMING_SCHEMES)
renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
- initrd_files = current_actor_context.consume(InitrdIncludes)[0]
t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
- assert initrd_files.files == t_initrafms_tasks.include_files
assert len(renamed_interfaces.renamed) == 1
- assert renamed_interfaces.renamed[0].rhel7_name == 'eth0'
- assert renamed_interfaces.renamed[0].rhel8_name == 'eth4'
+ assert renamed_interfaces.renamed[0].original_name == 'eth0'
+ assert renamed_interfaces.renamed[0].new_name == 'eth4'
assert not t_initrafms_tasks.include_files
@@ -135,18 +141,16 @@ def test_renamed_swap_eth(monkeypatch, current_actor_context):
interfaces[0].name = 'eth3'
interfaces[3].name = 'eth0'
current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
- current_actor_context.run(config_model=mock_configs.CONFIG)
+ current_actor_context.run(config_model=CONFIG_DISABLED_NAMING_SCHEMES)
renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
- initrd_files = current_actor_context.consume(InitrdIncludes)[0]
t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
- assert initrd_files.files == t_initrafms_tasks.include_files
assert len(renamed_interfaces.renamed) == 2
for interface in renamed_interfaces.renamed:
- if interface.rhel7_name == 'eth0':
- assert interface.rhel8_name == 'eth3'
- elif interface.rhel7_name == 'eth3':
- assert interface.rhel8_name == 'eth0'
+ if interface.original_name == 'eth0':
+ assert interface.new_name == 'eth3'
+ elif interface.original_name == 'eth3':
+ assert interface.new_name == 'eth0'
assert not t_initrafms_tasks.include_files
@@ -179,7 +183,7 @@ def test_bz_1899455_crash_iface(monkeypatch, adjust_cwd):
monkeypatch.setattr(persistentnetnamesconfig.api, 'produce', produce_mocked())
persistentnetnamesconfig.process()
- for prod_models in [RenamedInterfaces, InitrdIncludes, TargetInitramfsTasks]:
+ for prod_models in [RenamedInterfaces, TargetInitramfsTasks]:
any(isinstance(i, prod_models) for i in persistentnetnamesconfig.api.produce.model_instances)
assert any('Some network devices' in x for x in persistentnetnamesconfig.api.current_logger.warnmsg)
diff --git a/repos/system_upgrade/common/models/persistentnetnamesfacts.py b/repos/system_upgrade/common/models/persistentnetnamesfacts.py
index 3c71cdcd..e5cf979b 100644
--- a/repos/system_upgrade/common/models/persistentnetnamesfacts.py
+++ b/repos/system_upgrade/common/models/persistentnetnamesfacts.py
@@ -1,5 +1,6 @@
from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic
+from leapp.utils.deprecation import deprecated
class PCIAddress(Model):
@@ -82,25 +83,37 @@ class PersistentNetNamesFactsInitramfs(PersistentNetNamesFacts):
pass
+@deprecated(
+ since="2026-03-18",
+ message=(
+ "Information provided in this message is not always complete and it's"
+ " not used nowadays when net naming scheme is set during the upgrade."
+ )
+)
class RenamedInterface(Model):
"""
Provide original and new name of the network interface when renamed
"""
topic = SystemInfoTopic
- # TODO(pstodulk) deprecate these fields and replace them by new ones
- # or deprecate the model completely.
- rhel7_name = fields.String()
+ original_name = fields.String()
"""
Original interface name.
"""
- rhel8_name = fields.String()
+ new_name = fields.String()
"""
New interface name.
"""
+@deprecated(
+ since="2026-03-18",
+ message=(
+ "Information provided in this message is not always complete and it's"
+ " not used nowadays when net naming scheme is set during the upgrade."
+ )
+)
class RenamedInterfaces(Model):
"""
Provide list of renamed network interfaces
--
2.53.0

View File

@ -1,124 +0,0 @@
From 0872576049715c7a909379d5de5cc53bab3a408a Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Fri, 20 Mar 2026 17:19:20 +0100
Subject: [PATCH 32/44] persistentnetnamesdisable: Disable net.ifnames
correctly for single eth
Original solution disabled net.ifnames only for the target kernel
bootloader entry. But the upgrade initramfs stayed unhandled.
So it could happen that a "false positive" detection of changed
network interface names could been detected during the upgrade process
even when the target system booted again with "eth0" net interface
(because of net.ifnames=0). Set the parameter also for the upgrade
environment to behave same as on the target system.
Also, actor originally produced just the KernelCmdlineArg msg,
which is nowadays considered kind of obsoleted for this purpose
- still valid, but obsoleted. So produce UpgradeKernelCmdlineArgTasks
and TargetKernelCmdlineArgTasks msgs instead.
---
.../tests/test_persistentnetnamesconfig.py | 2 +-
.../common/actors/persistentnetnamesdisable/actor.py | 8 ++++++--
.../libraries/persistentnetnamesdisable.py | 11 +++++++++--
.../tests/test_persistentnetnamesdisable.py | 10 +++++++++-
4 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py b/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
index b107612b..f2519d77 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py
@@ -8,8 +8,8 @@ from leapp.libraries.common.config import mock_configs
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.models import (
EnvVar,
- IPUConfig,
Interface,
+ IPUConfig,
PCIAddress,
PersistentNetNamesFacts,
PersistentNetNamesFactsInitramfs,
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
index 15c43141..741e2c86 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
@@ -1,6 +1,10 @@
from leapp.actors import Actor
from leapp.libraries.actor import persistentnetnamesdisable
-from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
+from leapp.models import (
+ PersistentNetNamesFacts,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -12,7 +16,7 @@ class PersistentNetNamesDisable(Actor):
name = 'persistentnetnamesdisable'
consumes = (PersistentNetNamesFacts,)
- produces = (KernelCmdlineArg, Report)
+ produces = (Report, TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks)
tags = (ChecksPhaseTag, IPUWorkflowTag)
def process(self):
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
index 0d1a90e2..38eef133 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
@@ -3,7 +3,12 @@ import re
from leapp import reporting
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api
-from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
+from leapp.models import (
+ KernelCmdlineArg,
+ PersistentNetNamesFacts,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
from leapp.reporting import create_report
@@ -29,7 +34,9 @@ def disable_persistent_naming():
"Single eth0 network interface detected."
" Appending 'net.ifnames=0' for the target system kernel commandline"
)
- api.produce(KernelCmdlineArg(**{'key': 'net.ifnames', 'value': '0'}))
+ k_arg = KernelCmdlineArg(key='net.ifnames', value='0')
+ api.produce(UpgradeKernelCmdlineArgTasks(to_add=[k_arg]))
+ api.produce(TargetKernelCmdlineArgTasks(to_add=[k_arg]))
def report_ethX_ifaces():
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
index 2369e80f..81b5a28c 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
@@ -1,7 +1,13 @@
import pytest
from leapp.libraries.actor import persistentnetnamesdisable
-from leapp.models import Interface, KernelCmdlineArg, PCIAddress, PersistentNetNamesFacts
+from leapp.models import (
+ Interface,
+ PCIAddress,
+ PersistentNetNamesFacts,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor
@@ -51,6 +57,8 @@ def test_actor_single_eth0(current_actor_context):
current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
current_actor_context.run()
assert not current_actor_context.consume(Report)
+ assert current_actor_context.consume(UpgradeKernelCmdlineArgTasks)
+ assert current_actor_context.consume(TargetKernelCmdlineArgTasks)
@pytest.mark.parametrize(
--
2.53.0

View File

@ -1,303 +0,0 @@
From 806e65d67cac9e16c0341f366133ae3c14844fcd Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Wed, 25 Mar 2026 15:57:37 +0100
Subject: [PATCH 33/44] persistentnetnamesdisable: Mention net.naming-scheme in
report
Since RHEL 8 it's possible to specifcy net.naming-scheme to have
a consistent NIC names across RHEL major releases. When a specific
net.naming-scheme is not specified in kernel cmdline, suggest people
this option as well.
---
.../actors/persistentnetnamesdisable/actor.py | 15 +++-
.../libraries/persistentnetnamesdisable.py | 86 +++++++++++++++----
.../tests/test_persistentnetnamesdisable.py | 78 ++++++++++++++++-
3 files changed, 158 insertions(+), 21 deletions(-)
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
index 741e2c86..0bcc3827 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py
@@ -1,6 +1,7 @@
from leapp.actors import Actor
from leapp.libraries.actor import persistentnetnamesdisable
from leapp.models import (
+ KernelCmdline,
PersistentNetNamesFacts,
TargetKernelCmdlineArgTasks,
UpgradeKernelCmdlineArgTasks
@@ -11,11 +12,21 @@ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
class PersistentNetNamesDisable(Actor):
"""
- Disable systemd-udevd persistent network naming on machine with single eth0 NIC
+ Check whether the system has any (physical) NICs with kernel naming (ethX)
+
+ The kernel naming is in general unstable - there is no guarantee of persistent
+ NIC names between reboots, so eth0 can becaome eth3 and vice versa. If the
+ system has more than one physical network interface, the kernel naming must
+ not be used otherwise the upgrade is inhibited. The report contains remediation
+ hints for user to resolve the problem before the upgrade.
+
+ On systems with only one physical network interface with eth0 NIC, register
+ task to disable systemd-udevd persistent network naming (set `net.ifnames=0`
+ on kernel cmdline for the upgrade environment and the upgraded system.
"""
name = 'persistentnetnamesdisable'
- consumes = (PersistentNetNamesFacts,)
+ consumes = (PersistentNetNamesFacts, KernelCmdline)
produces = (Report, TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks)
tags = (ChecksPhaseTag, IPUWorkflowTag)
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
index 38eef133..0a4209b8 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/libraries/persistentnetnamesdisable.py
@@ -1,9 +1,11 @@
import re
from leapp import reporting
-from leapp.libraries.common.config.version import get_target_major_version
+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 (
+ KernelCmdline,
KernelCmdlineArg,
PersistentNetNamesFacts,
TargetKernelCmdlineArgTasks,
@@ -29,6 +31,40 @@ def single_eth0(interfaces):
return len(interfaces) == 1 and interfaces[0].name == 'eth0'
+def is_kernel_arg_present(key, value=None):
+ """
+ Return True if requested argument is set in kernel cmdline. Return False otherwise.
+
+ If the `value` is specified, check also whether the specific value is set.
+ The function consumes :class:`KernelCmdline`.
+
+ :param key: The kernel argument to search for
+ :type key: str
+ :param value: If string is specified, check for a specific string as well.
+ :type value: str|None
+ :rtype: bool
+ """
+ # NOTE(pstodulk): with small update a possible candidate to move into the
+ # kernel shared library. For now, keeping it just in this actor.
+ k_cmdline = next(api.consume(KernelCmdline), None)
+ if not k_cmdline:
+ # NOTE(pstodulk): this hypothetical situation, skipping coverage by
+ # unit tests
+ raise StopActorExecutionError(
+ message='Missing information about current kernel command line.',
+ details={
+ 'details': 'Missing the KernelCmdline message.'
+ }
+ )
+
+ for k_arg in k_cmdline.parameters:
+ if k_arg.key != key:
+ continue
+ if value is None or k_arg.value == value:
+ return True
+ return False
+
+
def disable_persistent_naming():
api.current_logger().info(
"Single eth0 network interface detected."
@@ -40,27 +76,30 @@ def disable_persistent_naming():
def report_ethX_ifaces():
- report_entries = [
- reporting.Title('Unsupported network configuration'),
- reporting.Summary(
- 'Detected multiple physical network interfaces where one or more'
- ' use kernel naming (e.g. eth0). Upgrade process cannot continue'
- ' because stability of names can not be guaranteed.'
- ),
+ url_title_kb = 'How to Perform an In-Place Upgrade when Using Kernel-Assigned NIC Names'
+ hint_text = f'Rename all ethX network interfaces following the "{url_title_kb}" solution article.'
+ report_external_links = [
reporting.ExternalLink(
- title='How to Perform an In-Place Upgrade when Using Kernel-Assigned NIC Names',
+ title=url_title_kb,
url='https://access.redhat.com/solutions/4067471'
- ),
- reporting.Remediation(
- hint='Rename all ethX network interfaces following the attached KB solution article.'
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.NETWORK]),
- reporting.Groups([reporting.Groups.INHIBITOR])
+ )
]
+ if not is_kernel_arg_present('net.naming-scheme') and not is_kernel_arg_present('net.ifnames', '0'):
+ hint_text += (
+ ' If the detected ethX interfaces are not manually configured, it is'
+ ' possible that new names were not assigned due to a naming conflict'
+ ' in the current `net.naming-scheme`. This can be resolved by'
+ ' configuring a newer naming scheme via the kernel argument.'
+ ' For more information, see "Implementing consistent network interface naming".'
+ )
+ # NOTE(pstodulk): the link is covered for RHEL 8, 9, 10
+ report_external_links.append(reporting.ExternalLink(
+ title='Implementing consistent network interface naming',
+ url='https://red.ht/rhel-{}-consistent-nic-naming'.format(get_source_major_version())
+ ))
if get_target_major_version() == '9':
- report_entries.append(
+ report_external_links.append(
reporting.ExternalLink(
title='RHEL 8 to RHEL 9: inplace upgrade fails at '
'"Network configuration for unsupported device types detected"',
@@ -68,6 +107,19 @@ def report_ethX_ifaces():
)
)
+ report_entries = [
+ reporting.Title('Unsupported network configuration'),
+ reporting.Summary(
+ 'Detected multiple physical network interfaces where one or more'
+ ' use kernel naming (e.g. eth0). Upgrade process cannot continue'
+ ' because stability of names can not be guaranteed.'
+ ),
+ reporting.Remediation(hint=hint_text),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.NETWORK]),
+ reporting.Groups([reporting.Groups.INHIBITOR])
+ ] + report_external_links
+
create_report(report_entries)
diff --git a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
index 81b5a28c..4b8669bb 100644
--- a/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
+++ b/repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py
@@ -1,8 +1,11 @@
import pytest
from leapp.libraries.actor import persistentnetnamesdisable
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.models import (
Interface,
+ KernelCmdline,
+ KernelCmdlineArg,
PCIAddress,
PersistentNetNamesFacts,
TargetKernelCmdlineArgTasks,
@@ -65,6 +68,7 @@ def test_actor_single_eth0(current_actor_context):
'target_version', ['9', '10']
)
def test_actor_more_ethX(monkeypatch, current_actor_context, target_version):
+ monkeypatch.setattr(persistentnetnamesdisable, 'get_source_major_version', lambda: str(int(target_version) - 1))
monkeypatch.setattr(persistentnetnamesdisable, 'get_target_major_version', lambda: target_version)
pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
@@ -84,7 +88,10 @@ def test_actor_more_ethX(monkeypatch, current_actor_context, target_version):
pci_info=pci2,
devpath="/devices/hidraw/hidraw0")
]
- current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
+ current_actor_context.feed(
+ PersistentNetNamesFacts(interfaces=interface),
+ KernelCmdline(parameters=[KernelCmdlineArg(key='what', value='ever')])
+ )
current_actor_context.run()
report_fields = current_actor_context.consume(Report)[0].report
@@ -122,6 +129,7 @@ def test_actor_single_int_not_ethX(current_actor_context):
'target_version', ['9', '10']
)
def test_actor_ethX_and_not_ethX(monkeypatch, current_actor_context, target_version):
+ monkeypatch.setattr(persistentnetnamesdisable, 'get_source_major_version', lambda: str(int(target_version) - 1))
monkeypatch.setattr(persistentnetnamesdisable, 'get_target_major_version', lambda: target_version)
pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
@@ -141,7 +149,10 @@ def test_actor_ethX_and_not_ethX(monkeypatch, current_actor_context, target_vers
pci_info=pci2,
devpath="/devices/hidraw/hidraw0")
]
- current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
+ current_actor_context.feed(
+ PersistentNetNamesFacts(interfaces=interface),
+ KernelCmdline(parameters=[KernelCmdlineArg(key='what', value='ever')])
+ )
current_actor_context.run()
assert current_actor_context.consume(Report)
@@ -158,3 +169,66 @@ def test_actor_ethX_and_not_ethX(monkeypatch, current_actor_context, target_vers
assert rhel8to9_present
else:
assert not rhel8to9_present
+
+
+@pytest.mark.parametrize(('result_expected', 'key', 'value'), (
+ (True, 'net.ifnames', None),
+ (True, 'net.ifnames', '0'),
+ (False, 'net.ifname', None),
+ (False, 'inet.ifnames', None),
+ (False, 'missing', None),
+ (False, 'missing', 'whatever'),
+ (False, 'net.ifnames', '1'),
+))
+def test_is_kernel_arg_present(monkeypatch, result_expected, key, value):
+ k_args = [
+ KernelCmdlineArg(key='Foo', value='0'),
+ KernelCmdlineArg(key='net.ifnames', value='0'),
+ KernelCmdlineArg(key='Something', value='None'),
+ ]
+ curr_actor_mocked = CurrentActorMocked(
+ msgs=[KernelCmdline(parameters=k_args)]
+ )
+ monkeypatch.setattr(persistentnetnamesdisable.api, 'current_actor', curr_actor_mocked)
+ assert result_expected is persistentnetnamesdisable.is_kernel_arg_present(key, value)
+
+
+@pytest.mark.parametrize(('naming_expected', 'k_args'), (
+ (False, [KernelCmdlineArg(key='net.ifnames', value='0')]),
+ (True, [KernelCmdlineArg(key='net.ifnames', value='1')]),
+ (True, [KernelCmdlineArg(key='net.naming-scheme-foo', value='rhel-8.10')]),
+ (
+ # NOTE(pstodulk): this is kind of nonsense, but let's test it
+ False,
+ [
+ KernelCmdlineArg(key='net.naming-scheme', value='rhel-8.10'),
+ KernelCmdlineArg(key='net.ifnames', value='0'),
+ ]
+ ),
+ (False, [KernelCmdlineArg(key='net.naming-scheme', value='rhel-8.10')]),
+ (False, [KernelCmdlineArg(key='net.naming-scheme', value='rhel-9.10')]),
+))
+@pytest.mark.parametrize('src_ver', ('8.10', '9.8', '10.6'))
+def test_report_ethx_ifaces_scheme(monkeypatch, naming_expected, src_ver, k_args):
+ _v_split = src_ver.split('.')
+ dst_ver = '{}.{}'.format(int(_v_split[0]) + 1, _v_split[1])
+ curr_actor_mocked = CurrentActorMocked(
+ msgs=[KernelCmdline(parameters=k_args)],
+ src_ver=src_ver,
+ dst_ver=dst_ver
+ )
+ monkeypatch.setattr(persistentnetnamesdisable, 'create_report', create_report_mocked())
+ monkeypatch.setattr(persistentnetnamesdisable.api, 'current_actor', curr_actor_mocked)
+
+ persistentnetnamesdisable.report_ethX_ifaces()
+ assert persistentnetnamesdisable.create_report.called
+ report = persistentnetnamesdisable.create_report.reports[0]
+
+ if naming_expected:
+ url = 'https://red.ht/rhel-{}-consistent-nic-naming'.format(_v_split[0])
+ assert any(url == link['url'] for link in report['detail']['external'])
+ assert 'net.naming-scheme' in report['detail']['remediations'][0]['context']
+ else:
+ url_str = 'consistent-nic-naming'
+ assert not any(url_str in link['url'] for link in report['detail']['external'])
+ assert 'net.naming-scheme' not in report['detail']['remediations'][0]['context']
--
2.53.0

View File

@ -1,46 +0,0 @@
From 0706cb54f4d1ba953de5e348cb03f9553b952669 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Tue, 17 Mar 2026 13:01:17 +0100
Subject: [PATCH 34/44] fix quoting in list representation of commands
Removed redundant quotes included due to incorrect handling of command
conversion to string representation in leapp framework's reporting
module. The reporting module is fixed by RHEL-156521 and this patch is a
followup that fixes usage of the module so that the commands are
propagated to the leapp-report.txt correctly.
Jira: RHEL-155517, RHEL-155515
---
.../actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py | 2 +-
repos/system_upgrade/common/actors/checkrootsymlinks/actor.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py b/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
index ce705361..be169e7e 100644
--- a/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
+++ b/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
@@ -21,7 +21,7 @@ def check_dnf_pluginpath(dnf_pluginpath_detected):
reporting.Remediation(
hint='Remove or comment out the pluginpath option in the DNF '
'configuration file to be able to upgrade the system',
- commands=[['sed', '-i', '\'s/^pluginpath[[:space:]]*=/#pluginpath=/\'', DNF_CONFIG_PATH]],
+ commands=[['sed', '-i', 's/^pluginpath[[:space:]]*=/#pluginpath=/', DNF_CONFIG_PATH]],
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Groups([reporting.Groups.INHIBITOR]),
diff --git a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
index 7b89bf7a..bee672bf 100644
--- a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
+++ b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
@@ -55,7 +55,7 @@ class CheckRootSymlinks(Actor):
os.path.relpath(item.target, '/'),
os.path.join('/', item.name)])
commands.append(command)
- rem_commands = [['sh', '-c', '"{}"'.format(' && '.join(commands))]]
+ rem_commands = [['sh', '-c', '{}'.format(' && '.join(commands))]]
# Generate reports about non-utf8 absolute links presence
nonutf_count = len(absolute_links_nonutf)
if nonutf_count > 0:
--
2.53.0

View File

@ -1,116 +0,0 @@
From a71fb36ca529b323a904adc2e9048a93074bf723 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Wed, 25 Mar 2026 18:01:42 +0100
Subject: [PATCH 35/44] fixup! fix quoting in list representation of commands
---
packaging/leapp-repository.spec | 2 +-
.../common/actors/checkrootsymlinks/actor.py | 6 +++---
.../libraries/checkyumpluginsenabled.py | 2 +-
.../common/actors/verifydialogs/libraries/verifydialogs.py | 7 ++++---
.../actors/inhibitcgroupsv1/libraries/inhibitcgroupsv1.py | 6 +++---
.../actors/inhibitcgroupsv1/tests/test_inhibitcgroupsv1.py | 4 ++--
6 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
index 97bc261a..3ffafafa 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.2, leapp-framework < 7
+Requires: leapp-framework >= 6.4, leapp-framework < 7
# Since we provide sub-commands for the leapp utility, we expect the leapp
# tool to be installed as well.
diff --git a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
index bee672bf..2e805542 100644
--- a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
+++ b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
@@ -52,10 +52,10 @@ class CheckRootSymlinks(Actor):
for item in absolute_links:
command = ' '.join(['ln',
'-snf',
- os.path.relpath(item.target, '/'),
- os.path.join('/', item.name)])
+ f"'{os.path.relpath(item.target, '/')}'",
+ f"'{os.path.join('/', item.name)}'"])
commands.append(command)
- rem_commands = [['sh', '-c', '{}'.format(' && '.join(commands))]]
+ rem_commands = [['bash', '-c', '{}'.format(' && '.join(commands))]]
# Generate reports about non-utf8 absolute links presence
nonutf_count = len(absolute_links_nonutf)
if nonutf_count > 0:
diff --git a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
index 87ff6511..9afa51ac 100644
--- a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
+++ b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
@@ -53,7 +53,7 @@ def check_required_dnf_plugins_enabled(pkg_manager_info):
.format(dnf_conf_path, plugin_configs_dir)
),
# Provide all commands as one due to problems with satellites
- commands=[['bash', '-c', '"{0}"'.format('; '.join(remediation_commands))]]
+ commands=[['bash', '-c', '{0}'.format('; '.join(remediation_commands))]]
),
reporting.ExternalLink(
url='https://access.redhat.com/solutions/7028063',
diff --git a/repos/system_upgrade/common/actors/verifydialogs/libraries/verifydialogs.py b/repos/system_upgrade/common/actors/verifydialogs/libraries/verifydialogs.py
index a79079b1..84b745ca 100644
--- a/repos/system_upgrade/common/actors/verifydialogs/libraries/verifydialogs.py
+++ b/repos/system_upgrade/common/actors/verifydialogs/libraries/verifydialogs.py
@@ -13,13 +13,14 @@ def check_dialogs(inhibit_if_no_userchoice=True):
dialogs_remediation = ('Please register user choices with leapp answer cli command or by manually editing '
'the answerfile.')
# FIXME: Enable more choices once we can do multi-command remediations
- cmd_remediation = [['leapp', 'answer', '--section', "{}={}".format(s, choice)]
- for s, choices in dialog.answerfile_sections.items() for choice in choices[:1]]
+ cmd_remediations = [['leapp', 'answer', '--section', '{}={}'.format(s, choice)]
+ for s, choices in dialog.answerfile_sections.items()
+ for choice in choices[:1]]
report_data = [reporting.Title('Missing required answers in the answer file'),
reporting.Severity(reporting.Severity.HIGH),
reporting.Summary(summary.format('\n'.join(sections))),
reporting.Groups([reporting.Groups.INHIBITOR] if inhibit_if_no_userchoice else []),
- reporting.Remediation(hint=dialogs_remediation, commands=cmd_remediation),
+ reporting.Remediation(hint=dialogs_remediation, commands=cmd_remediations),
reporting.ExternalLink(
url='https://access.redhat.com/solutions/7035321',
title='Leapp upgrade fail with error "Inhibitor: Missing required answers '
diff --git a/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/libraries/inhibitcgroupsv1.py b/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/libraries/inhibitcgroupsv1.py
index 6ae41be2..a54883b2 100644
--- a/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/libraries/inhibitcgroupsv1.py
+++ b/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/libraries/inhibitcgroupsv1.py
@@ -49,9 +49,9 @@ def process():
# remove the args from commandline, the defaults are the desired values
commands=[
[
- "grubby",
- "--update-kernel=ALL",
- '--remove-args="{}"'.format(" ".join(remediation_cmd_args)),
+ 'grubby',
+ '--update-kernel', 'ALL',
+ '--remove-args', '{}'.format(' '.join(remediation_cmd_args))
],
],
),
diff --git a/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/tests/test_inhibitcgroupsv1.py b/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/tests/test_inhibitcgroupsv1.py
index 9b3ec96f..629e8798 100644
--- a/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/tests/test_inhibitcgroupsv1.py
+++ b/repos/system_upgrade/el9toel10/actors/inhibitcgroupsv1/tests/test_inhibitcgroupsv1.py
@@ -39,9 +39,9 @@ def test_inhibit_should_inhibit(monkeypatch, cmdline_params):
assert reporting.Groups.INHIBITOR in report["groups"]
command = [r for r in report["detail"]["remediations"] if r["type"] == "command"][0]
- assert "systemd.unified_cgroup_hierarchy" in command['context'][2]
+ assert "systemd.unified_cgroup_hierarchy" in command['context'][4]
if len(cmdline_params) == 2:
- assert "systemd.legacy_systemd_cgroup_controller" in command['context'][2]
+ assert "systemd.legacy_systemd_cgroup_controller" in command['context'][4]
@pytest.mark.parametrize(
--
2.53.0

View File

@ -1,533 +0,0 @@
From 7d29232bdc45e15401d24a86f5508c3b3de826d1 Mon Sep 17 00:00:00 2001
From: Tomas Pelka <tpelka@redhat.com>
Date: Thu, 2 Apr 2026 09:24:33 +0200
Subject: [PATCH 36/44] pulseaudiocheck: Add PulseAudio config check for RHEL 9
to 10 upgrade
PulseAudio is replaced by PipeWire in RHEL 10. Add a scanner/checker
actor pair that detects custom PulseAudio configuration which won't
carry over after the upgrade.
The scanner (FactsCollectionPhase) checks for:
- Modified default config files via RPM verification
- Drop-in fragments in /etc/pulse/default.pa.d/ and system.pa.d/
- Per-user configuration in ~/.config/pulse/
The checker (ChecksPhase) consumes the scan results and produces
a report with a link to the PipeWire migration guide.
Resolves: DESKTOP-1151
---
.../pulseaudiocheck/checkpulseaudio/actor.py | 20 +++
.../libraries/checkpulseaudio.py | 86 ++++++++++++
.../tests/test_checkpulseaudio.py | 127 ++++++++++++++++++
.../pulseaudiocheck/scanpulseaudio/actor.py | 20 +++
.../libraries/scanpulseaudio.py | 98 ++++++++++++++
.../tests/test_scanpulseaudio.py | 77 +++++++++++
.../models/pulseaudioconfiguration.py | 24 ++++
7 files changed, 452 insertions(+)
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/actor.py
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/tests/test_checkpulseaudio.py
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/actor.py
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
create mode 100644 repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py
create mode 100644 repos/system_upgrade/el9toel10/models/pulseaudioconfiguration.py
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/actor.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/actor.py
new file mode 100644
index 00000000..9417d6d6
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/actor.py
@@ -0,0 +1,20 @@
+from leapp.actors import Actor
+from leapp.libraries.actor.checkpulseaudio import check_pulseaudio
+from leapp.models import DistributionSignedRPM, PulseAudioConfiguration, Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckPulseAudio(Actor):
+ """
+ Check for custom PulseAudio configuration that won't carry over after upgrade.
+
+ PulseAudio is replaced by PipeWire with the pipewire-pulseaudio compatibility
+ plugin in RHEL 10. Custom PulseAudio configuration will not be applied.
+ """
+ name = 'check_pulseaudio'
+ consumes = (DistributionSignedRPM, PulseAudioConfiguration)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ check_pulseaudio()
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
new file mode 100644
index 00000000..0453ca05
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
@@ -0,0 +1,86 @@
+from leapp import reporting
+from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+from leapp.libraries.common.rpms import has_package
+from leapp.libraries.stdlib import api
+from leapp.models import DistributionSignedRPM, PulseAudioConfiguration
+
+FMT_LIST_SEPARATOR = '\n - '
+
+
+def _report_custom_pulseaudio_config(modified_defaults, dropin_dirs, user_config_dirs):
+ """
+ Create a report warning about custom PulseAudio configuration.
+
+ :param modified_defaults: list of modified default config files
+ :type modified_defaults: list
+ :param dropin_dirs: list of drop-in directories with content
+ :type dropin_dirs: list
+ :param user_config_dirs: list of per-user config directories
+ :type user_config_dirs: list
+ """
+ details = []
+ if modified_defaults:
+ details.append(
+ 'The following default PulseAudio configuration files have been modified:{sep}{files}'.format(
+ sep=FMT_LIST_SEPARATOR,
+ files=FMT_LIST_SEPARATOR.join(modified_defaults),
+ )
+ )
+ if dropin_dirs:
+ details.append(
+ 'The following PulseAudio drop-in configuration directories contain custom '
+ 'fragments:{sep}{dirs}'.format(
+ sep=FMT_LIST_SEPARATOR,
+ dirs=FMT_LIST_SEPARATOR.join(dropin_dirs),
+ )
+ )
+ if user_config_dirs:
+ details.append(
+ 'Per-user PulseAudio configuration was found in:{sep}{dirs}'.format(
+ sep=FMT_LIST_SEPARATOR,
+ dirs=FMT_LIST_SEPARATOR.join(user_config_dirs),
+ )
+ )
+
+ summary = (
+ 'PulseAudio is replaced by PipeWire in {target_distro} 10. The PipeWire pipewire-pulseaudio plugin provides '
+ 'compatibility with the default PulseAudio configuration, but custom PulseAudio configuration will '
+ 'not be applied after the upgrade. Review your PulseAudio configuration and migrate any custom '
+ 'settings to PipeWire equivalents after upgrading.'
+ ).format_map(DISTRO_REPORT_NAMES)
+ if details:
+ summary += '\n\n' + '\n\n'.join(details)
+
+ all_paths = modified_defaults + dropin_dirs + user_config_dirs
+ reporting.create_report([
+ reporting.Title('Custom PulseAudio configuration detected'),
+ reporting.Summary(summary),
+ reporting.Severity(reporting.Severity.MEDIUM),
+ reporting.Groups([reporting.Groups.SERVICES]),
+ reporting.ExternalLink(title='Migrate PulseAudio to PipeWire',
+ url='https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Migrate-PulseAudio'),
+ reporting.Remediation(
+ hint='Review your PulseAudio configuration and plan to migrate custom settings to PipeWire '
+ 'after the upgrade. The pipewire-pulseaudio plugin handles default configuration automatically.'
+ ),
+ reporting.RelatedResource('package', 'pulseaudio'),
+ ] + [reporting.RelatedResource('file', f) for f in all_paths])
+
+
+def check_pulseaudio():
+ """
+ Consume PulseAudioConfiguration and generate report if custom config is found.
+ """
+ if not has_package(DistributionSignedRPM, 'pulseaudio'):
+ api.current_logger().debug('PulseAudio is not installed, skipping check.')
+ return
+
+ msg = next(api.consume(PulseAudioConfiguration), None)
+ if not msg:
+ api.current_logger().debug('No PulseAudioConfiguration message received.')
+ return
+
+ if msg.modified_defaults or msg.dropin_dirs or msg.user_config_dirs:
+ _report_custom_pulseaudio_config(msg.modified_defaults, msg.dropin_dirs, msg.user_config_dirs)
+ else:
+ api.current_logger().info('No custom PulseAudio configuration detected.')
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/tests/test_checkpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/tests/test_checkpulseaudio.py
new file mode 100644
index 00000000..518bfb73
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/tests/test_checkpulseaudio.py
@@ -0,0 +1,127 @@
+from leapp import reporting
+from leapp.libraries.actor.checkpulseaudio import check_pulseaudio
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import DistributionSignedRPM, PulseAudioConfiguration, RPM
+
+
+def _generate_rpm_with_name(name):
+ """
+ Generate new RPM model item with given name.
+
+ :param name: rpm name
+ :type name: str
+ :return: new RPM object with name parameter set
+ :rtype: RPM
+ """
+ return RPM(name=name,
+ version='0.1',
+ release='1.sm01',
+ epoch='1',
+ pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51',
+ packager='Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>',
+ arch='noarch')
+
+
+class TestCheckPulseaudio:
+ """Tests for check_pulseaudio checker function."""
+
+ def test_pulseaudio_not_installed(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('some-other-package')]
+ msg = PulseAudioConfiguration(modified_defaults=['/etc/pulse/daemon.conf'])
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert not reporting.create_report.called
+
+ def test_no_message(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms)])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert not reporting.create_report.called
+
+ def test_no_custom_config(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ msg = PulseAudioConfiguration()
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert not reporting.create_report.called
+
+ def test_modified_defaults(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ msg = PulseAudioConfiguration(
+ modified_defaults=['/etc/pulse/daemon.conf'],
+ )
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert reporting.create_report.called == 1
+ report_fields = reporting.create_report.report_fields
+ assert 'Custom PulseAudio configuration detected' in report_fields['title']
+ assert '/etc/pulse/daemon.conf' in report_fields['summary']
+
+ def test_dropin_dirs(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ msg = PulseAudioConfiguration(
+ dropin_dirs=['/etc/pulse/default.pa.d'],
+ )
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert reporting.create_report.called == 1
+ report_fields = reporting.create_report.report_fields
+ assert '/etc/pulse/default.pa.d' in report_fields['summary']
+
+ def test_user_config_dirs(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ msg = PulseAudioConfiguration(
+ user_config_dirs=['/home/admin/.config/pulse'],
+ )
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert reporting.create_report.called == 1
+ report_fields = reporting.create_report.report_fields
+ assert '/home/admin/.config/pulse' in report_fields['summary']
+
+ def test_report_all_sources(self, monkeypatch):
+ rpms = [_generate_rpm_with_name('pulseaudio')]
+ msg = PulseAudioConfiguration(
+ modified_defaults=['/etc/pulse/daemon.conf'],
+ dropin_dirs=['/etc/pulse/default.pa.d'],
+ user_config_dirs=['/root/.config/pulse'],
+ )
+ curr_actor_mocked = CurrentActorMocked(msgs=[DistributionSignedRPM(items=rpms), msg])
+ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ check_pulseaudio()
+
+ assert reporting.create_report.called == 1
+ report_fields = reporting.create_report.report_fields
+ resources = report_fields['detail']['related_resources']
+ pkg_resources = [r for r in resources if r['scheme'] == 'package']
+ file_resources = [r for r in resources if r['scheme'] == 'file']
+ assert len(pkg_resources) == 1
+ assert pkg_resources[0]['title'] == 'pulseaudio'
+ assert len(file_resources) == 3
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/actor.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/actor.py
new file mode 100644
index 00000000..5614c802
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/actor.py
@@ -0,0 +1,20 @@
+from leapp.actors import Actor
+from leapp.libraries.actor.scanpulseaudio import scan_pulseaudio
+from leapp.models import PulseAudioConfiguration
+from leapp.tags import FactsPhaseTag, IPUWorkflowTag
+
+
+class ScanPulseAudio(Actor):
+ """
+ Scan the system for PulseAudio custom configuration.
+
+ Detects whether PulseAudio is installed and checks for custom
+ configuration that will not carry over to PipeWire after upgrade.
+ """
+ name = 'scan_pulseaudio'
+ consumes = ()
+ produces = (PulseAudioConfiguration,)
+ tags = (FactsPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ self.produce(scan_pulseaudio())
diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
new file mode 100644
index 00000000..e5c2be53
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/libraries/scanpulseaudio.py
@@ -0,0 +1,98 @@
+import os
+import pwd
+
+from leapp.libraries.common.rpms import check_file_modification
+from leapp.models import PulseAudioConfiguration
+
+# System-wide PulseAudio configuration directory
+PULSEAUDIO_CONFIG_DIR = '/etc/pulse'
+
+# Drop-in directories included from default.pa and system.pa
+_DROPIN_DIRS = (
+ '/etc/pulse/default.pa.d',
+ '/etc/pulse/system.pa.d',
+)
+
+# Files that are part of the default PulseAudio installation
+_DEFAULT_CONFIG_FILES = frozenset((
+ 'client.conf',
+ 'daemon.conf',
+ 'default.pa',
+ 'system.pa',
+))
+
+# Per-user PulseAudio config directory relative to home
+_USER_CONFIG_SUBDIR = '.config/pulse'
+
+
+def _get_dropin_dirs_with_content():
+ """
+ Return list of drop-in directories that exist and contain files.
+
+ PulseAudio includes fragments from /etc/pulse/default.pa.d/ and
+ /etc/pulse/system.pa.d/ via .include directives. These directories
+ do not exist by default.
+
+ :return: list of drop-in directory paths that contain files
+ :rtype: list
+ """
+ found = []
+ for dropin_dir in _DROPIN_DIRS:
+ if os.path.isdir(dropin_dir) and os.listdir(dropin_dir):
+ found.append(dropin_dir)
+ return found
+
+
+def _get_user_config_dirs():
+ """
+ Return list of user home directories that contain PulseAudio configuration.
+
+ Checks ~/.config/pulse/ for each user with a valid home directory.
+
+ :return: list of per-user PulseAudio config directory paths
+ :rtype: list
+ """
+ found = []
+ for user in pwd.getpwall():
+ 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)
+ return sorted(found)
+
+
+def _check_default_configs_modified():
+ """
+ Check whether any of the default PulseAudio config files have been modified.
+
+ Uses RPM verification to detect changes to files owned by the pulseaudio
+ package. Returns list of modified default config file paths.
+
+ :return: list of modified default config file paths
+ :rtype: list
+ """
+ modified = []
+ for filename in sorted(_DEFAULT_CONFIG_FILES):
+ filepath = os.path.join(PULSEAUDIO_CONFIG_DIR, filename)
+ if os.path.isfile(filepath):
+ if check_file_modification(filepath):
+ modified.append(filepath)
+
+ return modified
+
+
+def scan_pulseaudio():
+ """
+ Scan the system for PulseAudio configuration and return findings.
+
+ :return: PulseAudioConfiguration message with scan results
+ :rtype: PulseAudioConfiguration
+ """
+ modified_defaults = _check_default_configs_modified()
+ dropin_dirs = _get_dropin_dirs_with_content()
+ user_config_dirs = _get_user_config_dirs()
+
+ return PulseAudioConfiguration(
+ modified_defaults=modified_defaults,
+ dropin_dirs=dropin_dirs,
+ user_config_dirs=user_config_dirs,
+ )
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
new file mode 100644
index 00000000..f499aafe
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/scanpulseaudio/tests/test_scanpulseaudio.py
@@ -0,0 +1,77 @@
+import os
+
+from leapp.libraries.actor import scanpulseaudio
+from leapp.libraries.actor.scanpulseaudio import _get_dropin_dirs_with_content, _get_user_config_dirs, scan_pulseaudio
+
+
+class TestGetDropinDirsWithContent:
+ """Tests for _get_dropin_dirs_with_content."""
+
+ def test_no_dropin_dirs(self, monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', lambda _: False)
+ assert _get_dropin_dirs_with_content() == []
+
+ def test_empty_dropin_dirs(self, monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', lambda _: True)
+ monkeypatch.setattr(os, 'listdir', lambda _: [])
+ assert _get_dropin_dirs_with_content() == []
+
+ def test_dropin_dirs_with_content(self, monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', lambda _: True)
+ monkeypatch.setattr(os, 'listdir', lambda _: ['custom.conf'])
+ result = _get_dropin_dirs_with_content()
+ assert result == ['/etc/pulse/default.pa.d', '/etc/pulse/system.pa.d']
+
+ def test_only_one_dropin_dir_exists(self, monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', lambda path: path == '/etc/pulse/default.pa.d')
+ monkeypatch.setattr(os, 'listdir', lambda _: ['custom.conf'])
+ result = _get_dropin_dirs_with_content()
+ assert result == ['/etc/pulse/default.pa.d']
+
+
+class TestGetUserConfigDirs:
+ """Tests for _get_user_config_dirs."""
+
+ def test_no_user_config(self, monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', lambda _: False)
+ assert _get_user_config_dirs() == []
+
+ def test_user_config_found(self, monkeypatch):
+ 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'
+
+ monkeypatch.setattr(scanpulseaudio.pwd, 'getpwall', lambda: [FakeUser()])
+ result = _get_user_config_dirs()
+ assert result == ['/home/testuser/.config/pulse']
+
+
+class TestScanPulseaudio:
+ """Tests for scan_pulseaudio main function."""
+
+ def test_no_custom_config(self, monkeypatch):
+ monkeypatch.setattr(scanpulseaudio, '_check_default_configs_modified', lambda: [])
+ monkeypatch.setattr(scanpulseaudio, '_get_dropin_dirs_with_content', lambda: [])
+ monkeypatch.setattr(scanpulseaudio, '_get_user_config_dirs', lambda: [])
+
+ result = scan_pulseaudio()
+
+ assert result.modified_defaults == []
+ assert result.dropin_dirs == []
+ assert result.user_config_dirs == []
+
+ def test_with_all_findings(self, monkeypatch):
+ monkeypatch.setattr(scanpulseaudio, '_check_default_configs_modified',
+ lambda: ['/etc/pulse/daemon.conf'])
+ monkeypatch.setattr(scanpulseaudio, '_get_dropin_dirs_with_content',
+ lambda: ['/etc/pulse/default.pa.d'])
+ monkeypatch.setattr(scanpulseaudio, '_get_user_config_dirs',
+ lambda: ['/root/.config/pulse'])
+
+ result = scan_pulseaudio()
+
+ assert result.modified_defaults == ['/etc/pulse/daemon.conf']
+ assert result.dropin_dirs == ['/etc/pulse/default.pa.d']
+ assert result.user_config_dirs == ['/root/.config/pulse']
diff --git a/repos/system_upgrade/el9toel10/models/pulseaudioconfiguration.py b/repos/system_upgrade/el9toel10/models/pulseaudioconfiguration.py
new file mode 100644
index 00000000..bfb7aa22
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/models/pulseaudioconfiguration.py
@@ -0,0 +1,24 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class PulseAudioConfiguration(Model):
+ """
+ Model describing the state of PulseAudio configuration on the system.
+ """
+ topic = SystemInfoTopic
+
+ modified_defaults = fields.List(fields.String(), default=[])
+ """
+ Default config files modified from RPM originals (full paths)
+ """
+
+ dropin_dirs = fields.List(fields.String(), default=[])
+ """
+ Drop-in directories that exist and contain files (full paths)
+ """
+
+ user_config_dirs = fields.List(fields.String(), default=[])
+ """
+ Per-user config directories that exist and contain files (full paths)
+ """
--
2.53.0

View File

@ -1,31 +0,0 @@
From 87c192519e8764f59516cca563816f062428a533 Mon Sep 17 00:00:00 2001
From: Peter Mocary <pmocary@redhat.com>
Date: Thu, 2 Apr 2026 16:28:48 +0200
Subject: [PATCH 37/44] fix(checkyumpluginsenabled): correctly create
remediation command
The remediation command for the inhibitor triggered when required dnf
plugins are not enabled was created incorrectly. This patch fixes the
command creation.
---
.../libraries/checkyumpluginsenabled.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
index 9afa51ac..869e2a88 100644
--- a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
+++ b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
@@ -36,8 +36,8 @@ def check_required_dnf_plugins_enabled(pkg_manager_info):
product_id_plugin_conf = os.path.join(plugin_configs_dir, 'product-id.conf')
remediation_commands = [
- f"sed -i 's/^plugins=0/plugins=1/' '{dnf_conf_path}'"
- f"sed -i 's/^enabled=0/enabled=1/' '{rhsm_plugin_conf}'"
+ f"sed -i 's/^plugins=0/plugins=1/' '{dnf_conf_path}'",
+ f"sed -i 's/^enabled=0/enabled=1/' '{rhsm_plugin_conf}'",
f"sed -i 's/^enabled=0/enabled=1/' '{product_id_plugin_conf}'"
]
--
2.53.0

View File

@ -1,201 +0,0 @@
From 0cfeee9fa113690c7e58877edf65842aad7d7ce5 Mon Sep 17 00:00:00 2001
From: Michal Hecko <mhecko@redhat.com>
Date: Fri, 27 Mar 2026 13:53:49 +0100
Subject: [PATCH 38/44] fix(rhui): consider source clients always signed
Previously, all RHUI clients mentioned in the cloud map in the rhui.py
library were considered as signed by RH. However, configs allow using
clients that are not known to leapp, and, therefore, such clients would
not be considered as signed. Consequently, these packages would not be
removed, causing the upgrade to fail. This patch changes this behavior
so that all source clients mentioned in the RHUIInfo message are
considered as signed.
---
.../filterrpmtransactionevents/actor.py | 12 +-
.../tests/test_filterrpmtransactionevents.py | 120 +++++++++++++++++-
2 files changed, 125 insertions(+), 7 deletions(-)
diff --git a/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py b/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py
index 582a5821..aa735da3 100644
--- a/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py
+++ b/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py
@@ -1,8 +1,11 @@
from leapp.actors import Actor
+from leapp.libraries.common.rpms import has_package
from leapp.models import (
DistributionSignedRPM,
FilteredRpmTransactionTasks,
+ InstalledRPM,
PESRpmTransactionTasks,
+ RHUIInfo,
RpmTransactionTasks
)
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -18,7 +21,7 @@ class FilterRpmTransactionTasks(Actor):
"""
name = 'check_rpm_transaction_events'
- consumes = (PESRpmTransactionTasks, RpmTransactionTasks, DistributionSignedRPM,)
+ consumes = (PESRpmTransactionTasks, RpmTransactionTasks, DistributionSignedRPM, RHUIInfo, InstalledRPM)
produces = (FilteredRpmTransactionTasks,)
tags = (IPUWorkflowTag, ChecksPhaseTag)
@@ -27,6 +30,13 @@ class FilterRpmTransactionTasks(Actor):
for rpm_pkgs in self.consume(DistributionSignedRPM):
installed_pkgs.update([pkg.name for pkg in rpm_pkgs.items])
+ # Consider all RHUI source clients as signed, so that we can always remove them
+ rhui_info = next(self.consume(RHUIInfo), None)
+ if rhui_info:
+ for source_client in rhui_info.src_client_pkg_names:
+ if has_package(InstalledRPM, source_client):
+ installed_pkgs.add(source_client)
+
local_rpms = set()
to_install = set()
to_remove = set()
diff --git a/repos/system_upgrade/common/actors/filterrpmtransactionevents/tests/test_filterrpmtransactionevents.py b/repos/system_upgrade/common/actors/filterrpmtransactionevents/tests/test_filterrpmtransactionevents.py
index 7173805e..a228ed07 100644
--- a/repos/system_upgrade/common/actors/filterrpmtransactionevents/tests/test_filterrpmtransactionevents.py
+++ b/repos/system_upgrade/common/actors/filterrpmtransactionevents/tests/test_filterrpmtransactionevents.py
@@ -1,7 +1,37 @@
-from leapp.models import DistributionSignedRPM, FilteredRpmTransactionTasks, Module, RPM, RpmTransactionTasks
+from leapp.models import (
+ DistributionSignedRPM,
+ FilteredRpmTransactionTasks,
+ InstalledRPM,
+ Module,
+ RHUIInfo,
+ RPM,
+ RpmTransactionTasks,
+ TargetRHUIPostInstallTasks,
+ TargetRHUIPreInstallTasks,
+ TargetRHUISetupInfo
+)
from leapp.snactor.fixture import current_actor_context
RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'
+UNSIGNED_PACKAGER = 'Third Party <third@party.com>'
+
+
+def _make_rpm(name, packager=RH_PACKAGER):
+ return RPM(name=name, version='0.1', release='1.sm01', epoch='1',
+ packager=packager, arch='noarch', pgpsig='SOME_PGP_SIG')
+
+
+def _make_rhui_info(src_clients, target_clients, provider='azure'):
+ setup_info = TargetRHUISetupInfo(
+ preinstall_tasks=TargetRHUIPreInstallTasks(),
+ postinstall_tasks=TargetRHUIPostInstallTasks(),
+ )
+ return RHUIInfo(
+ provider=provider,
+ src_client_pkg_names=src_clients,
+ target_client_pkg_names=target_clients,
+ target_client_setup_info=setup_info,
+ )
def test_actor_execution(current_actor_context):
@@ -10,11 +40,7 @@ def test_actor_execution(current_actor_context):
def test_actor_execution_with_sample_data(current_actor_context):
- installed_rpm = [
- RPM(name='sample01', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
- pgpsig='SOME_PGP_SIG'),
- RPM(name='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
- pgpsig='SOME_PGP_SIG')]
+ installed_rpm = [_make_rpm('sample01'), _make_rpm('sample02')]
modules_to_enable = [Module(name='enable', stream='1'), Module(name='enable', stream='2')]
modules_to_reset = [Module(name='reset', stream='1'), Module(name='reset', stream='2')]
current_actor_context.feed(DistributionSignedRPM(items=installed_rpm))
@@ -43,3 +69,85 @@ def test_actor_execution_with_sample_data(current_actor_context):
assert all(m.name == 'reset' for m in result[0].modules_to_reset)
assert '1' in {m.stream for m in result[0].modules_to_reset}
assert '2' in {m.stream for m in result[0].modules_to_reset}
+
+
+def test_rhui_source_client_treated_as_signed(current_actor_context):
+ """Installed RHUI source clients should be removable even if they are not distribution-signed."""
+ rhui_client_rpm = _make_rpm('rhui-azure-rhel8', packager=UNSIGNED_PACKAGER)
+ signed_rpm = _make_rpm('sample01')
+
+ current_actor_context.feed(DistributionSignedRPM(items=[signed_rpm]))
+ current_actor_context.feed(InstalledRPM(items=[rhui_client_rpm, signed_rpm]))
+ current_actor_context.feed(_make_rhui_info(['rhui-azure-rhel8'], ['rhui-azure-rhel9']))
+ current_actor_context.feed(RpmTransactionTasks(to_remove=['rhui-azure-rhel8']))
+ current_actor_context.run()
+
+ result = current_actor_context.consume(FilteredRpmTransactionTasks)
+ assert len(result) == 1
+ assert 'rhui-azure-rhel8' in result[0].to_remove
+
+
+def test_rhui_source_client_not_installed_is_ignored(current_actor_context):
+ """RHUI source clients that are not installed should not be added to the installed set."""
+ signed_rpm = _make_rpm('sample01')
+
+ current_actor_context.feed(DistributionSignedRPM(items=[signed_rpm]))
+ current_actor_context.feed(InstalledRPM(items=[signed_rpm]))
+ current_actor_context.feed(_make_rhui_info(['rhui-azure-rhel8'], ['rhui-azure-rhel9']))
+ current_actor_context.feed(RpmTransactionTasks(to_remove=['rhui-azure-rhel8']))
+ current_actor_context.run()
+
+ result = current_actor_context.consume(FilteredRpmTransactionTasks)
+ assert len(result) == 1
+ assert 'rhui-azure-rhel8' not in result[0].to_remove
+
+
+def test_no_rhui_info_unsigned_pkg_not_removable(current_actor_context):
+ """Without RHUIInfo, unsigned packages should not be removable (baseline behavior)."""
+ unsigned_rpm = _make_rpm('some-unsigned-pkg', packager=UNSIGNED_PACKAGER)
+ signed_rpm = _make_rpm('sample01')
+
+ current_actor_context.feed(DistributionSignedRPM(items=[signed_rpm]))
+ current_actor_context.feed(InstalledRPM(items=[unsigned_rpm, signed_rpm]))
+ current_actor_context.feed(RpmTransactionTasks(to_remove=['some-unsigned-pkg']))
+ current_actor_context.run()
+
+ result = current_actor_context.consume(FilteredRpmTransactionTasks)
+ assert len(result) == 1
+ assert 'some-unsigned-pkg' not in result[0].to_remove
+
+
+def test_rhui_multiple_source_clients(current_actor_context):
+ """Multiple RHUI source clients should all be treated as signed when installed."""
+ client_rpms = [
+ _make_rpm('rhui-client-a', packager=UNSIGNED_PACKAGER),
+ _make_rpm('rhui-client-b', packager=UNSIGNED_PACKAGER),
+ ]
+
+ current_actor_context.feed(DistributionSignedRPM(items=[]))
+ current_actor_context.feed(InstalledRPM(items=client_rpms))
+ current_actor_context.feed(_make_rhui_info(['rhui-client-a', 'rhui-client-b'], ['rhui-client-target']))
+ current_actor_context.feed(RpmTransactionTasks(
+ to_remove=['rhui-client-a', 'rhui-client-b'],
+ ))
+ current_actor_context.run()
+
+ result = current_actor_context.consume(FilteredRpmTransactionTasks)
+ assert len(result) == 1
+ assert 'rhui-client-a' in result[0].to_remove
+ assert 'rhui-client-b' in result[0].to_remove
+
+
+def test_rhui_source_client_ends_up_in_upgrade_if_not_removed(current_actor_context):
+ """An installed RHUI source client not in to_remove/to_install should be upgraded."""
+ rhui_client_rpm = _make_rpm('rhui-azure-rhel8', packager=UNSIGNED_PACKAGER)
+
+ current_actor_context.feed(DistributionSignedRPM(items=[]))
+ current_actor_context.feed(InstalledRPM(items=[rhui_client_rpm]))
+ current_actor_context.feed(_make_rhui_info(['rhui-azure-rhel8'], ['rhui-azure-rhel9']))
+ current_actor_context.feed(RpmTransactionTasks())
+ current_actor_context.run()
+
+ result = current_actor_context.consume(FilteredRpmTransactionTasks)
+ assert len(result) == 1
+ assert 'rhui-azure-rhel8' in result[0].to_upgrade
--
2.53.0

View File

@ -1,42 +0,0 @@
From f11f7c4c022b990f3cad15eff5149591e747a27b Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Thu, 2 Apr 2026 10:57:55 +0200
Subject: [PATCH 39/44] Ensure that greenboot rpms are removed during IPU
The greenboot packages have a value just for rpm-ostree based systems.
However, if someone install them anyway (e.g. configuration mistake..),
they would damage the upgraded systems - mainly in case IPU 8.10 -> 9.8+.
Systemd fails with error:
```
Failed to isolate the default target.
```
Due to bugs in greenboot scriptlets, that do not count with DNF
execution inside container, nor with the update of greenboot packages
from 0.14.x to 0.16.x and newer.
As there is not value to have these packages on rpm based system
at all, just remove them during the upgrade to stay on a safe side.
---
etc/leapp/transaction/to_remove | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/etc/leapp/transaction/to_remove b/etc/leapp/transaction/to_remove
index 0feb7827..420078f4 100644
--- a/etc/leapp/transaction/to_remove
+++ b/etc/leapp/transaction/to_remove
@@ -1,3 +1,11 @@
### List of packages (each on new line) to be removed from the upgrade transaction
# Removing initial-setup package to avoid it asking for EULA acceptance during upgrade - OAMG-1531
initial-setup
+
+# greenboot packages have a value just for rpm-ostree based systems
+# however, if someone would install them anyway, they would damage
+# the upgraded systems (mainly in case IPU 8.10 -> 9.8+).
+# As there is not value for of these packages on systems upgraded by leapp
+# (rpm based only), let's just remove them to stay safe
+greenboot
+greenboot-default-health-checks
--
2.53.0

View File

@ -1,205 +0,0 @@
From 83540ae7dbd6cb030a024249d6308c56f0d3216d Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Tue, 31 Mar 2026 17:23:39 +0200
Subject: [PATCH 40/44] Regenerate grub config during 8->9 conversions
This is mainly a preventative action to make sure that the grub config
is compatible with the target grub.
The actor only runs grub2-mkconfig when BLS is enabled in
/etc/default/grub. When BLS is disabled, the regeneration is taken care
of by the grub kernel install scripts (in /usr/lib/kernel/install/),
which do call grub2-mkconfig as required.
On 9to10 conversions the kernel install scripts handle regeneration
whether the BLS is enabled or not.
Note that in some cases grub2-mkconfig might be ran twice, as there are
2 more actors that run it (ensurevalidgrubcfghybrid and
grub2mkconfigonppc64). This shouldn't be a problem since the generation
is quick. However a better solution could be designed in the future.
Jira: RHEL-110712
---
.../actors/regenerategrubcfg/actor.py | 23 ++++
.../libraries/regenerategrubcfg.py | 28 +++++
.../tests/test_regenerategrubcfg.py | 102 ++++++++++++++++++
3 files changed, 153 insertions(+)
create mode 100644 repos/system_upgrade/el8toel9/actors/regenerategrubcfg/actor.py
create mode 100644 repos/system_upgrade/el8toel9/actors/regenerategrubcfg/libraries/regenerategrubcfg.py
create mode 100644 repos/system_upgrade/el8toel9/actors/regenerategrubcfg/tests/test_regenerategrubcfg.py
diff --git a/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/actor.py b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/actor.py
new file mode 100644
index 00000000..d87e9c7b
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/actor.py
@@ -0,0 +1,23 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import regenerategrubcfg
+from leapp.models import DefaultGrubInfo, TransactionCompleted
+from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag
+
+
+class RegenerateGrubCfg(Actor):
+ """
+ Regenerate GRUB2 configuration during conversion from EL8 to EL9.
+
+ During distribution conversions (e.g. CentOS to RHEL), the GRUB2
+ configuration may need to be regenerated to ensure compatibility
+ with the target distribution's GRUB2 tooling. This actor runs
+ grub2-mkconfig when BLS is enabled in /etc/default/grub.
+ """
+
+ name = 'regenerate_grub_cfg'
+ consumes = (DefaultGrubInfo, TransactionCompleted)
+ produces = ()
+ tags = (IPUWorkflowTag, ApplicationsPhaseTag)
+
+ def process(self):
+ regenerategrubcfg.process()
diff --git a/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/libraries/regenerategrubcfg.py b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/libraries/regenerategrubcfg.py
new file mode 100644
index 00000000..bedd0897
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/libraries/regenerategrubcfg.py
@@ -0,0 +1,28 @@
+from leapp.libraries.common import grub
+from leapp.libraries.common.config import architecture, is_conversion
+from leapp.libraries.stdlib import api, CalledProcessError, run
+from leapp.models import DefaultGrubInfo
+
+GRUB_CFG_PATH = '/boot/grub2/grub.cfg'
+
+
+def process():
+ if architecture.matches_architecture(architecture.ARCH_S390X):
+ return
+
+ if not is_conversion():
+ return
+
+ default_grub_msg = next(api.consume(DefaultGrubInfo), None)
+ if not default_grub_msg:
+ api.current_logger().warning('No DefaultGrubInfo message, skipping GRUB config regeneration.')
+ return
+
+ if not grub.is_blscfg_enabled_in_defaultgrub(default_grub_msg):
+ return
+
+ api.current_logger().info('Conversion detected with BLS enabled, regenerating GRUB config')
+ try:
+ run(['grub2-mkconfig', '-o', GRUB_CFG_PATH])
+ except CalledProcessError as e:
+ api.current_logger().error('Failed to regenerate GRUB config: {}'.format(e))
diff --git a/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/tests/test_regenerategrubcfg.py b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/tests/test_regenerategrubcfg.py
new file mode 100644
index 00000000..7d8fb6f3
--- /dev/null
+++ b/repos/system_upgrade/el8toel9/actors/regenerategrubcfg/tests/test_regenerategrubcfg.py
@@ -0,0 +1,102 @@
+from unittest import mock
+
+import pytest
+
+from leapp.libraries.actor import regenerategrubcfg
+from leapp.libraries.common.config import architecture
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+from leapp.models import DefaultGrub, DefaultGrubInfo
+
+GRUB2_MKCONFIG_CMD = ['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg']
+
+BLS_ENABLED = DefaultGrubInfo(
+ default_grub_info=[DefaultGrub(name='GRUB_ENABLE_BLSCFG', value='true')]
+)
+BLS_DISABLED = DefaultGrubInfo(
+ default_grub_info=[DefaultGrub(name='GRUB_ENABLE_BLSCFG', value='false')]
+)
+
+
+@pytest.fixture
+def mocked_run():
+ with mock.patch.object(regenerategrubcfg, 'run') as m:
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mocked_logger():
+ with mock.patch.object(api, 'current_logger', logger_mocked()):
+ yield api.current_logger
+
+
+@pytest.fixture
+def mock_actor(monkeypatch):
+
+ def make_mock(msgs, arch=architecture.ARCH_X86_64, is_conversion=False):
+ instance = CurrentActorMocked(
+ msgs=msgs,
+ arch=arch,
+ src_ver="8.10",
+ dst_ver="9.6",
+ )
+ monkeypatch.setattr(api, 'current_actor', instance)
+ # let's use this to cover all conversion paths
+ monkeypatch.setattr(regenerategrubcfg, 'is_conversion', lambda: is_conversion)
+ return instance
+
+ return make_mock
+
+
+def test_conversion_bls_enabled_regenerates(mock_actor, mocked_run):
+ """Conversion with BLS enabled -> regenerate."""
+ mock_actor([BLS_ENABLED], is_conversion=True)
+ regenerategrubcfg.process()
+ mocked_run.assert_called_once_with(GRUB2_MKCONFIG_CMD)
+
+
+def test_conversion_bls_disabled_skips(mock_actor, mocked_run):
+ """Conversion with BLS not enabled -> skip."""
+ mock_actor([BLS_DISABLED], is_conversion=True)
+ regenerategrubcfg.process()
+ mocked_run.assert_not_called()
+
+
+def test_non_conversion_skips(mock_actor, mocked_run):
+ """Non-conversion upgrade -> skip."""
+ mock_actor([BLS_ENABLED])
+ regenerategrubcfg.process()
+ mocked_run.assert_not_called()
+
+
+def test_s390x_skips(mock_actor, mocked_run):
+ """s390x -> skip (uses ZIPL)."""
+ mock_actor([BLS_ENABLED], arch=architecture.ARCH_S390X)
+ regenerategrubcfg.process()
+ mocked_run.assert_not_called()
+
+
+def test_no_default_grub_info_skips(mock_actor, mocked_run, mocked_logger):
+ """No DefaultGrubInfo -> skip."""
+ mock_actor([], is_conversion=True)
+ regenerategrubcfg.process()
+ mocked_run.assert_not_called()
+ assert any(
+ "No DefaultGrubInfo message, skipping GRUB config regeneration." in msg
+ for msg in mocked_logger.warnmsg
+ )
+
+
+def test_failure_nonfatal(mock_actor, mocked_run, mocked_logger):
+ """grub2-mkconfig failure -> non-fatal, logs error."""
+ mocked_run.side_effect = CalledProcessError(
+ message='A Leapp Command Error occurred.',
+ command=GRUB2_MKCONFIG_CMD,
+ result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
+ )
+ mock_actor([BLS_ENABLED], is_conversion=True)
+
+ regenerategrubcfg.process()
+
+ mocked_run.assert_called_once_with(GRUB2_MKCONFIG_CMD)
+ assert any('Failed to regenerate GRUB config' in msg for msg in mocked_logger.errmsg)
--
2.53.0

View File

@ -1,75 +0,0 @@
From 971e5e329290d8e5047ac85c6c85943dfadd5fe6 Mon Sep 17 00:00:00 2001
From: Petr Stodulka <pstodulk@redhat.com>
Date: Tue, 14 Apr 2026 11:25:01 +0200
Subject: [PATCH 41/44] python tests deps: update version requirements per
python
tl;dr; We haven't updated the requirements.txt file for a long time
(basically since IPU 7 -> 8; if we do not count some minor updates).
Let's update dependencies to use newer python modules for newer
versions of python.
* Pytest
Originally we required pytest v6.2.5 for any python 3.6+. This is
pretty old requirement coming from time of python ~ 3.8 and recently
a CVE-2025-71176 occuared, which is fix in pytest v9.0.3.
So let's update ranges to actually allow use of newer pytest on newer
systems:
+----------------+---------------+
| pytest version | python range |
+----------------+---------------+
| 6.2.5 | 3.6.x - 3.7.x |
| 7.4.4 | 3.8.x - 3.9.x |
| 9.0.3 | 3.10+ |
+----------------+---------------+
Note that Pytest 8.x is messing with imports which breaks leapp
import handling unfortunately (race-condition). Seems that these
problems are fixed for Pytest 9 with Python 3.10+. In case we
figure out problems anyway, pytest will need to be executed with
additional parameters to use old import style again. As there is
no high demand for newer pytest on python 3.9, let's stick just
with working pytest versions.
* Pyudev
The original v0.22 is used up to CS 9 (pyton 3.9-). Use 0.24.1 for
python 3.10+ (reflects CS 10 as well)
* Distro
The original v1.5.0 can be used till CS 9 (python 3.9 included) safely
as nowadays. Let's use version 1.9.0 for Python 3.10+
* Drop unsued dependencies (old python that is no longer supported
in the project).
---
requirements.txt | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 3c79b23d..e4294718 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,13 +5,14 @@ isort
funcsigs==1.0.2
mock==2.0.0
pylint
-pytest==4.6.11; python_version < '3.0'
-pytest==6.2.5; python_version >= '3.6'
-pyudev==0.22.0
-distro==1.5.0
+pytest==6.2.5; python_version >= '3.6' and python_version < '3.8'
+pytest==7.4.4; python_version >= '3.8' and python_version < '3.10'
+pytest==9.0.3; python_version >= '3.10'
+pyudev==0.22.0; python_version < '3.10'
+pyudev==0.24.1; python_version >= '3.10'
+distro==1.5.0; python_version < '3.10'
+distro==1.9.0; python_version >= '3.10'
ipaddress==1.0.23
git+https://github.com/oamg/leapp
requests
-# pinning a py27 troublemaking transitive dependency
-lazy-object-proxy==1.5.2; python_version < '3'
rpm
--
2.53.0

View File

@ -1,149 +0,0 @@
From 0f0be8e4ca80721173d289a3db0b170334529c2c Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Wed, 29 Oct 2025 19:31:00 +0100
Subject: [PATCH 42/44] Drop dependency on mock
The mock library is now a part of the Python standard library since
Python 3.3 and is available as the unittest.mock. The 'mock' library is
only a backport which is not needed anymore.
Also remove some unnecessary imports.
---
commands/tests/test_upgrade_paths.py | 2 +-
.../common/actors/biosdevname/tests/test_biosdevname.py | 3 ++-
.../actors/cephvolumescan/tests/test_cephvolumescan.py | 5 +----
.../tests/test_distributionsignedrpmscanner.py | 3 +--
.../actors/ifcfgscanner/tests/unit_test_ifcfgscanner.py | 2 +-
.../common/actors/scanmemory/tests/test_scanmemory.py | 3 ++-
.../tests/component_test_selinuxcontentscanner.py | 4 +---
.../el8toel9/actors/nisscanner/tests/test_nisscan.py | 3 ++-
requirements.txt | 1 -
9 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/commands/tests/test_upgrade_paths.py b/commands/tests/test_upgrade_paths.py
index 773cdf1c..83e8b7f9 100644
--- a/commands/tests/test_upgrade_paths.py
+++ b/commands/tests/test_upgrade_paths.py
@@ -1,7 +1,7 @@
import os
import resource
+import unittest.mock as mock
-import mock
import pytest
from leapp.cli.commands import command_utils
diff --git a/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py b/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
index 3aa8c614..1b11174f 100644
--- a/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
+++ b/repos/system_upgrade/common/actors/biosdevname/tests/test_biosdevname.py
@@ -1,7 +1,8 @@
+from unittest.mock import mock_open, patch
+
import pytest
import pyudev
import six
-from mock import mock_open, patch
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import biosdevname
diff --git a/repos/system_upgrade/common/actors/cephvolumescan/tests/test_cephvolumescan.py b/repos/system_upgrade/common/actors/cephvolumescan/tests/test_cephvolumescan.py
index 168b8fc2..14e1f359 100644
--- a/repos/system_upgrade/common/actors/cephvolumescan/tests/test_cephvolumescan.py
+++ b/repos/system_upgrade/common/actors/cephvolumescan/tests/test_cephvolumescan.py
@@ -1,9 +1,6 @@
-import pytest
-from mock import Mock, patch
+from unittest.mock import patch
from leapp.libraries.actor import cephvolumescan
-from leapp.models import InstalledRPM, LsblkEntry, RPM, StorageInfo
-from leapp.reporting import Report
CONT_PS_COMMAND_OUTPUT = {
"stdout":
diff --git a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/tests/test_distributionsignedrpmscanner.py b/repos/system_upgrade/common/actors/distributionsignedrpmscanner/tests/test_distributionsignedrpmscanner.py
index b0c616cb..e5db0c8f 100644
--- a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/tests/test_distributionsignedrpmscanner.py
+++ b/repos/system_upgrade/common/actors/distributionsignedrpmscanner/tests/test_distributionsignedrpmscanner.py
@@ -1,4 +1,4 @@
-import mock
+import unittest.mock as mock
from leapp.libraries.common import rpms
from leapp.libraries.common.config import mock_configs
@@ -8,7 +8,6 @@ from leapp.models import (
fields,
InstalledRPM,
InstalledUnsignedRPM,
- IPUConfig,
Model,
OSRelease,
RPM,
diff --git a/repos/system_upgrade/common/actors/ifcfgscanner/tests/unit_test_ifcfgscanner.py b/repos/system_upgrade/common/actors/ifcfgscanner/tests/unit_test_ifcfgscanner.py
index d996de84..bef3d485 100644
--- a/repos/system_upgrade/common/actors/ifcfgscanner/tests/unit_test_ifcfgscanner.py
+++ b/repos/system_upgrade/common/actors/ifcfgscanner/tests/unit_test_ifcfgscanner.py
@@ -1,9 +1,9 @@
import errno
import textwrap
+import unittest.mock as mock
from io import StringIO
from os.path import basename
-import mock
import six
from leapp.libraries.actor import ifcfgscanner
diff --git a/repos/system_upgrade/common/actors/scanmemory/tests/test_scanmemory.py b/repos/system_upgrade/common/actors/scanmemory/tests/test_scanmemory.py
index 13e4e724..20fa8e91 100644
--- a/repos/system_upgrade/common/actors/scanmemory/tests/test_scanmemory.py
+++ b/repos/system_upgrade/common/actors/scanmemory/tests/test_scanmemory.py
@@ -1,4 +1,5 @@
-import mock
+import unittest.mock as mock
+
import six
from leapp.libraries.actor import scanmemory
diff --git a/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/tests/component_test_selinuxcontentscanner.py b/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/tests/component_test_selinuxcontentscanner.py
index 802e038a..15aef5d8 100644
--- a/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/tests/component_test_selinuxcontentscanner.py
+++ b/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/tests/component_test_selinuxcontentscanner.py
@@ -4,9 +4,7 @@ import pytest
from leapp.libraries.common.config import mock_configs
from leapp.libraries.stdlib import api, CalledProcessError, run
-from leapp.models import SELinuxCustom, SELinuxFacts, SELinuxModule, SELinuxModules, SELinuxRequestRPMs
-from leapp.reporting import Report
-from leapp.snactor.fixture import current_actor_context
+from leapp.models import SELinuxCustom, SELinuxFacts, SELinuxModules, SELinuxRequestRPMs
# compat module ensures compatibility with newer systems and is not part of testing
TEST_MODULES = [
diff --git a/repos/system_upgrade/el8toel9/actors/nisscanner/tests/test_nisscan.py b/repos/system_upgrade/el8toel9/actors/nisscanner/tests/test_nisscan.py
index ed000ce0..d7e77a28 100644
--- a/repos/system_upgrade/el8toel9/actors/nisscanner/tests/test_nisscan.py
+++ b/repos/system_upgrade/el8toel9/actors/nisscanner/tests/test_nisscan.py
@@ -1,4 +1,5 @@
-import mock
+import unittest.mock as mock
+
import pytest
import six
diff --git a/requirements.txt b/requirements.txt
index e4294718..ee1d9006 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,6 @@
flake8
isort
funcsigs==1.0.2
-mock==2.0.0
pylint
pytest==6.2.5; python_version >= '3.6' and python_version < '3.8'
pytest==7.4.4; python_version >= '3.8' and python_version < '3.10'
--
2.53.0

View File

@ -1,194 +0,0 @@
From d8c82c9460e2ab08ac735c7af06ec4a73a9b4de4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Mo=C4=8D=C3=A1ry?=
<68905580+PeterMocary@users.noreply.github.com>
Date: Thu, 16 Apr 2026 23:00:14 +0200
Subject: [PATCH 43/44] remove single quoting from remediation commands (#1520)
The framework now uses shlex.quote to always make literals out of
remediation command arguments. Some of the existing remediation commands used single
quotes in their subcommands so the resulting command in leapp-report.txt used
unintuitive escaping of single quotes.
The chackrootsymlinks can still end up with a single quote if the user
has it in one of the symlinks that need to be adjusted. However, it is
not possible to handle this differently without introducing side effects.
jira: RHEL-156521
---
packaging/leapp-repository.spec | 2 +-
.../common/actors/checkrootsymlinks/actor.py | 53 +--------------
.../libraries/checkrootsymlinks.py | 65 +++++++++++++++++++
.../libraries/checkyumpluginsenabled.py | 6 +-
4 files changed, 71 insertions(+), 55 deletions(-)
create mode 100644 repos/system_upgrade/common/actors/checkrootsymlinks/libraries/checkrootsymlinks.py
diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
index 3ffafafa..70b1d7d3 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.4, leapp-framework < 7
+Requires: leapp-framework >= 6.5, leapp-framework < 7
# Since we provide sub-commands for the leapp utility, we expect the leapp
# tool to be installed as well.
diff --git a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
index 2e805542..391e5589 100644
--- a/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
+++ b/repos/system_upgrade/common/actors/checkrootsymlinks/actor.py
@@ -1,8 +1,5 @@
-import os
-
-from leapp import reporting
from leapp.actors import Actor
-from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.actor import checkrootsymlinks
from leapp.models import Report, RootDirectory
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
@@ -20,50 +17,4 @@ class CheckRootSymlinks(Actor):
tags = (IPUWorkflowTag, ChecksPhaseTag)
def process(self):
- rootdir = next(self.consume(RootDirectory), None)
- if not rootdir:
- raise StopActorExecutionError('Cannot check root symlinks',
- details={'Problem': 'Did not receive a message with '
- 'root subdirectories'})
- absolute_links = [item for item in rootdir.items if item.target and os.path.isabs(item.target)]
- absolute_links_nonutf = [item for item in rootdir.invalid_items if item.target and os.path.isabs(item.target)]
- if not absolute_links and not absolute_links_nonutf:
- return
-
- report_fields = [
- reporting.Title('Upgrade requires links in root directory to be relative'),
- reporting.Summary(
- 'After rebooting, parts of the upgrade process can fail if symbolic links in / '
- 'point to absolute paths.\n'
- 'Please change these links to relative ones.'
- ),
- reporting.ExternalLink(
- url='https://access.redhat.com/solutions/6989732',
- title='leapp upgrade stops with Inhibitor "Upgrade requires links in root '
- 'directory to be relative"'
- ),
- reporting.Severity(reporting.Severity.HIGH),
- reporting.Groups([reporting.Groups.INHIBITOR])]
-
- # Generate reports about absolute links presence
- rem_commands = []
- if absolute_links:
- commands = []
- for item in absolute_links:
- command = ' '.join(['ln',
- '-snf',
- f"'{os.path.relpath(item.target, '/')}'",
- f"'{os.path.join('/', item.name)}'"])
- commands.append(command)
- rem_commands = [['bash', '-c', '{}'.format(' && '.join(commands))]]
- # Generate reports about non-utf8 absolute links presence
- nonutf_count = len(absolute_links_nonutf)
- if nonutf_count > 0:
- # for non-utf encoded filenames can't provide a remediation command, so will mention this fact in a hint
- rem_hint = ("{} symbolic links point to absolute paths that have non-utf8 encoding and need to be"
- " fixed additionally".format(nonutf_count))
- report_fields.append(reporting.Remediation(hint=rem_hint, commands=rem_commands))
- else:
- report_fields.append(reporting.Remediation(commands=rem_commands))
-
- reporting.create_report(report_fields)
+ checkrootsymlinks.process()
diff --git a/repos/system_upgrade/common/actors/checkrootsymlinks/libraries/checkrootsymlinks.py b/repos/system_upgrade/common/actors/checkrootsymlinks/libraries/checkrootsymlinks.py
new file mode 100644
index 00000000..2c5676bd
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkrootsymlinks/libraries/checkrootsymlinks.py
@@ -0,0 +1,65 @@
+import os
+
+from leapp import reporting
+from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.stdlib import api
+from leapp.models import RootDirectory
+
+
+def _dquote(s):
+ """Double-quote a string for use inside a non-interactive shell script."""
+ escaped = (s.replace('\\', '\\\\')
+ .replace('"', '\\"')
+ .replace('$', '\\$')
+ .replace('`', '\\`'))
+ return '"{}"'.format(escaped)
+
+
+def process():
+ rootdir = next(api.consume(RootDirectory), None)
+ if not rootdir:
+ raise StopActorExecutionError('Cannot check root symlinks',
+ details={'Problem': 'Did not receive a message with '
+ 'root subdirectories'})
+ absolute_links = [item for item in rootdir.items if item.target and os.path.isabs(item.target)]
+ absolute_links_nonutf = [item for item in rootdir.invalid_items if item.target and os.path.isabs(item.target)]
+ if not absolute_links and not absolute_links_nonutf:
+ return
+
+ report_fields = [
+ reporting.Title('Upgrade requires links in root directory to be relative'),
+ reporting.Summary(
+ 'After rebooting, parts of the upgrade process can fail if symbolic links in / '
+ 'point to absolute paths.\n'
+ 'Please change these links to relative ones.'
+ ),
+ reporting.ExternalLink(
+ url='https://access.redhat.com/solutions/6989732',
+ title='leapp upgrade stops with Inhibitor "Upgrade requires links in root '
+ 'directory to be relative"'
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR])]
+
+ # Generate reports about absolute links presence
+ rem_commands = []
+ if absolute_links:
+ commands = []
+ for item in absolute_links:
+ command = ' '.join(['ln',
+ '-snf',
+ _dquote(os.path.relpath(item.target, '/')),
+ _dquote(os.path.join('/', item.name))])
+ commands.append(command)
+ rem_commands = [['bash', '-c', '{}'.format(' && '.join(commands))]]
+ # Generate reports about non-utf8 absolute links presence
+ nonutf_count = len(absolute_links_nonutf)
+ if nonutf_count > 0:
+ # for non-utf encoded filenames can't provide a remediation command, so will mention this fact in a hint
+ rem_hint = ("{} symbolic links point to absolute paths that have non-utf8 encoding and need to be"
+ " fixed additionally".format(nonutf_count))
+ report_fields.append(reporting.Remediation(hint=rem_hint, commands=rem_commands))
+ else:
+ report_fields.append(reporting.Remediation(commands=rem_commands))
+
+ reporting.create_report(report_fields)
diff --git a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
index 869e2a88..5c99a0d9 100644
--- a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
+++ b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
@@ -36,9 +36,9 @@ def check_required_dnf_plugins_enabled(pkg_manager_info):
product_id_plugin_conf = os.path.join(plugin_configs_dir, 'product-id.conf')
remediation_commands = [
- f"sed -i 's/^plugins=0/plugins=1/' '{dnf_conf_path}'",
- f"sed -i 's/^enabled=0/enabled=1/' '{rhsm_plugin_conf}'",
- f"sed -i 's/^enabled=0/enabled=1/' '{product_id_plugin_conf}'"
+ f'sed -i "s/^plugins=0/plugins=1/" "{dnf_conf_path}"',
+ f'sed -i "s/^enabled=0/enabled=1/" "{rhsm_plugin_conf}"',
+ f'sed -i "s/^enabled=0/enabled=1/" "{product_id_plugin_conf}"',
]
reporting.create_report([
--
2.53.0

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -1,133 +0,0 @@
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/108] 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))
- # in case nothing is enabled in the default target, the directory does not exist
+ 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 target, the directory does not exist
try:
- os.mkdir(os.path.join(systemd_dir, 'default.target.wants'))
+ os.mkdir(target_wants_path)
except OSError:
pass
diff --git a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
index 859237a1..749b4b84 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
+++ b/repos/system_upgrade/common/actors/createresumeservice/files/leapp_resume.service
@@ -1,6 +1,6 @@
[Unit]
Description=Temporary Leapp service which resumes execution after reboot
-After=default.target
+After=multi-user.target
DefaultDependencies=no
After=dbus.service
After=network-online.target
diff --git a/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py b/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
index c1cefc37..f2590291 100644
--- a/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
+++ b/repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py
@@ -1,21 +1,18 @@
import os
-import distro
import pytest
@pytest.mark.skipif(os.getuid() != 0, reason='User is not a root')
-@pytest.mark.skipif(
- distro.id() == 'fedora',
- reason='default.target.wants does not exists on Fedora distro',
-)
def test_create_resume_service(current_actor_context):
-
current_actor_context.run()
+ systemd_dir = '/etc/systemd/system'
service_name = 'leapp_resume.service'
- service_path = '/etc/systemd/system/{}'.format(service_name)
- symlink_path = '/etc/systemd/system/default.target.wants/{}'.format(service_name)
+ target_name = 'multi-user.target'
+
+ service_path = os.path.join(systemd_dir, service_name)
+ symlink_path = os.path.join(systemd_dir, '{}.wants'.format(target_name), service_name)
try:
assert os.path.isfile(service_path)
diff --git a/repos/system_upgrade/common/actors/removeresumeservice/actor.py b/repos/system_upgrade/common/actors/removeresumeservice/actor.py
index 59935394..cf22ad07 100644
--- a/repos/system_upgrade/common/actors/removeresumeservice/actor.py
+++ b/repos/system_upgrade/common/actors/removeresumeservice/actor.py
@@ -21,13 +21,16 @@ class RemoveSystemdResumeService(Actor):
tags = (FirstBootPhaseTag.After, IPUWorkflowTag)
def process(self):
+ systemd_dir = '/etc/systemd/system'
service_name = 'leapp_resume.service'
- if os.path.isfile('/etc/systemd/system/{}'.format(service_name)):
+ target_name = 'multi-user.target'
+
+ service_path = os.path.join(systemd_dir, service_name)
+ target_wants_path = os.path.join(systemd_dir, '{}.wants'.format(target_name), service_name)
+
+ if os.path.isfile(service_path):
run(['systemctl', 'disable', service_name])
- paths_to_unlink = [
- '/etc/systemd/system/{}'.format(service_name),
- '/etc/systemd/system/default.target.wants/{}'.format(service_name),
- ]
+ paths_to_unlink = [service_path, target_wants_path]
for path in paths_to_unlink:
try:
os.unlink(path)
--
2.54.0

View File

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

View File

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

View File

@ -1,47 +0,0 @@
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/108] multipathutil: handle the overrides:protocol
subsection
Starting in rhel-8, multipath.conf supports a protocol subection of the
overrides configuration section. Accept this when parsing the config,
and add a test for it.
Jira: RHEL-151509
---
repos/system_upgrade/common/libraries/multipathutil.py | 3 ++-
.../common/libraries/tests/test_multipathutil.py | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/repos/system_upgrade/common/libraries/multipathutil.py b/repos/system_upgrade/common/libraries/multipathutil.py
index cb5c3693..78ed5f13 100644
--- a/repos/system_upgrade/common/libraries/multipathutil.py
+++ b/repos/system_upgrade/common/libraries/multipathutil.py
@@ -7,7 +7,8 @@ _sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
'overrides', 'multipaths')
_subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
- 'devices': 'device', 'multipaths': 'multipath'}
+ 'devices': 'device', 'multipaths': 'multipath',
+ 'overrides': 'protocol'}
def read_config(path):
diff --git a/repos/system_upgrade/common/libraries/tests/test_multipathutil.py b/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
index 3ddfcddf..852f3463 100644
--- a/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
+++ b/repos/system_upgrade/common/libraries/tests/test_multipathutil.py
@@ -38,7 +38,8 @@ def test_section_start():
sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
'overrides', 'multipaths')
subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
- 'devices': 'device', 'multipaths': 'multipath'}
+ 'devices': 'device', 'multipaths': 'multipath',
+ 'overrides': 'protocol'}
for section in sections:
data = lib.LineData(section + ' {', None, False)
assert data.type == data.TYPE_SECTION_START
--
2.54.0

View File

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

View File

@ -1,83 +0,0 @@
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/108] 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

View File

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

View File

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

View File

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

View File

@ -1,227 +0,0 @@
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/108] 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 | 45 ++++++++-----------
.../libraries/mpath_conf_check.py | 41 +++++++----------
.../libraries/mpathconfupdate.py | 31 ++++++-------
.../el9toel10/libraries/mpathfiles.py | 22 +++++++++
4 files changed, 70 insertions(+), 69 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
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(configs)
- 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
+ attrs_to_populate = [
+ ('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE),
+ ('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE),
+ ('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ # Set the attribute value only if it is not configured or equal to default.
+ # Setting mpath_info.X = Y would cause the config at Y to be copied into
+ # target_uspace/Y. However, we want to place Y at the default location due
+ # to 9>10 deprecations and there is no easy way to remove target_uspace/Y
+ # once created.
+ for mpath_info_attr, configured_value, default_value in attrs_to_populate:
+ if (
+ configured_value is None or
+ os.path.normpath(configured_value) == default_value
+ ):
+ setattr(mpath_info, mpath_info_attr, default_value)
def scan_and_emit_multipath_info(default_config_path='/etc/multipath.conf'):
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
index 49aefc5a..8c4dc531 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_conf_check/libraries/mpath_conf_check.py
@@ -1,6 +1,7 @@
import os
from leapp import reporting
+from leapp.libraries.common import mpathfiles
from leapp.reporting import create_report
_DEFAULT_CONFIG_DIR = '/etc/multipath/conf.d'
@@ -145,34 +146,22 @@ def check_configs(facts):
if _default_config_dir_has_conf_files():
_report_config_dir_conflict(config_dir)
- # bindings_file, wwids_file, prkeys_file: last non-None value wins
- bindings_file = None
- wwids_file = None
- prkeys_file = None
- for conf in facts.configs:
- if conf.bindings_file is not None:
- bindings_file = conf.bindings_file
- if conf.wwids_file is not None:
- wwids_file = conf.wwids_file
- if conf.prkeys_file is not None:
- prkeys_file = conf.prkeys_file
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(facts.configs)
file_list = []
- if (
- bindings_file is not None and
- os.path.normpath(bindings_file) != _DEFAULT_BINDINGS_FILE
- ):
- file_list.append(('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE))
- if (
- wwids_file is not None and
- os.path.normpath(wwids_file) != _DEFAULT_WWIDS_FILE
- ):
- file_list.append(('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE))
- if (
- prkeys_file is not None and
- os.path.normpath(prkeys_file) != _DEFAULT_PRKEYS_FILE
- ):
- file_list.append(('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE))
+ files_to_report = [
+ ('bindings_file', bindings_file, _DEFAULT_BINDINGS_FILE),
+ ('wwids_file', wwids_file, _DEFAULT_WWIDS_FILE),
+ ('prkeys_file', prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ for file_name, configured_path, default_path in files_to_report:
+ if (
+ configured_path is not None and
+ os.path.normpath(configured_path) != default_path
+ ):
+ file_list.append((file_name, configured_path, default_path))
+
if file_list:
_report_files(file_list)
diff --git a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
index a2e3bafc..d5618018 100644
--- a/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
+++ b/repos/system_upgrade/el9toel10/actors/multipath/mpath_upgrade_conf_patcher/libraries/mpathconfupdate.py
@@ -1,7 +1,7 @@
import os
import shutil
-from leapp.libraries.common import multipathutil
+from leapp.libraries.common import mpathfiles, multipathutil
from leapp.libraries.stdlib import api
from leapp.models import MultipathConfigUpdatesInfo, UpdatedMultipathConfig
@@ -55,24 +55,21 @@ def _update_config(config):
def _get_file_locations(facts):
- bindings_file = None
- wwids_file = None
- prkeys_file = None
- for conf in facts.configs:
- if conf.bindings_file is not None:
- bindings_file = os.path.normpath(conf.bindings_file)
- if conf.wwids_file is not None:
- wwids_file = os.path.normpath(conf.wwids_file)
- if conf.prkeys_file is not None:
- prkeys_file = os.path.normpath(conf.prkeys_file)
+ bindings_file, wwids_file, prkeys_file = mpathfiles.mpath_file_locations(facts.configs)
file_updates = []
- if bindings_file is not None and bindings_file != _DEFAULT_BINDINGS_FILE:
- file_updates.append((bindings_file, _DEFAULT_BINDINGS_FILE))
- if wwids_file is not None and wwids_file != _DEFAULT_WWIDS_FILE:
- file_updates.append((wwids_file, _DEFAULT_WWIDS_FILE))
- if prkeys_file is not None and prkeys_file != _DEFAULT_PRKEYS_FILE:
- file_updates.append((prkeys_file, _DEFAULT_PRKEYS_FILE))
+ files_to_move = [
+ (bindings_file, _DEFAULT_BINDINGS_FILE),
+ (wwids_file, _DEFAULT_WWIDS_FILE),
+ (prkeys_file, _DEFAULT_PRKEYS_FILE),
+ ]
+
+ for configured_path, default_path in files_to_move:
+ if configured_path is not None:
+ configured_path = os.path.normpath(configured_path)
+ if configured_path != default_path:
+ file_updates.append((configured_path, default_path))
+
return file_updates
diff --git a/repos/system_upgrade/el9toel10/libraries/mpathfiles.py b/repos/system_upgrade/el9toel10/libraries/mpathfiles.py
new file mode 100644
index 00000000..ada276a3
--- /dev/null
+++ b/repos/system_upgrade/el9toel10/libraries/mpathfiles.py
@@ -0,0 +1,22 @@
+def mpath_file_locations(configs):
+ """
+ Returns the configured location of the bindings_file, wwids_file, and
+ prkeys_file multipath files, handling cases where the location is set
+ multiple times (the last set value is used) or not set at all (None is
+ used).
+
+ :param configs: The ordered list of all multipath config files data
+ :type configs: List[MultipathConfig9to10]
+ :return: The locations of bindings_file, wwids_file, and prkeys_file
+ :rtype: Tuple[Optional[str], Optional[str], Optional[str]]
+
+ """
+
+ bindings_file = None
+ wwids_file = None
+ prkeys_file = None
+ for conf in configs:
+ bindings_file = conf.bindings_file or bindings_file
+ wwids_file = conf.wwids_file or wwids_file
+ prkeys_file = conf.prkeys_file or prkeys_file
+ return (bindings_file, wwids_file, prkeys_file)
--
2.54.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
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/108] 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

View File

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

View File

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

View File

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

View File

@ -1,111 +0,0 @@
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/108] 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 | 57 +++++++------------
.../common/libraries/dnfplugin.py | 3 +-
2 files changed, 21 insertions(+), 39 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),
- }
-
- def __init__(self): # noqa: W0231; pylint: disable=super-init-not-called
- self.data = ""
-
- 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/'
+_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 install(target_basedir):
"""
Installs our plugin to the DNF plugins.
"""
+ # NOTE(pstodulk): Keeping KeyError unhandled as this can happen only
+ # for the new major upgrade path.
+ tgt_plugin_path = os.path.join(
+ target_basedir,
+ _DNF_PLUGIN_PATHS[get_target_major_version()].lstrip('/')
+ )
try:
- shutil.copy2(
- api.get_file_path(DNF_PLUGIN_NAME),
- os.path.join(target_basedir, DNF_PLUGIN_PATH.lstrip('/')))
+ shutil.copy2(api.get_file_path(DNF_PLUGIN_NAME), tgt_plugin_path)
except EnvironmentError as e:
api.current_logger().debug('Failed to install DNF plugin', exc_info=True)
raise StopActorExecutionError(
diff --git a/repos/system_upgrade/common/libraries/dnfplugin.py b/repos/system_upgrade/common/libraries/dnfplugin.py
index 713cc34b..19e4d4e2 100644
--- a/repos/system_upgrade/common/libraries/dnfplugin.py
+++ b/repos/system_upgrade/common/libraries/dnfplugin.py
@@ -10,9 +10,8 @@ This shim will be removed in a future version. Please update imports to:
from leapp.libraries.common.dnflibs import dnfplugin as _dnfplugin
from leapp.utils.deprecation import deprecated
-# Re-export constants (not deprecated)
+# Re-export constants
DNF_PLUGIN_NAME = _dnfplugin.DNF_PLUGIN_NAME
-DNF_PLUGIN_PATH = _dnfplugin.DNF_PLUGIN_PATH
DNF_PLUGIN_DATA_NAME = _dnfplugin.DNF_PLUGIN_DATA_NAME
DNF_PLUGIN_DATA_PATH = _dnfplugin.DNF_PLUGIN_DATA_PATH
DNF_PLUGIN_DATA_LOG_PATH = _dnfplugin.DNF_PLUGIN_DATA_LOG_PATH
--
2.54.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,439 +0,0 @@
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/108] 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 | 68 ++++++++++++++--
.../systemfacts/libraries/systemfacts.py | 20 +++++
.../tests/test_systemfacts_selinux.py | 61 +++++++++++++-
.../common/models/selinuxfacts.py | 4 +
6 files changed, 234 insertions(+), 14 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
+from leapp.models import (
+ KernelCmdline,
+ KernelCmdlineArg,
+ SELinuxFacts,
+ SelinuxPermissiveDecision,
+ SelinuxRelabelDecision,
+ TargetKernelCmdlineArgTasks,
+ UpgradeKernelCmdlineArgTasks
+)
-def create_selinuxfacts(static_mode, enabled, policy='targeted', mls_enabled=True):
+def create_selinuxfacts(
+ static_mode,
+ enabled,
+ policy='targeted',
+ mls_enabled=True,
+ enforcing_via_any_cmdline=False,
+):
runtime_mode = static_mode if static_mode != 'disabled' else None
return SELinuxFacts(
@@ -16,7 +29,8 @@ def create_selinuxfacts(static_mode, enabled, policy='targeted', mls_enabled=Tru
static_mode=static_mode,
enabled=enabled,
policy=policy,
- mls_enabled=mls_enabled
+ mls_enabled=mls_enabled,
+ enforcing_via_any_cmdline=enforcing_via_any_cmdline,
)
@@ -25,7 +39,7 @@ def test_actor_schedule_relabelling(monkeypatch, mode):
fact = create_selinuxfacts(static_mode=mode, enabled=True)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, KernelCmdline(parameters=[])]))
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -38,7 +52,7 @@ def test_actor_schedule_relabelling(monkeypatch, mode):
def test_actor_set_permissive(monkeypatch):
relabel = create_selinuxfacts(static_mode='enforcing', enabled=True)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[relabel]))
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[relabel, KernelCmdline(parameters=[])]))
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -56,7 +70,9 @@ def test_actor_selinux_disabled(monkeypatch, el8_to_el9):
target_ver = '8' if not el8_to_el9 else '9'
monkeypatch.setattr(checkselinux, 'get_target_major_version', lambda: target_ver)
- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[disabled]))
+ monkeypatch.setattr(
+ api, 'current_actor', CurrentActorMocked(msgs=[disabled, KernelCmdline(parameters=[])])
+ )
monkeypatch.setattr(api, 'produce', produce_mocked())
monkeypatch.setattr(reporting, "create_report", create_report_mocked())
@@ -67,3 +83,41 @@ def test_actor_selinux_disabled(monkeypatch, el8_to_el9):
else:
assert not api.produce.model_instances
assert reporting.create_report.called
+
+
+def test_enforcing_one_in_proc_cmdline_schedules_removal(monkeypatch):
+ fact = create_selinuxfacts(static_mode='permissive', enabled=True)
+ kcmd = KernelCmdline(parameters=[KernelCmdlineArg(key='enforcing', value='1')])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, kcmd]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ checkselinux.process()
+
+ upgrade_produced = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert upgrade_produced
+ assert upgrade_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+ target_produced = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert target_produced
+ assert target_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+
+def test_enforcing_one_only_in_bootloader_schedules_removal(monkeypatch):
+ fact = create_selinuxfacts(
+ static_mode='permissive', enabled=True, enforcing_via_any_cmdline=True
+ )
+ kcmd = KernelCmdline(parameters=[KernelCmdlineArg(key='ro')])
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact, kcmd]))
+ monkeypatch.setattr(api, 'produce', produce_mocked())
+ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
+
+ checkselinux.process()
+
+ upgrade_produced = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)]
+ assert upgrade_produced
+ assert upgrade_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
+
+ target_produced = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)]
+ assert target_produced
+ assert target_produced[0].to_remove == [KernelCmdlineArg(key='enforcing', value='1')]
diff --git a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
index ba7bdb82..578f96cb 100644
--- a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
+++ b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
@@ -230,6 +230,24 @@ def get_repositories_status():
})
+def _bootloader_entries_contain_enforcing_one():
+ """
+ True if grubby reports enforcing=1 in any boot loader entry's kernel arguments.
+ """
+ try:
+ out = run(['/usr/sbin/grubby', '--info', 'ALL'], split=False)['stdout']
+ except (OSError, CalledProcessError):
+ api.current_logger().debug(
+ 'grubby --info ALL failed; assuming no bootloader enforcing=1', exc_info=True
+ )
+ return False
+
+ for line in out.splitlines():
+ if line.startswith('args=') and 'enforcing=1' in line[len('args='):].strip('"').split():
+ return True
+ return False
+
+
def get_selinux_status():
""" Get SELinux status information """
# will be None if something went wrong or contain SELinuxFacts otherwise
@@ -259,6 +277,8 @@ def get_selinux_status():
outdata['static_mode'] = 'disabled'
outdata['policy'] = 'targeted'
+ outdata['enforcing_via_any_cmdline'] = _bootloader_entries_contain_enforcing_one()
+
res = SELinuxFacts(**outdata)
return res
diff --git a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
index d36900bd..85a3ff3d 100644
--- a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
+++ b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py
@@ -2,6 +2,7 @@ import warnings
import pytest
+from leapp.libraries.actor import systemfacts
from leapp.libraries.actor.systemfacts import get_selinux_status
from leapp.models import SELinuxFacts
@@ -20,6 +21,23 @@ reason_to_skip_msg = "Selinux is not available"
# FIXME: create valid tests...
+def _run_result(stdout):
+ return {'stdout': stdout, 'stderr': '', 'signal': None, 'exit_code': 0, 'pid': 1}
+
+
+@pytest.fixture(autouse=True)
+def stub_grubby_info_all_without_enforcing(monkeypatch):
+ """
+ Avoid invoking real grubby from get_selinux_status(); default: no enforcing=1 in boot entries.
+ """
+
+ def mocked_run(cmd, split=False, **dummy_kwargs):
+ assert cmd == ['/usr/sbin/grubby', '--info', 'ALL'], f"Unexpected grubby cmd: {cmd}"
+ return _run_result('index=0\nkernel="/boot/vmlinuz-1"\nargs="ro rhgb quiet"\nroot="UUID=xxx"\n')
+
+ monkeypatch.setattr(systemfacts, 'run', mocked_run)
+
+
@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_enabled_enforcing(monkeypatch):
"""
@@ -34,7 +52,8 @@ def test_selinux_enabled_enforcing(monkeypatch):
'mls_enabled': True,
'enabled': True,
'runtime_mode': 'enforcing',
- 'static_mode': 'enforcing'}
+ 'static_mode': 'enforcing',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -52,7 +71,8 @@ def test_selinux_enabled_permissive(monkeypatch):
'mls_enabled': True,
'enabled': True,
'runtime_mode': 'permissive',
- 'static_mode': 'permissive'}
+ 'static_mode': 'permissive',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -70,7 +90,8 @@ def test_selinux_disabled(monkeypatch):
'mls_enabled': False,
'enabled': False,
'runtime_mode': 'permissive',
- 'static_mode': 'permissive'}
+ 'static_mode': 'permissive',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
@@ -93,6 +114,38 @@ def test_selinux_disabled_no_config_file(monkeypatch):
'mls_enabled': False,
'enabled': False,
'runtime_mode': 'permissive',
- 'static_mode': 'disabled'}
+ 'static_mode': 'disabled',
+ 'enforcing_via_any_cmdline': False}
assert SELinuxFacts(**expected_data) == get_selinux_status()
+
+
+@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
+@pytest.mark.parametrize('grubby_stdout,expected', [
+ ('index=0\nargs="ro enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=1 rhgb quiet"\n', True),
+ ('index=0\nargs="enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=1"\nindex=1\nargs="ro rhgb quiet"\n', True),
+ ('index=0\nargs="ro rhgb quiet"\nindex=1\nargs="ro enforcing=1"\n', True),
+ ('index=0\nargs="ro enforcing=0"\n', False),
+ ('index=0\nargs="ro rhgb quiet"\n', False),
+ ('index=0\nargs="ro fooenforcing=1"\n', False),
+ ('index=0\nargs="ro enforcing=1bar"\n', False),
+ ('index=0\nargs="ro"\n', False),
+ ('index=0\nargs=""\n', False),
+])
+def test_bootloader_enforcing_one_detected(monkeypatch, grubby_stdout, expected):
+ monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 1)
+ monkeypatch.setattr(selinux, 'security_getenforce', lambda: 1)
+ monkeypatch.setattr(selinux, 'selinux_getenforcemode', lambda: [0, 1])
+ monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 1)
+ monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
+
+ def mocked_run(cmd, split=False, **dummy_kwargs):
+ assert cmd == ['/usr/sbin/grubby', '--info', 'ALL'], f"Unexpected grubby cmd: {cmd}"
+ return _run_result(grubby_stdout)
+
+ monkeypatch.setattr(systemfacts, 'run', mocked_run)
+
+ fact = get_selinux_status()
+ assert fact.enforcing_via_any_cmdline is expected
diff --git a/repos/system_upgrade/common/models/selinuxfacts.py b/repos/system_upgrade/common/models/selinuxfacts.py
index 12b0cf8a..9376e4b1 100644
--- a/repos/system_upgrade/common/models/selinuxfacts.py
+++ b/repos/system_upgrade/common/models/selinuxfacts.py
@@ -10,3 +10,7 @@ class SELinuxFacts(Model):
enabled = fields.Boolean()
policy = fields.StringEnum(['targeted', 'minimum', 'mls'])
mls_enabled = fields.Boolean()
+ enforcing_via_any_cmdline = fields.Boolean(default=False)
+ """
+ True when any bootloader entries have set enforcing=1.
+ """
--
2.54.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +0,0 @@
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/108] checkvarrunsymlink: Log warning when /var/run is
missing from tracked files
Address review feedback from #1546: emit a warning when /var/run
is unexpectedly absent from TrackedFilesInfoSource instead of
silently skipping the check.
---
.../checkvarrunsymlink/libraries/checkvarrunsymlink.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
index e2b198c1..4e6f030e 100644
--- a/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
+++ b/repos/system_upgrade/common/actors/checkvarrunsymlink/libraries/checkvarrunsymlink.py
@@ -21,7 +21,12 @@ def process():
return
real_path = _get_real_path(tracked_files)
- if real_path is None or real_path == '/run':
+ if real_path is None:
+ api.current_logger().warning(
+ '/var/run not found in TrackedFilesInfoSource. Skipping the symlink check.'
+ )
+ return
+ if real_path == '/run':
return
reporting.create_report([
--
2.54.0

View File

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

View File

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

View File

@ -1,26 +0,0 @@
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/108] Bump leapp-framework
I forgot to bump it here and also in the framework with b70ff3.
---
packaging/leapp-repository.spec | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
index 70b1d7d3..627fd11a 100644
--- a/packaging/leapp-repository.spec
+++ b/packaging/leapp-repository.spec
@@ -120,7 +120,7 @@ Requires: leapp-repository-dependencies = %{leapp_repo_deps}
# IMPORTANT: this is capability provided by the leapp framework rpm.
# Check that 'version' instead of the real framework rpm version.
-Requires: leapp-framework >= 6.5, leapp-framework < 7
+Requires: leapp-framework >= 6.6, leapp-framework < 7
# Since we provide sub-commands for the leapp utility, we expect the leapp
# tool to be installed as well.
--
2.54.0

View File

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

View File

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

View File

@ -1,53 +0,0 @@
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/108] Update actions/checkout action to v7
---
.github/workflows/codespell.yml | 2 +-
.github/workflows/differential-shellcheck.yml | 2 +-
.github/workflows/unit-tests.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 2ac322e5..03a62d51 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: codespell-project/actions-codespell@v2
with:
ignore_words_list: ro,fo,couldn,repositor,zeor,bootup
diff --git a/.github/workflows/differential-shellcheck.yml b/.github/workflows/differential-shellcheck.yml
index 3b92d771..fc29bf30 100644
--- a/.github/workflows/differential-shellcheck.yml
+++ b/.github/workflows/differential-shellcheck.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Repository checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index f855baa2..bd829936 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -52,7 +52,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
# NOTE(ivasilev) fetch-depth 0 is critical here as leapp deps discovery depends on specific substring in
# commit message and default 1 option will get us just merge commit which has an unrelevant message.
--
2.54.0

View File

@ -1,115 +0,0 @@
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/108] 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

Some files were not shown because too many files have changed in this diff Show More