diff --git a/SOURCES/leapp-repository-0.25.0-elevate.patch b/SOURCES/leapp-repository-0.25.0-elevate.patch
index 77bda0d..8c650f5 100644
--- a/SOURCES/leapp-repository-0.25.0-elevate.patch
+++ b/SOURCES/leapp-repository-0.25.0-elevate.patch
@@ -3515,6 +3515,56 @@ index 00000000..370758e6
+ end
+ end
+end
+diff --git a/docs/source/libraries-and-api/deprecations-list.md b/docs/source/libraries-and-api/deprecations-list.md
+index cf45a5d0..6141a55c 100644
+--- a/docs/source/libraries-and-api/deprecations-list.md
++++ b/docs/source/libraries-and-api/deprecations-list.md
+@@ -13,7 +13,9 @@ 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 (till TODO date)
+-- Nothing deprecated yet
++- Models:
++- **`rhel_version`** field in the `TargetOSInstallationImage` model is replaced by the
++`os_version` field.
+
+ ## v0.25.0 (till January 2027)
+ - Environment variables
+diff --git a/docs/source/tutorials/custom-content.md b/docs/source/tutorials/custom-content.md
+index 40310bc7..02170502 100644
+--- a/docs/source/tutorials/custom-content.md
++++ b/docs/source/tutorials/custom-content.md
+@@ -72,18 +72,24 @@ git repo and register related leapp repositories using `snactor` manually. See
+ only there.
+ ```
+
+-5. Create a symlink in `/etc/leapp/repos.d/` to register your repository for leapp:
++5. If you do not find an `/etc/leapp/repos.d/custom-repositories` symlink on
++ the system, create it manually:
+ ```shell
+- ln -s /usr/share/leapp-repository/custom-repositories/ /etc/leapp/repos.d/
++ ln -s /usr/share/leapp-repository/custom-repositories /etc/leapp/repos.d/custom-repositories
+ ```
+
++ This symlink ensures that any leapp repository created under
++ `/usr/share/leapp-repository/custom-repositories/` is discovered by leapp
++ without any additional steps.
++
+ ```{note}
+- This step is required to be done just on the system where the custom repository
+- is installed so leapp will discover the repository.
++ The symlink is nowadays part of upstream development packages and will be
++ part of `leapp-upgrade-*` RPM packages of v0.26.0 and newer.
+ ```
+
+-Note that such a created leapp repository can be installed to other systems as it is,
+-just the symlink needs to be always created as well (step `5`).
++Note that such a created leapp repository can be installed to other systems as it is.
++Ensure the target system has the symlink (step 5) and the repository is placed under
++`/usr/share/leapp-repository/custom-repositories/`.
+
+ ## Create custom actor
+
diff --git a/etc/leapp/transaction/to_reinstall b/etc/leapp/transaction/to_reinstall
new file mode 100644
index 00000000..c6694a8e
@@ -3524,6 +3574,22 @@ index 00000000..c6694a8e
+### List of packages (each on new line) to be reinstalled to the upgrade transaction
+### Useful for packages that have identical version strings but contain binary changes between major OS versions
+### Packages that aren't installed will be skipped
+diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
+index 5191c8f9..3dbc517a 100644
+--- a/packaging/leapp-repository.spec
++++ b/packaging/leapp-repository.spec
+@@ -299,6 +299,11 @@ do
+ ln -s %{repositorydir}/$REPOSITORY %{buildroot}%{_sysconfdir}/leapp/repos.d/$REPOSITORY
+ done;
+
++# Enable custom repository discovery - any leapp repository created under
++# custom-repositories/ will be automatically discovered by leapp without
++# requiring manual symlink creation.
++ln -s %{custom_repositorydir} %{buildroot}%{_sysconfdir}/leapp/repos.d/custom-repositories
++
+ # __python2 could be problematic on systems with Python3 only, but we have
+ # no choice as __python became error on F33+:
+ # https://fedoraproject.org/wiki/Changes/PythonMacroError
diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
index 8fb2e404..99382cd5 100644
--- a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py
@@ -3632,6 +3698,213 @@ index 09632c0c..4df099bc 100644
+ assert undesired == {('rd.lvm.lv', ('vg00/lv_root', 'vg00/lv_swap'))}
+ assert 'rd.lvm.lv=vg00/lv_root rd.lvm.lv=vg00/lv_swap' == \
+ addupgradebootentry.format_grubby_args_from_args_set(undesired)
+diff --git a/repos/system_upgrade/common/actors/checkcustommodifications/libraries/checkcustommodifications.py b/repos/system_upgrade/common/actors/checkcustommodifications/libraries/checkcustommodifications.py
+index f1744531..4d1046be 100644
+--- a/repos/system_upgrade/common/actors/checkcustommodifications/libraries/checkcustommodifications.py
++++ b/repos/system_upgrade/common/actors/checkcustommodifications/libraries/checkcustommodifications.py
+@@ -1,25 +1,17 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import CustomModifications
+
+-FMT_LIST_SEPARATOR = "\n - "
+-
+
+ def _pretty_files(messages):
+ """
+ Return formatted string of discovered files from obtained CustomModifications messages.
+ """
+- flist = []
++ items = []
+ for msg in messages:
+ actor = ' (Actor: {})'.format(msg.actor_name) if msg.actor_name else ''
+- flist.append(
+- '{sep}{filename}{actor}'.format(
+- sep=FMT_LIST_SEPARATOR,
+- filename=msg.filename,
+- actor=actor
+- )
+- )
+- return ''.join(flist)
++ items.append('{}{}'.format(msg.filename, actor))
++ return format_list(items)
+
+
+ def _is_modified_config(msg):
+diff --git a/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py b/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
+index 22a39c29..10d46335 100644
+--- a/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
++++ b/repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py
+@@ -4,7 +4,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.libraries.stdlib import api, format_list
+ from leapp.models import DetectedDeviceOrDriver
+
+
+@@ -29,10 +29,9 @@ def create_inhibitors(inhibiting_entries):
+ reporting.Summary(
+ (
+ 'Support for the following {source_distro} {source_version} device drivers has'
+- ' been removed in {target_distro} {target_version}:\n'
+- ' - {drivers}\n'
++ ' been removed in {target_distro} {target_version}:{drivers}'
+ ).format(
+- drivers='\n - '.join([entry.driver_name for entry in drivers]),
++ drivers=format_list([entry.driver_name for entry in drivers]),
+ target_version=get_target_major_version(),
+ source_version=get_source_major_version(),
+ **DISTRO_REPORT_NAMES
+@@ -68,11 +67,10 @@ def create_inhibitors(inhibiting_entries):
+ ),
+ reporting.Summary(
+ (
+- 'Support for the following devices has been removed in {target_distro} {version}:\n'
+- ' - {devices}\n'
++ 'Support for the following devices has been removed in {target_distro} {version}:{devices}'
+ ).format(
+- devices='\n - '.join(['{name} ({pci})'.format(name=entry.device_name,
+- pci=entry.device_id) for entry in devices]),
++ devices=format_list(['{name} ({pci})'.format(name=entry.device_name,
++ pci=entry.device_id) for entry in devices]),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
+ )
+@@ -93,10 +91,9 @@ def create_inhibitors(inhibiting_entries):
+ ),
+ reporting.Summary(
+ (
+- 'Support for the following processors has been removed in {target_distro} {version}:\n'
+- ' - {processors}\n'
++ 'Support for the following processors has been removed in {target_distro} {version}:{processors}'
+ ).format(
+- processors='\n - '.join([entry.device_name for entry in cpus]),
++ processors=format_list([entry.device_name for entry in cpus]),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
+ )
+@@ -122,10 +119,9 @@ def create_warnings(unmaintained_entries):
+ reporting.Summary(
+ (
+ 'The following {source_distro} {source_version} device drivers are no longer'
+- ' maintained {target_distro} {target_version}:\n'
+- ' - {drivers}\n'
++ ' maintained {target_distro} {target_version}:{drivers}'
+ ).format(
+- drivers='\n - '.join([entry.driver_name for entry in drivers]),
++ drivers=format_list([entry.driver_name for entry in drivers]),
+ target_version=get_target_major_version(),
+ source_version=get_source_major_version(),
+ **DISTRO_REPORT_NAMES
+@@ -146,10 +142,10 @@ def create_warnings(unmaintained_entries):
+ reporting.Summary(
+ (
+ 'The support for the following devices has been removed in {target_distro} {version} and '
+- 'are no longer maintained:\n - {devices}\n'
++ 'are no longer maintained:{devices}'
+ ).format(
+- devices='\n - '.join(['{name} ({pci})'.format(name=entry.device_name,
+- pci=entry.device_id) for entry in devices]),
++ devices=format_list(['{name} ({pci})'.format(name=entry.device_name,
++ pci=entry.device_id) for entry in devices]),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
+ )
+@@ -168,10 +164,9 @@ def create_warnings(unmaintained_entries):
+ ),
+ reporting.Summary(
+ (
+- 'The following processors are no longer maintained in {target_distro} {version}:\n'
+- ' - {processors}\n'
++ 'The following processors are no longer maintained in {target_distro} {version}:{processors}'
+ ).format(
+- processors='\n - '.join([entry.device_name for entry in cpus]),
++ processors=format_list([entry.device_name for entry in cpus]),
+ version=get_target_major_version(),
+ **DISTRO_REPORT_NAMES
+ )
+diff --git a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py
+index 9ead892e..8ff937fc 100644
+--- a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py
++++ b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py
+@@ -1,13 +1,11 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import DynamicLinkerConfiguration
+
+ LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
+ LD_SO_CONF_MAIN = '/etc/ld.so.conf'
+ LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH'
+ LD_PRELOAD_VAR = 'LD_PRELOAD'
+-FMT_LIST_SEPARATOR_1 = '\n- '
+-FMT_LIST_SEPARATOR_2 = '\n - '
+
+
+ def _report_custom_dynamic_linker_configuration(summary):
+@@ -19,6 +17,11 @@ def _report_custom_dynamic_linker_configuration(summary):
+ reporting.Remediation(hint=('Remove or revert the custom dynamic linker configurations and apply the changes '
+ 'using the ldconfig command. In case of possible active software collections we '
+ 'suggest disabling them persistently.')),
++ reporting.ExternalLink(
++ url='https://access.redhat.com/solutions/7130046',
++ title='Leapp upgrade command returns Error: No handle specified'
++ ' with Message: Unable to install RHEL X userspace packages.'
++ ),
+ reporting.RelatedResource('file', '/etc/ld.so.conf'),
+ reporting.RelatedResource('directory', '/etc/ld.so.conf.d'),
+ reporting.Severity(reporting.Severity.HIGH),
+@@ -34,9 +37,9 @@ def check_dynamic_linker_configuration():
+ custom_configurations = ''
+ if configuration.main_config.modified:
+ custom_configurations += (
+- '{}The {} file has unexpected contents:{}{}'
+- .format(FMT_LIST_SEPARATOR_1, LD_SO_CONF_MAIN,
+- FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(configuration.main_config.modified_lines))
++ '\n- The {} file has unexpected contents:{}'
++ .format(LD_SO_CONF_MAIN,
++ format_list(configuration.main_config.modified_lines, callback_sort=None))
+ )
+
+ custom_configs = []
+@@ -46,15 +49,14 @@ def check_dynamic_linker_configuration():
+
+ if custom_configs:
+ custom_configurations += (
+- '{}The following drop in config files were marked as custom:{}{}'
+- .format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(custom_configs))
++ '\n- The following drop in config files were marked as custom:{}'
++ .format(format_list(custom_configs))
+ )
+
+ if configuration.used_variables:
+ custom_configurations += (
+- '{}The following variables contain unexpected dynamic linker configuration:{}{}'
+- .format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2,
+- FMT_LIST_SEPARATOR_2.join(configuration.used_variables))
++ '\n- The following variables contain unexpected dynamic linker configuration:{}'
++ .format(format_list(configuration.used_variables))
+ )
+
+ if custom_configurations:
+diff --git a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py
+index d640f0c5..428517bc 100644
+--- a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py
++++ b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py
+@@ -47,6 +47,10 @@ def test_check_ld_so_configuration(monkeypatch, included_configs_modifications,
+
+ assert reporting.create_report.called == 1
+ assert 'configuration for dynamic linker' in reporting.create_report.reports[0]['title']
++ external_links = reporting.create_report.reports[0].get('detail', {}).get('external', [])
++ assert any(
++ 'https://access.redhat.com/solutions/7130046' in link.get('url', '') for link in external_links
++ )
+ summary = reporting.create_report.reports[0]['summary']
+
+ if any(included_configs_modifications):
diff --git a/repos/system_upgrade/common/actors/checkenabledvendorrepos/actor.py b/repos/system_upgrade/common/actors/checkenabledvendorrepos/actor.py
new file mode 100644
index 00000000..52f5af9d
@@ -3691,6 +3964,82 @@ index 00000000..52f5af9d
+ api.produce(ActiveVendorList(data=list(active_vendors)))
+ else:
+ self.log.info("No active vendors found, vendor list not generated")
+diff --git a/repos/system_upgrade/common/actors/checkfstabmountorder/libraries/checkfstabmountorder.py b/repos/system_upgrade/common/actors/checkfstabmountorder/libraries/checkfstabmountorder.py
+index be9b5e82..3e61109e 100644
+--- a/repos/system_upgrade/common/actors/checkfstabmountorder/libraries/checkfstabmountorder.py
++++ b/repos/system_upgrade/common/actors/checkfstabmountorder/libraries/checkfstabmountorder.py
+@@ -1,11 +1,9 @@
+ import os
+
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import StorageInfo
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _get_common_path(path1, path2):
+ """
+@@ -78,8 +76,8 @@ def check_fstab_mount_order():
+ summary += '\nDetected order of overshadowing mount points: {}'.format(', '.join(overshadowing_in_order))
+ hint += (
+ ' Reorder the detected overshadowing entries. Possible order of all mount '
+- 'points without overshadowing:{}{}'
+- ).format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(overshadowing_fixed))
++ 'points without overshadowing:{}'
++ ).format(format_list(overshadowing_fixed, callback_sort=None))
+
+ reporting.create_report([
+ reporting.Title(
+diff --git a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
+index dee1f995..c7bb218c 100644
+--- a/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
++++ b/repos/system_upgrade/common/actors/checkluks/libraries/checkluks.py
+@@ -1,7 +1,7 @@
+ from leapp import reporting
+ 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.libraries.stdlib import api, format_list
+ from leapp.models import (
+ CephInfo,
+ CopyFile,
+@@ -22,12 +22,6 @@ from leapp.reporting import create_report
+ CLEVIS_DOC_URL_FMT = 'https://red.ht/clevis-tpm2-luks-auto-unlock-rhel{}'
+ LUKS2_CONVERT_DOC_URL_FMT = 'https://red.ht/convert-to-luks2-rhel{}'
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+-
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in input_list]
+-
+
+ def _at_least_one_tpm_token(luks_dump):
+ return any(token.token_type == "clevis-tpm2" for token in luks_dump.tokens)
+@@ -59,7 +53,7 @@ def report_inhibitor(luks1_partitions, no_tpm2_partitions):
+ ' it has some limitations in comparison to LUKS2.'
+ ' Only the LUKS2 format is supported for upgrades.'
+ ' The following LUKS1 partitions have been discovered on your system:{partitions}'
+- .format(**DISTRO_REPORT_NAMES, partitions=''.join(_formatted_list_output(luks1_partitions)))
++ .format(**DISTRO_REPORT_NAMES, partitions=format_list(luks1_partitions))
+ )
+ report_hints.append(reporting.Remediation(
+ hint=(
+@@ -80,9 +74,9 @@ def report_inhibitor(luks1_partitions, no_tpm2_partitions):
+ ' encrypted devices during the upgrade process.'
+ ' Currently we support automatic unlocking during the upgrade only'
+ ' for volumes bound to Clevis TPM2 token.'
+- ' The following LUKS2 devices without Clevis TPM2 token '
+- ' have been discovered on your system: {}'
+- .format(''.join(_formatted_list_output(no_tpm2_partitions)))
++ ' The following LUKS2 devices without Clevis TPM2 token'
++ ' have been discovered on your system:{}'
++ .format(format_list(no_tpm2_partitions))
+ )
+
+ report_hints.append(reporting.Remediation(
diff --git a/repos/system_upgrade/common/actors/checkmicroarchitecture/actor.py b/repos/system_upgrade/common/actors/checkmicroarchitecture/actor.py
index bb342f2f..6c11a244 100644
--- a/repos/system_upgrade/common/actors/checkmicroarchitecture/actor.py
@@ -3824,6 +4173,532 @@ index eeca8be0..949a731f 100644
checkmicroarchitecture.process()
+diff --git a/repos/system_upgrade/common/actors/checknvme/libraries/checknvme.py b/repos/system_upgrade/common/actors/checknvme/libraries/checknvme.py
+index cce11f43..36e503c5 100644
+--- a/repos/system_upgrade/common/actors/checknvme/libraries/checknvme.py
++++ b/repos/system_upgrade/common/actors/checknvme/libraries/checknvme.py
+@@ -5,7 +5,7 @@ from typing import List
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.config.version import get_source_major_version
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import (
+ CopyFile,
+ DracutModule,
+@@ -22,7 +22,6 @@ from leapp.models import (
+ UpgradeKernelCmdlineArgTasks
+ )
+
+-FMT_LIST_SEPARATOR = '\n - '
+ FABRICS_TRANSPORT_TYPES = ['fc', 'tcp', 'rdma']
+ BROKEN_TRANSPORT_TYPES = ['tcp', 'rdma']
+ SAFE_TRANSPORT_TYPES = ['pcie', 'fc']
+@@ -76,20 +75,6 @@ class NVMEDeviceCollection:
+ return fabrics_devices
+
+
+-def _format_list(data, sep=FMT_LIST_SEPARATOR, callback_sort=sorted, limit=0):
+- # NOTE(pstodulk): Teaser O:-> https://issues.redhat.com/browse/RHEL-126447
+-
+- def identity(values):
+- return values
+-
+- if callback_sort is None:
+- callback_sort = identity
+- res = ['{}{}'.format(sep, item) for item in callback_sort(data)]
+- if limit:
+- return ''.join(res[:limit])
+- return ''.join(res)
+-
+-
+ def is_livemode_enabled() -> bool:
+ livemode_config = next(api.consume(LiveModeConfig), None)
+ if livemode_config and livemode_config.is_enabled:
+@@ -326,8 +311,8 @@ def check_unhandled_devices_present_in_fstab(nvme_device_collection: NVMEDeviceC
+ if required_unhandled_dev_nodes:
+ summary = (
+ 'The system has NVMe devices with a transport type that is currently '
+- 'not handled during the upgrade process present in fstab. Problematic devices: {0}'
+- ).format(_format_list(required_unhandled_dev_nodes))
++ 'not handled during the upgrade process present in fstab. Problematic devices:{0}'
++ ).format(format_list(required_unhandled_dev_nodes))
+
+ reporting.create_report([
+ reporting.Title('NVMe devices with unhandled transport type present in fstab'),
+diff --git a/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py b/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
+index 1ab89a8d..51e47a36 100644
+--- a/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
++++ b/repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py
+@@ -3,9 +3,9 @@ import os
+ from leapp import reporting
+ from leapp.libraries.common.config import version
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
++from leapp.libraries.stdlib import format_list
+
+ COMMON_REPORT_TAGS = [reporting.Groups.SANITY]
+-FMT_LIST_SEPARATOR = '\n - '
+
+ related = [reporting.RelatedResource('file', '/etc/os-release')]
+
+@@ -41,9 +41,9 @@ def check_os_version():
+ ),
+ reporting.Summary(
+ 'The supported OS releases for the upgrade process:'
+- '{}{}\n\nThe detected OS release is: {}'.format(FMT_LIST_SEPARATOR,
+- FMT_LIST_SEPARATOR.join(supported_releases),
+- current_release)
++ '{}\n\nThe detected OS release is: {}'.format(
++ format_list(supported_releases, callback_sort=None),
++ current_release)
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups(COMMON_REPORT_TAGS),
+diff --git a/repos/system_upgrade/common/actors/checksaphana/libraries/checksaphana.py b/repos/system_upgrade/common/actors/checksaphana/libraries/checksaphana.py
+index dcc91222..5b667154 100644
+--- a/repos/system_upgrade/common/actors/checksaphana/libraries/checksaphana.py
++++ b/repos/system_upgrade/common/actors/checksaphana/libraries/checksaphana.py
+@@ -1,6 +1,6 @@
+ from leapp import reporting
+ from leapp.libraries.common.config import architecture, version
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import SapHanaInfo
+
+ # SAP HANA Compatibility
+@@ -82,9 +82,7 @@ def _create_detected_instances_list(details):
+ instances=', '.join(meta['numbers']),
+ admin=meta['admin'],
+ path=meta['path']))
+- if result:
+- return '- {}'.format('\n- '.join(result))
+- return ''
++ return format_list(result, callback_sort=None)
+
+
+ def _min_ver_string():
+diff --git a/repos/system_upgrade/common/actors/checktargetiso/libraries/check_target_iso.py b/repos/system_upgrade/common/actors/checktargetiso/libraries/check_target_iso.py
+index fcb23028..78ac7d6f 100644
+--- a/repos/system_upgrade/common/actors/checktargetiso/libraries/check_target_iso.py
++++ b/repos/system_upgrade/common/actors/checktargetiso/libraries/check_target_iso.py
+@@ -3,42 +3,55 @@ import os
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.config import version
++from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+ from leapp.libraries.stdlib import api, CalledProcessError, run
+ from leapp.models import StorageInfo, TargetOSInstallationImage
+
+
+ def inhibit_if_not_valid_iso_file(iso):
+- inhibit_title = None
+- target_os = 'RHEL {}'.format(version.get_target_major_version())
++ target_os = f'{DISTRO_REPORT_NAMES.target} {version.get_target_major_version()}'
++ remediation_hint = (
++ 'Check whether the supplied target OS installation path points to a valid'
++ f' {target_os} ISO image.'
++ )
++
+ if not os.path.exists(iso.path):
+- inhibit_title = 'Provided {target_os} installation ISO does not exists.'.format(target_os=target_os)
+- inhibit_summary_tpl = 'The supplied {target_os} ISO path \'{iso_path}\' does not point to an existing file.'
+- inhibit_summary = inhibit_summary_tpl.format(target_os=target_os, iso_path=iso.path)
+- else:
+- try:
+- # TODO(mhecko): Figure out whether we will keep this since the scan actor is mounting the ISO anyway
+- file_cmd_output = run(['file', '--mime', iso.path])
+- if 'application/x-iso9660-image' not in file_cmd_output['stdout']:
+- inhibit_title = 'Provided {target_os} installation image is not a valid ISO.'.format(
+- target_os=target_os)
+- summary_tpl = ('The provided {target_os} installation image path \'{iso_path}\''
+- 'does not point to a valid ISO image.')
+- inhibit_summary = summary_tpl.format(target_os=target_os, iso_path=iso.path)
+-
+- except CalledProcessError as err:
+- raise StopActorExecutionError(message='Failed to check whether {0} is an ISO file.'.format(iso.path),
+- details={'details': '{}'.format(err)})
+- if inhibit_title:
+- remediation_hint = ('Check whether the supplied target OS installation path points to a valid'
+- '{target_os} ISO image.'.format(target_os=target_os))
++ reporting.create_report([
++ reporting.Title('Provided target OS installation ISO does not exist.'),
++ reporting.Summary(
++ f'The supplied {target_os} ISO path \'{iso.path}\' does not point to an existing file.'
++ ),
++ reporting.Remediation(hint=remediation_hint),
++ reporting.Severity(reporting.Severity.HIGH),
++ reporting.Groups([reporting.Groups.INHIBITOR]),
++ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('e99b767d6b6623641ba06b5e9c7542ce4c69f35f'),
++ ])
++ return True
+
++ try:
++ # TODO(mhecko): Figure out whether we will keep this since the scan actor is mounting the ISO anyway
++ file_cmd_output = run(['file', '--mime', iso.path])
++ except CalledProcessError as err:
++ raise StopActorExecutionError(
++ message=f'Failed to check whether {iso.path} is an ISO file.',
++ details={'details': f'{err}'},
++ )
++
++ if 'application/x-iso9660-image' not in file_cmd_output['stdout']:
+ reporting.create_report([
+- reporting.Title(inhibit_title),
+- reporting.Summary(inhibit_summary),
++ reporting.Title(
++ 'Provided target OS installation image is not a valid ISO.'
++ ),
++ reporting.Summary(
++ f'The provided {target_os} installation image path \'{iso.path}\''
++ ' does not point to a valid ISO image.'
++ ),
+ reporting.Remediation(hint=remediation_hint),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('21bf7b8c7bf079baa038374ccbce66a8d6d7d775'),
+ ])
+ return True
+ return False
+@@ -48,51 +61,66 @@ def inhibit_if_failed_to_mount_iso(iso):
+ if iso.was_mounted_successfully:
+ return False
+
+- target_os = 'RHEL {0}'.format(version.get_target_major_version())
+- title = 'Failed to mount the provided {target_os} installation image.'
+- summary = 'The provided {target_os} installation image {iso_path} could not be mounted.'
+- hint = 'Verify that the provided ISO is a valid {target_os} installation image'
++ title = 'Failed to mount the provided target OS installation image.'
++ target_os = f'{DISTRO_REPORT_NAMES.target} {version.get_target_major_version()}'
++ summary = f'The provided {target_os} installation image {iso.path} could not be mounted.'
++ hint = f'Verify that the provided ISO is a valid {target_os} installation image'
++
+ reporting.create_report([
+- reporting.Title(title.format(target_os=target_os)),
+- reporting.Summary(summary.format(target_os=target_os, iso_path=iso.path)),
+- reporting.Remediation(hint=hint.format(target_os=target_os)),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Title(title),
++ reporting.Summary(summary),
++ reporting.Remediation(hint=hint),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('156b28ab6ae13ccc2f2dddb6bca243e73eba7c6b'),
+ ])
+ return True
+
+
+-def inhibit_if_wrong_iso_rhel_version(iso):
+- # If the major version could not be determined, the iso.rhel_version will be an empty string
+- if not iso.rhel_version:
++def inhibit_if_wrong_iso_os_version(iso):
++ # If the major version could not be determined, the iso.os_version will be an empty string
++ if not iso.os_version:
+ reporting.create_report([
+ reporting.Title(
+- 'Failed to determine RHEL version provided by the supplied installation image.'),
++ 'Failed to determine target OS provided by the supplied installation image.'
++ ),
+ reporting.Summary(
+- 'Could not determine what RHEL version does the supplied installation image'
+- ' located at {iso_path} provide.'.format(iso_path=iso.path)
++ 'Could not determine what OS or OS version is provided by the supplied'
++ f' installation image located at {iso.path}.'
+ ),
+- reporting.Remediation(hint='Check that the supplied image is a valid RHEL installation image.'),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Remediation(
++ hint=(
++ 'Check that the supplied image is a valid installation image of the'
++ f' target OS and version for the upgrade - {DISTRO_REPORT_NAMES.target}'
++ f' {version.get_target_major_version()}.'
++ )
++ ),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('abfee8507fdb049fea07e4c29bc74a501780287d'),
+ ])
+ return
+
+- iso_rhel_major_version = iso.rhel_version.split('.')[0]
++ iso_os_major_version = iso.os_version.split('.')[0]
+ req_major_ver = version.get_target_major_version()
+- if iso_rhel_major_version != req_major_ver:
+- summary = ('The provided RHEL installation image provides RHEL {iso_rhel_ver}, however, a RHEL '
+- '{required_rhel_ver} image is required for the upgrade.')
++ if iso_os_major_version != req_major_ver:
++ target_distro = DISTRO_REPORT_NAMES.target
+
+ reporting.create_report([
+- reporting.Title('The provided installation image provides invalid RHEL version.'),
+- reporting.Summary(summary.format(iso_rhel_ver=iso.rhel_version, required_rhel_ver=req_major_ver)),
+- reporting.Remediation(hint='Check that the supplied image is a valid RHEL installation image.'),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Title('The provided installation image provides invalid target OS version.'),
++ reporting.Summary(
++ f'The provided {target_distro} installation image provides {target_distro} {iso.os_version},'
++ f' however, a {target_distro} {req_major_ver} image is required for the upgrade.'
++ ),
++ reporting.Remediation(
++ hint=f'Check that the supplied image is a valid {target_distro} installation image.'
++ ),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('e94ecfc02541adca3fa3c9703847425dde736afe'),
+ ])
+
+
+@@ -103,37 +131,41 @@ def inhibit_if_iso_not_located_on_persistent_partition(iso):
+ raise StopActorExecutionError('Actor did not receive any StorageInfo message.')
+
+ # Assumes that the path has been already checked for validity, e.g., the ISO path points to a file
+- iso_mountpoint = iso.path
++ iso_mountpoint = os.path.realpath(iso.path)
+ while not os.path.ismount(iso_mountpoint): # Guaranteed to terminate because we must reach / eventually
+ iso_mountpoint = os.path.dirname(iso_mountpoint)
+
+ is_iso_on_persistent_partition = False
+ for fstab_entry in storage_info.fstab:
+- if fstab_entry.fs_file == iso_mountpoint:
++ if os.path.realpath(fstab_entry.fs_file) == iso_mountpoint:
+ is_iso_on_persistent_partition = True
+ break
+
+ if not is_iso_on_persistent_partition:
+ target_ver = version.get_target_major_version()
+- title = 'The RHEL {target_ver} installation image is not located on a persistently mounted partition'
+- summary = ('The provided RHEL {target_ver} installation image {iso_path} is located'
+- ' on a partition without an entry in /etc/fstab, causing the partition '
+- ' to be persistently mounted.')
+- hint = ('Move the installation image to a partition that is persistently mounted, or create an /etc/fstab'
+- ' entry for the partition on which the installation image is located.')
++ title = 'The target OS installation image is not located on a persistently mounted partition'
++ summary = (
++ f'The provided {DISTRO_REPORT_NAMES.target} {target_ver} installation image {iso.path} is located'
++ ' on a partition without an entry in /etc/fstab, causing the partition to be persistently mounted.'
++ )
++ hint = (
++ 'Move the installation image to a partition that is persistently mounted, or create an /etc/fstab'
++ ' entry for the partition on which the installation image is located.'
++ )
+
+ reporting.create_report([
+- reporting.Title(title.format(target_ver=target_ver)),
+- reporting.Summary(summary.format(target_ver=target_ver, iso_path=iso.path)),
++ reporting.Title(title),
++ reporting.Summary(summary),
+ reporting.Remediation(hint=hint),
+ reporting.RelatedResource('file', '/etc/fstab'),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('d379897ee3c164576c022fe68c8a43e6c9236bf5'),
+ ])
+
+
+-def inihibit_if_iso_does_not_contain_basic_repositories(iso):
++def inhibit_if_iso_does_not_contain_basic_repositories(iso):
+ missing_basic_repoids = {'BaseOS', 'AppStream'}
+
+ for custom_repo in iso.repositories:
+@@ -142,23 +174,25 @@ def inihibit_if_iso_does_not_contain_basic_repositories(iso):
+ break
+
+ if missing_basic_repoids:
+- target_ver = version.get_target_major_version()
++ target_os = f'{DISTRO_REPORT_NAMES.target} {version.get_target_major_version()}'
++ title = 'Provided target OS installation ISO is missing fundamental repositories.'
+
+- title = 'Provided RHEL {target_ver} installation ISO is missing fundamental repositories.'
+- summary = ('The supplied RHEL {target_ver} installation ISO {iso_path} does not contain '
+- '{missing_repos} repositor{suffix}')
+- hint = 'Check whether the supplied ISO is a valid RHEL {target_ver} installation image.'
++ missing_repos = ','.join(missing_basic_repoids)
++ suffix = ('y' if len(missing_basic_repoids) == 1 else 'ies')
++ summary = (
++ f'The supplied {target_os} installation ISO {iso.path} does not contain'
++ f' {missing_repos} repositor{suffix}'
++ )
++ hint = f'Check whether the supplied ISO is a valid {target_os} installation image.'
+
+ reporting.create_report([
+- reporting.Title(title.format(target_ver=target_ver)),
+- reporting.Summary(summary.format(target_ver=target_ver,
+- iso_path=iso.path,
+- missing_repos=','.join(missing_basic_repoids),
+- suffix=('y' if len(missing_basic_repoids) == 1 else 'ies'))),
+- reporting.Remediation(hint=hint.format(target_ver=target_ver)),
+- reporting.Severity(reporting.Severity.MEDIUM),
++ reporting.Title(title),
++ reporting.Summary(summary),
++ reporting.Remediation(hint),
++ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
++ reporting.Key('58fc6fc9530aabd7454d4b4a6381046cab60a189'),
+ ])
+
+
+@@ -177,6 +211,6 @@ def perform_target_iso_checks():
+ if not is_iso_invalid:
+ failed_to_mount_iso = inhibit_if_failed_to_mount_iso(target_iso)
+ if not failed_to_mount_iso:
+- inhibit_if_wrong_iso_rhel_version(target_iso)
++ inhibit_if_wrong_iso_os_version(target_iso)
+ inhibit_if_iso_not_located_on_persistent_partition(target_iso)
+- inihibit_if_iso_does_not_contain_basic_repositories(target_iso)
++ inhibit_if_iso_does_not_contain_basic_repositories(target_iso)
+diff --git a/repos/system_upgrade/common/actors/checktargetiso/tests/test_check_target_iso.py b/repos/system_upgrade/common/actors/checktargetiso/tests/test_check_target_iso.py
+index d819bc34..d2c16530 100644
+--- a/repos/system_upgrade/common/actors/checktargetiso/tests/test_check_target_iso.py
++++ b/repos/system_upgrade/common/actors/checktargetiso/tests/test_check_target_iso.py
+@@ -29,9 +29,9 @@ def test_inhibit_on_iso_mount_failure(monkeypatch, mount_successful):
+ assert is_inhibitor(create_report_mock.reports[0])
+
+
+-@pytest.mark.parametrize(('detected_iso_rhel_ver', 'required_target_ver', 'should_inhibit'),
++@pytest.mark.parametrize(('detected_iso_os_ver', 'required_target_ver', 'should_inhibit'),
+ (('8.6', '8.6', False), ('7.9', '8.6', True), ('8.5', '8.6', False), ('', '8.6', True)))
+-def test_inhibit_on_detected_rhel_version(monkeypatch, detected_iso_rhel_ver, required_target_ver, should_inhibit):
++def test_inhibit_on_detected_iso_version(monkeypatch, detected_iso_os_ver, required_target_ver, should_inhibit):
+ create_report_mock = create_report_mocked()
+ monkeypatch.setattr(reporting, 'create_report', create_report_mock)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver=required_target_ver))
+@@ -39,10 +39,10 @@ def test_inhibit_on_detected_rhel_version(monkeypatch, detected_iso_rhel_ver, re
+ target_iso_msg = TargetOSInstallationImage(path='',
+ mountpoint='',
+ repositories=[],
+- rhel_version=detected_iso_rhel_ver,
++ os_version=detected_iso_os_ver,
+ was_mounted_successfully=True)
+
+- check_target_iso.inhibit_if_wrong_iso_rhel_version(target_iso_msg)
++ check_target_iso.inhibit_if_wrong_iso_os_version(target_iso_msg)
+
+ expected_report_count = 1 if should_inhibit else 0
+ assert create_report_mock.called == expected_report_count
+@@ -52,7 +52,7 @@ def test_inhibit_on_detected_rhel_version(monkeypatch, detected_iso_rhel_ver, re
+
+ @pytest.mark.parametrize(('iso_repoids', 'should_inhibit'),
+ ((('BaseOS', 'AppStream'), False), (('BaseOS',), True), (('AppStream',), True), ((), True)))
+-def test_inhibit_on_invalid_rhel_version(monkeypatch, iso_repoids, should_inhibit):
++def test_inhibit_on_invalid_os_version(monkeypatch, iso_repoids, should_inhibit):
+ create_report_mock = create_report_mocked()
+ monkeypatch.setattr(reporting, 'create_report', create_report_mock)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+@@ -64,7 +64,7 @@ def test_inhibit_on_invalid_rhel_version(monkeypatch, iso_repoids, should_inhibi
+ repositories=iso_repositories,
+ was_mounted_successfully=True)
+
+- check_target_iso.inihibit_if_iso_does_not_contain_basic_repositories(target_iso_msg)
++ check_target_iso.inhibit_if_iso_does_not_contain_basic_repositories(target_iso_msg)
+
+ expected_report_count = 1 if should_inhibit else 0
+ assert create_report_mock.called == expected_report_count
+diff --git a/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py
+index 2369ae11..1e506d70 100644
+--- a/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py
++++ b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py
+@@ -1,8 +1,6 @@
+ from leapp import reporting
+ from leapp.libraries.common.config import get_env, version
+-from leapp.libraries.stdlib import api
+-
+-FMT_LIST_SEPARATOR = '\n - '
++from leapp.libraries.stdlib import api, format_list
+
+
+ def get_supported_target_versions():
+@@ -62,10 +60,9 @@ def process():
+ ' is not supported from the current system version. Follow the official'
+ ' documentation for up to date information about supported upgrade'
+ ' paths and future plans (see the attached link).'
+- ' The in-place upgrade is enabled to the following versions of the target system:{sep}{ver_list}'
++ ' The in-place upgrade is enabled to the following versions of the target system:{ver_list}'
+ .format(
+- sep=FMT_LIST_SEPARATOR,
+- ver_list=FMT_LIST_SEPARATOR.join(supported_target_versions),
++ ver_list=format_list(supported_target_versions, callback_sort=None),
+ tgt_ver=target_version
+ )
+ ),
+diff --git a/repos/system_upgrade/common/actors/checkthirdpartytargetpythonmodules/libraries/checkthirdpartytargetpythonmodules.py b/repos/system_upgrade/common/actors/checkthirdpartytargetpythonmodules/libraries/checkthirdpartytargetpythonmodules.py
+index 7ed34738..07d82fd7 100644
+--- a/repos/system_upgrade/common/actors/checkthirdpartytargetpythonmodules/libraries/checkthirdpartytargetpythonmodules.py
++++ b/repos/system_upgrade/common/actors/checkthirdpartytargetpythonmodules/libraries/checkthirdpartytargetpythonmodules.py
+@@ -1,23 +1,19 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, FMT_LIST_SEPARATOR, format_list
+ from leapp.models import ThirdPartyTargetPythonModules
+
+-FMT_LIST_SEPARATOR = '\n - '
+ MAX_REPORTED_ITEMS = 30
+
+
+-def _formatted_list_output_with_max_items(input_list, sep=FMT_LIST_SEPARATOR, max_items=MAX_REPORTED_ITEMS):
++def _formatted_list_output_with_max_items(input_list, max_items=MAX_REPORTED_ITEMS):
+ if not input_list:
+ return ''
+
+- total_count = len(input_list)
+- items_to_show = input_list[:max_items]
+- formatted = ['{}{}'.format(sep, item) for item in items_to_show]
++ result = format_list(input_list, limit=max_items, callback_sort=None)
++ if len(input_list) > max_items:
++ result += '{}... and {} more'.format(FMT_LIST_SEPARATOR, len(input_list) - max_items)
+
+- if total_count > max_items:
+- formatted.append('{}... and {} more'.format(sep, total_count - max_items))
+-
+- return ''.join(formatted)
++ return result
+
+
+ def check_third_party_target_python_modules(third_party_target_python_modules):
+diff --git a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
+index 5c99a0d9..59675997 100644
+--- a/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
++++ b/repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py
+@@ -2,11 +2,11 @@ import os
+
+ from leapp import reporting
+ from leapp.libraries.common.rhsm import skip_rhsm
++from leapp.libraries.stdlib import format_list
+
+ # If LEAPP_NO_RHSM is set, subscription-manager and product-id will not be
+ # considered as required when checking whether the required plugins are enabled.
+ REQUIRED_DNF_PLUGINS = {'subscription-manager', 'product-id'}
+-FMT_LIST_SEPARATOR = '\n - '
+
+
+ def check_required_dnf_plugins_enabled(pkg_manager_info):
+@@ -24,9 +24,7 @@ def check_required_dnf_plugins_enabled(pkg_manager_info):
+ missing_required_plugins -= {'subscription-manager', 'product-id'}
+
+ if missing_required_plugins:
+- missing_required_plugins_text = ''
+- for missing_plugin in missing_required_plugins:
+- missing_required_plugins_text += '{0}{1}'.format(FMT_LIST_SEPARATOR, missing_plugin)
++ missing_required_plugins_text = format_list(missing_required_plugins)
+
+ # dnf_conf_path - enable/disable plugins globally
+ # rhsm_plugin_conf, product_id_plugin_conf - plugins can be disabled individually
+@@ -44,7 +42,7 @@ def check_required_dnf_plugins_enabled(pkg_manager_info):
+ reporting.create_report([
+ reporting.Title('Required DNF plugins are not being loaded.'),
+ reporting.Summary(
+- 'The following DNF plugins are not being loaded: {}'.format(missing_required_plugins_text)
++ 'The following DNF plugins are not being loaded:{}'.format(missing_required_plugins_text)
+ ),
+ reporting.Remediation(
+ hint=(
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 7d470459..af67cab5 100755
--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh
@@ -3833,6 +4708,361 @@ index 7d470459..af67cab5 100755
mount -o "remount,$old_opts" "$NEWROOT"
exit $result
-
+diff --git a/repos/system_upgrade/common/actors/convert/securebootinhibit/actor.py b/repos/system_upgrade/common/actors/convert/securebootinhibit/actor.py
+index 53f41e71..8372a69a 100644
+--- a/repos/system_upgrade/common/actors/convert/securebootinhibit/actor.py
++++ b/repos/system_upgrade/common/actors/convert/securebootinhibit/actor.py
+@@ -1,4 +1,6 @@
+ from leapp.actors import Actor
++from leapp.dialogs import Dialog
++from leapp.dialogs.components import BooleanComponent
+ from leapp.libraries.actor import securebootinhibit
+ from leapp.models import FirmwareFacts
+ from leapp.reporting import Report
+@@ -8,12 +10,45 @@ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+ class SecureBootInhibit(Actor):
+ """
+ Inhibit the conversion if SecureBoot is enabled.
++
++ When EFI runtime variables are inaccessible and the Secure Boot state
++ cannot be determined automatically, the user is asked to confirm the
++ state via a dialog.
+ """
+
+ name = 'secure_boot_inhibit'
+ consumes = (FirmwareFacts,)
+ produces = (Report,)
+ tags = (IPUWorkflowTag, ChecksPhaseTag)
++ dialogs = (
++ Dialog(
++ scope='secure_boot_inhibit',
++ reason='Confirmation',
++ components=(
++ BooleanComponent(
++ key='confirm_secureboot_enabled',
++ label='Is Secure Boot enabled on this system?',
++ description=(
++ 'Set to True if Secure Boot is enabled, False if disabled.'
++ ' If unsure, check your UEFI firmware settings.'
++ ),
++ reason=(
++ 'The Secure Boot state cannot be determined automatically'
++ ' because UEFI runtime variables are not accessible.'
++ ),
++ ),
++ ),
++ ),
++ )
++
++ _asked_answer = False
++ _sb_answer = None
++
++ def get_sb_answer(self):
++ if not self._asked_answer:
++ self._asked_answer = True
++ self._sb_answer = self.get_answers(self.dialogs[0]).get('confirm_secureboot_enabled')
++ return self._sb_answer
+
+ def process(self):
+ securebootinhibit.process()
+diff --git a/repos/system_upgrade/common/actors/convert/securebootinhibit/libraries/securebootinhibit.py b/repos/system_upgrade/common/actors/convert/securebootinhibit/libraries/securebootinhibit.py
+index 5edb9fa2..23376f4d 100644
+--- a/repos/system_upgrade/common/actors/convert/securebootinhibit/libraries/securebootinhibit.py
++++ b/repos/system_upgrade/common/actors/convert/securebootinhibit/libraries/securebootinhibit.py
+@@ -5,6 +5,65 @@ from leapp.libraries.stdlib import api
+ from leapp.models import FirmwareFacts
+
+
++def _report_secureboot_enabled(answered_by_user):
++ answerfile_hint = ''
++ if answered_by_user:
++ # extra space in the beginning due to pasting inside the hint str
++ answerfile_hint = (
++ ' '
++ 'Then update your answer about the secure boot state in the answerfile'
++ ' to reflect the new state (for key: "confirm_secureboot_enabled").'
++ )
++
++ hint = (
++ 'To be able to convert the system to a different Linux distribution,'
++ ' disable Secure Boot.{} Then re-enable Secure Boot'
++ ' again after the upgrade and conversion process is finished successfully.'
++ ' Check instructions for your current OS, or hypervisor in'
++ ' case of virtual machines, for more information how to'
++ ' disable Secure Boot.'
++ .format(answerfile_hint)
++ )
++
++ reporting.create_report([
++ reporting.Title(
++ 'Detected enabled Secure Boot when trying to convert the system'
++ ),
++ reporting.Summary(
++ 'Conversion to a different Linux distribution is not possible'
++ ' when the Secure Boot is enabled. Artifacts of the target'
++ ' Linux distribution are signed by keys that are not accepted'
++ ' by the source Linux distribution.'
++ ),
++ reporting.Severity(reporting.Severity.HIGH),
++ reporting.Groups([reporting.Groups.INHIBITOR, reporting.Groups.BOOT]),
++ reporting.Remediation(hint=hint),
++ ])
++
++
++def _report_missing_answer():
++ reporting.create_report([
++ reporting.Title('Cannot determine the Secure Boot state'),
++ reporting.Summary(
++ 'The system is booted in UEFI mode but the Secure Boot state'
++ ' cannot be determined automatically because EFI runtime'
++ ' variables are not accessible. The information Secure Boot state'
++ ' is required to determine the next steps for the in-place upgrade'
++ ' and conversion process.'
++ ),
++
++ reporting.Severity(reporting.Severity.HIGH),
++ reporting.Groups([reporting.Groups.INHIBITOR, reporting.Groups.BOOT]),
++ reporting.Remediation(
++ hint='Verify the Secure Boot state in your UEFI firmware'
++ ' settings. If Secure Boot is enabled, disable it before'
++ ' proceeding with the upgrade and conversion. Then provide the Secure Boot'
++ ' state by setting the value of "confirm_secureboot_enabled"'
++ ' to True or False in the answer file and re-run.'
++ ),
++ ])
++
++
+ def process():
+ if not is_conversion():
+ return
+@@ -12,31 +71,32 @@ def process():
+ ff = next(api.consume(FirmwareFacts), None)
+ if not ff:
+ raise StopActorExecutionError(
+- "Could not identify system firmware",
+- details={"details": "Actor did not receive FirmwareFacts message."},
++ 'Could not identify system firmware',
++ details={'details': 'Actor did not receive FirmwareFacts message.'},
+ )
+
+- if ff.firmware == "efi" and ff.secureboot_enabled:
+- report = [
+- reporting.Title(
+- "Detected enabled Secure Boot when trying to convert the system"
+- ),
+- reporting.Summary(
+- "Conversion to a different Linux distribution is not possible"
+- " when the Secure Boot is enabled. Artifacts of the target"
+- " Linux distribution are signed by keys that are not accepted"
+- " by the source Linux distribution."
+- ),
+- reporting.Severity(reporting.Severity.HIGH),
+- reporting.Groups([reporting.Groups.INHIBITOR, reporting.Groups.BOOT]),
+- # TODO some link
+- reporting.Remediation(
+- hint="Disable Secure Boot to be able to convert the system to"
+- " a different Linux distribution. Then re-enable Secure Boot"
+- " again after the upgrade process is finished successfully."
+- " Check instructions for your current OS, or hypervisor in"
+- " case of virtual machines, for more information how to"
+- " disable Secure Boot."
+- ),
+- ]
+- reporting.create_report(report)
++ if ff.firmware != 'efi' or ff.secureboot_enabled is False:
++ return
++
++ secureboot_enabled = ff.secureboot_enabled
++ if secureboot_enabled is None and ff.efi_vars_accessible is True:
++ # HW does not support Secure Boot (EFI vars readable, SB state is None).
++ api.current_logger().info('Secure Boot is not supported by the HW. Skipping.')
++ return
++
++ answered_by_user = False
++ if secureboot_enabled is None:
++ # Cannot determine the SB state (no EFI vars) -> get answer from user
++ secureboot_enabled = api.current_actor().get_sb_answer()
++ if secureboot_enabled is None:
++ _report_missing_answer()
++ return
++ answered_by_user = True
++
++ if secureboot_enabled is True:
++ _report_secureboot_enabled(answered_by_user)
++ else:
++ api.current_logger().info(
++ 'User confirmed Secure Boot is disabled; proceeding despite'
++ ' inaccessible EFI variables.'
++ )
+diff --git a/repos/system_upgrade/common/actors/convert/securebootinhibit/tests/test_securebootinhibit.py b/repos/system_upgrade/common/actors/convert/securebootinhibit/tests/test_securebootinhibit.py
+index 340e6b16..39ad9475 100644
+--- a/repos/system_upgrade/common/actors/convert/securebootinhibit/tests/test_securebootinhibit.py
++++ b/repos/system_upgrade/common/actors/convert/securebootinhibit/tests/test_securebootinhibit.py
+@@ -7,52 +7,93 @@ from leapp.libraries.stdlib import api
+ from leapp.models import FirmwareFacts
+
+
++def _ff(firmware='efi', secureboot_enabled=None, efi_vars_accessible=None):
++ return FirmwareFacts(
++ firmware=firmware,
++ ppc64le_opal=None,
++ secureboot_enabled=secureboot_enabled,
++ efi_vars_accessible=efi_vars_accessible,
++ )
++
++
++class _CurrentActorWithDialog(CurrentActorMocked):
++ def __init__(self, *args, **kwargs):
++ self._sb_answer = kwargs.pop('sb_answer', None)
++ super().__init__(*args, **kwargs)
++
++ def get_sb_answer(self):
++ return self._sb_answer
++
++
+ @pytest.mark.parametrize(
+ 'ff,is_conversion,should_inhibit', [
+- # conversion, secureboot enabled = inhibit
+- (
+- FirmwareFacts(firmware='efi', ppc64le_opal=None, secureboot_enabled=True),
+- True,
+- True
+- ),
+- (
+- FirmwareFacts(firmware='efi', ppc64le_opal=None, secureboot_enabled=True),
+- False,
+- False
+- ),
+- # bios is ok
+- (
+- FirmwareFacts(firmware='bios', ppc64le_opal=None, secureboot_enabled=False),
+- False,
+- False
+- ),
+- # bios is ok during conversion too
+- (
+- FirmwareFacts(firmware='bios', ppc64le_opal=None, secureboot_enabled=False),
+- True,
+- False
+- ),
+- (
+- FirmwareFacts(firmware='efi', ppc64le_opal=None, secureboot_enabled=False),
+- True,
+- False
+- ),
+- (
+- FirmwareFacts(firmware='efi', ppc64le_opal=None, secureboot_enabled=False),
+- False,
+- False
+- ),
++ # SB enabled + conversion = inhibit
++ (_ff(secureboot_enabled=True, efi_vars_accessible=True), True, True),
++ # SB enabled + no conversion = no report
++ (_ff(secureboot_enabled=True, efi_vars_accessible=True), False, False),
++ # SB disabled + conversion = no report
++ (_ff(secureboot_enabled=False, efi_vars_accessible=True), True, False),
++ # SB disabled + no conversion = no report
++ (_ff(secureboot_enabled=False, efi_vars_accessible=True), False, False),
++ # BIOS + conversion = no report
++ (_ff(firmware='bios', secureboot_enabled=False), True, False),
++ # BIOS + no conversion = no report
++ (_ff(firmware='bios', secureboot_enabled=False), False, False),
+ ]
+ )
+-def test_process(monkeypatch, ff, is_conversion, should_inhibit):
++def test_process_definitive_sb_state(monkeypatch, ff, is_conversion, should_inhibit):
++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[ff]))
++ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
++ monkeypatch.setattr(securebootinhibit, 'is_conversion', lambda: is_conversion)
++
++ securebootinhibit.process()
++
++ if should_inhibit:
++ assert reporting.create_report.called == 1
++ assert reporting.Groups.INHIBITOR in reporting.create_report.report_fields['groups']
++ assert reporting.create_report.report_fields['title'] == (
++ 'Detected enabled Secure Boot when trying to convert the system'
++ )
++ else:
++ assert not reporting.create_report.called
++
++
++def test_sb_none_efi_vars_accessible(monkeypatch):
++ """HW does not support Secure Boot -- efi_vars work, sb is None."""
++ ff = _ff(secureboot_enabled=None, efi_vars_accessible=True)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[ff]))
+- monkeypatch.setattr(reporting, "create_report", create_report_mocked())
+- monkeypatch.setattr(securebootinhibit, "is_conversion", lambda: is_conversion)
++ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
++ monkeypatch.setattr(securebootinhibit, 'is_conversion', lambda: True)
++
++ securebootinhibit.process()
++
++ assert not reporting.create_report.called
++
++
++@pytest.mark.parametrize(
++ 'sb_answer,should_inhibit,expected_title,efi_vars_accessible', [
++ (False, False, None, False),
++ (True, True, 'Detected enabled Secure Boot when trying to convert the system', False),
++ (None, True, 'Cannot determine the Secure Boot state', False),
++ (None, True, 'Cannot determine the Secure Boot state', None),
++ ]
++)
++def test_sb_none_efi_vars_inaccessible_dialog(monkeypatch, sb_answer, should_inhibit, expected_title,
++ efi_vars_accessible):
++ """EFI vars inaccessible or None -- dialog determines outcome."""
++ ff = _ff(secureboot_enabled=None, efi_vars_accessible=efi_vars_accessible)
++ monkeypatch.setattr(
++ api, 'current_actor',
++ _CurrentActorWithDialog(sb_answer=sb_answer, msgs=[ff]),
++ )
++ monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
++ monkeypatch.setattr(securebootinhibit, 'is_conversion', lambda: True)
+
+ securebootinhibit.process()
+
+ if should_inhibit:
+ assert reporting.create_report.called == 1
+ assert reporting.Groups.INHIBITOR in reporting.create_report.report_fields['groups']
++ assert reporting.create_report.report_fields['title'] == expected_title
+ else:
+ assert not reporting.create_report.called
+diff --git a/repos/system_upgrade/common/actors/distributionsignedrpmcheck/libraries/distributionsignedrpmcheck.py b/repos/system_upgrade/common/actors/distributionsignedrpmcheck/libraries/distributionsignedrpmcheck.py
+index 1c0df635..86033646 100644
+--- a/repos/system_upgrade/common/actors/distributionsignedrpmcheck/libraries/distributionsignedrpmcheck.py
++++ b/repos/system_upgrade/common/actors/distributionsignedrpmcheck/libraries/distributionsignedrpmcheck.py
+@@ -1,10 +1,8 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.libraries.stdlib.config import is_verbose
+ from leapp.models import ThirdPartyRPM
+
+-FMT_LIST_SEPARATOR = "\n - "
+-
+
+ def _generate_report(packages):
+ """Generate a report with installed packages not signed by the distribution"""
+@@ -24,8 +22,8 @@ def _generate_report(packages):
+ " cannot be satisfied and hence such packages cannot be installed on"
+ " the target system.\n\n"
+ "The following packages have not been signed by the vendor of the"
+- " distribution:{}{}"
+- ).format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(packages))
++ " distribution:{}"
++ ).format(format_list(packages))
+ hint = (
+ "The most simple solution that does not require additional knowledge"
+ " about the upgrade process is the uninstallation of such packages"
+@@ -71,7 +69,6 @@ def get_third_party_pkgs():
+ )
+
+ third_party_pkgs = list(set(pkg.name for pkg in data.items))
+- third_party_pkgs.sort()
+ return third_party_pkgs
+
+
diff --git a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py b/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py
index 003f3fc5..9e7bbf4a 100644
--- a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py
@@ -4037,11 +5267,111 @@ index aa735da3..fdb4c5ac 100644
+ to_reinstall=list(to_reinstall),
modules_to_reset=list(modules_to_reset.values()),
modules_to_enable=list(modules_to_enable.values())))
+diff --git a/repos/system_upgrade/common/actors/gpgpubkeycheck/libraries/gpgpubkeycheck.py b/repos/system_upgrade/common/actors/gpgpubkeycheck/libraries/gpgpubkeycheck.py
+index 387c6cef..6290c663 100644
+--- a/repos/system_upgrade/common/actors/gpgpubkeycheck/libraries/gpgpubkeycheck.py
++++ b/repos/system_upgrade/common/actors/gpgpubkeycheck/libraries/gpgpubkeycheck.py
+@@ -1,11 +1,9 @@
+ from leapp import reporting
+ from leapp.libraries.common.gpg import is_nogpgcheck_set
+ from leapp.libraries.common.rpms import get_installed_rpms
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import TrustedGpgKeys
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _get_installed_fps_tuple():
+ """
+@@ -40,10 +38,9 @@ def _report_cannot_check_keys(installed_fps):
+ ' in the start of the upgrade process on the original system.'
+ ' Unexpected unexpected installed GPG keys could be e.g. a mark of'
+ ' a malicious attempt to hijack the upgrade process.'
+- ' The list of all GPG keys in RPM DB:{sep}{key_list}'
++ ' The list of all GPG keys in RPM DB:{key_list}'
+ .format(
+- sep=FMT_LIST_SEPARATOR,
+- key_list=FMT_LIST_SEPARATOR.join(installed_fps)
++ key_list=format_list(installed_fps, callback_sort=None)
+ )
+ )
+ hint = (
+@@ -68,11 +65,9 @@ def _report_unexpected_keys(unexpected_fps):
+ 'The system contains unexpected GPG keys after upgrade.'
+ ' This can be caused e.g. by a manual intervention'
+ ' or by malicious attempt to hijack the upgrade process.'
+- ' The unexpected keys are the following:'
+- ' {sep}{key_list}'
++ ' The unexpected keys are the following:{key_list}'
+ .format(
+- sep=FMT_LIST_SEPARATOR,
+- key_list=FMT_LIST_SEPARATOR.join(unexpected_fps)
++ key_list=format_list(unexpected_fps, callback_sort=None)
+ )
+ )
+ hint = (
+diff --git a/repos/system_upgrade/common/actors/initramfs/checkinitramfstasks/libraries/checkinitramfstasks.py b/repos/system_upgrade/common/actors/initramfs/checkinitramfstasks/libraries/checkinitramfstasks.py
+index 0d7d8317..fd0abf36 100644
+--- a/repos/system_upgrade/common/actors/initramfs/checkinitramfstasks/libraries/checkinitramfstasks.py
++++ b/repos/system_upgrade/common/actors/initramfs/checkinitramfstasks/libraries/checkinitramfstasks.py
+@@ -2,7 +2,7 @@ import os
+ from collections import defaultdict
+
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import TargetInitramfsTasks, UpgradeInitramfsTasks
+
+ DRACUT_MOD_DIR = '/usr/lib/dracut/modules.d/'
+@@ -15,11 +15,8 @@ SUMMARY_FMT = (
+
+
+ def _printable_modules(conflicts):
+- list_separator_fmt = '\n - '
+- for name, paths in conflicts.items():
+- paths = sorted([str(i) for i in paths])
+- output = ['{}{}: {}'.format(list_separator_fmt, name, paths)]
+- return ''.join(output)
++ items = ['{}: {}'.format(name, sorted([str(i) for i in paths])) for name, paths in conflicts.items()]
++ return format_list(items)
+
+
+ def _treat_path_dracut(dmodule):
diff --git a/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py b/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py
-index 32e4527b..1e595e9a 100644
+index 32e4527b..ef03d0fa 100644
--- a/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py
+++ b/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py
-@@ -152,11 +152,11 @@ def _report(title, summary, keys, inhibitor=False):
+@@ -10,7 +10,7 @@ from leapp import reporting
+ from leapp.exceptions import StopActorExecution, StopActorExecutionError
+ from leapp.libraries.common.config.version import get_target_major_version
+ from leapp.libraries.common.gpg import get_gpg_fp_from_file, get_path_to_gpg_certs, is_nogpgcheck_set
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import (
+ DNFWorkaround,
+ TargetUserSpaceInfo,
+@@ -20,8 +20,6 @@ from leapp.models import (
+ )
+ from leapp.utils.deprecation import suppress_deprecation
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _expand_vars(path):
+ """
+@@ -143,20 +141,19 @@ def _report(title, summary, keys, inhibitor=False):
+ ' review is required, so any spurious keys are not imported in the system'
+ ' during the in-place upgrade.'
+ ' The following additional gpg keys are required to be imported during'
+- ' the upgrade:{sep}{key_list}'
++ ' the upgrade:{key_list}'
+ .format(
+ summary=summary,
+- sep=FMT_LIST_SEPARATOR,
+- key_list=FMT_LIST_SEPARATOR.join(keys)
++ key_list=format_list(keys, callback_sort=None)
+ )
)
hint = (
'Check the path to the listed GPG keys is correct, the keys are valid and'
@@ -4055,7 +5385,7 @@ index 32e4527b..1e595e9a 100644
)
groups = [reporting.Groups.REPOSITORY]
if inhibitor:
-@@ -188,7 +188,7 @@ def _report_missing_keys(keys):
+@@ -188,7 +185,7 @@ def _report_missing_keys(keys):
summary = (
'Some of the target repositories require GPG keys that are not installed'
' in the current RPM DB or are not stored in the {trust_dir} directory.'
@@ -4064,7 +5394,21 @@ index 32e4527b..1e595e9a 100644
)
_report('Detected unknown GPG keys for target system repositories', summary, keys, True)
-@@ -262,11 +262,12 @@ def _report_repos_missing_keys(repos):
+@@ -224,11 +221,9 @@ def _report_repos_missing_keys(repos):
+ ' Leapp is not able to guarantee validity of such gpg keys and manual'
+ ' review is required, so any spurious keys are not imported in the system'
+ ' during the in-place upgrade.'
+- ' The following repositories require some attention before the upgrade:'
+- ' {sep}{key_list}'
++ ' The following repositories require some attention before the upgrade:{key_list}'
+ .format(
+- sep=FMT_LIST_SEPARATOR,
+- key_list=FMT_LIST_SEPARATOR.join(repos)
++ key_list=format_list(repos, callback_sort=None)
+ )
+ )
+ hint = (
+@@ -262,11 +257,12 @@ def _report_repos_missing_keys(repos):
def register_dnfworkaround():
@@ -4082,11 +5426,64 @@ index 32e4527b..1e595e9a 100644
@suppress_deprecation(TMPTargetRepositoriesFacts)
+diff --git a/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py b/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
+index cd45ac87..226be17b 100644
+--- a/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
++++ b/repos/system_upgrade/common/actors/reportleftoverpackages/libraries/reportleftoverpackages.py
+@@ -1,10 +1,8 @@
+ from leapp import reporting
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import LeftoverPackages, RemovedPackages
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def process():
+ removed_packages = next(api.consume(RemovedPackages), None)
+@@ -15,12 +13,11 @@ def process():
+ 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 {source_distro} packages have been removed:{sep}{list}\n'
++ 'Following {source_distro} packages have been removed:{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=format_list(removed),
+ **DISTRO_REPORT_NAMES
+ )
+ )
+@@ -35,11 +32,10 @@ def process():
+
+ if leftover_packages and leftover_packages.items:
+ summary = (
+- 'Following {source_distro} packages have not been upgraded:{sep}{list}\n'
++ 'Following {source_distro} packages have not been upgraded:{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=format_list(leftover_pkgs_to_remove),
+ **DISTRO_REPORT_NAMES
+ )
+ )
diff --git a/repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/libraries/rpmtransactionconfigtaskscollector.py b/repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/libraries/rpmtransactionconfigtaskscollector.py
-index 84895f83..62aefaf4 100644
+index 84895f83..3ceb6488 100644
--- a/repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/libraries/rpmtransactionconfigtaskscollector.py
+++ b/repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/libraries/rpmtransactionconfigtaskscollector.py
-@@ -18,22 +18,37 @@ def load_tasks_file(path, logger):
+@@ -1,6 +1,6 @@
+ import os.path
+
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import DistributionSignedRPM, RpmTransactionTasks
+
+
+@@ -18,22 +18,34 @@ def load_tasks_file(path, logger):
return []
@@ -4097,10 +5494,7 @@ index 84895f83..62aefaf4 100644
+ # And these ones are the ones that are.
+ filtered_out = list(set(to_filter) - set(filtered_ok))
+ if filtered_out:
-+ api.current_logger().debug(
-+ debug_msg +
-+ '\n- ' + '\n- '.join(filtered_out)
-+ )
++ api.current_logger().debug('%s%s', debug_msg, format_list(filtered_out))
+ # We may want to use either of the two sets.
+ return filtered_ok, filtered_out
+
@@ -4131,6 +5525,395 @@ index 84895f83..62aefaf4 100644
+ to_reinstall=to_reinstall_filtered,
to_keep=load_tasks_file(os.path.join(base_dir, 'to_keep'), logger),
to_remove=load_tasks_file(os.path.join(base_dir, 'to_remove'), logger))
+diff --git a/repos/system_upgrade/common/actors/scantargetiso/libraries/scan_target_os_iso.py b/repos/system_upgrade/common/actors/scantargetiso/libraries/scan_target_os_iso.py
+index a5f0750a..0f755b09 100644
+--- a/repos/system_upgrade/common/actors/scantargetiso/libraries/scan_target_os_iso.py
++++ b/repos/system_upgrade/common/actors/scantargetiso/libraries/scan_target_os_iso.py
+@@ -1,50 +1,99 @@
+ import os
+
+ import leapp.libraries.common.config as ipu_config
++from leapp.libraries.common.distro import distro_id_to_pretty_name, get_distro_iso_config
+ from leapp.libraries.common.mounting import LoopMount, MountError
+ from leapp.libraries.stdlib import api, CalledProcessError, run
+ from leapp.models import CustomTargetRepository, TargetOSInstallationImage
+
+
+-def determine_rhel_version_from_iso_mountpoint(iso_mountpoint):
++def etc_release_extract_version(etc_release_contents, target_distro):
++ """
++ Extract product release version from /etc/release content
++
++ :return: The parsed version or None if it couldn't be determined
++ :rtype: str | None
++ """
++ # 'Red Hat Enterprise Linux release 8.10 (Ootpa)' -> ['Red Hat Enterprise Linux', '8.10 (Ootpa)']
++ # Red Hat Enterprise Linux release 9.8 (Plow)
++ # CentOS Stream release 8
++ api.current_logger().debug(
++ 'Determining OS version from etc release contents: {}'.format(etc_release_contents)
++ )
++ product_release_fragments = etc_release_contents.split('release')
++ if len(product_release_fragments) != 2:
++ api.current_logger().debug('Failed to determine OS version: Unexpected format of etc release')
++ return None # Unlikely. Either way we failed to parse the release
++
++ required_distro = distro_id_to_pretty_name(target_distro)
++ determined_distro = product_release_fragments[0].strip()
++ if not determined_distro.startswith(required_distro):
++ api.current_logger().debug(
++ 'Failed to determine OS version: The OS name from etc release ({}) does not match the'
++ ' requested target distro ({})'.format(determined_distro, required_distro)
++ )
++ return None
++
++ determined_ver = product_release_fragments[1].strip().split(' ', 1)[0] # Remove release name (Maipo)
++ return determined_ver
++
++
++def determine_os_version_from_iso_mountpoint(iso_mountpoint):
+ baseos_packages = os.path.join(iso_mountpoint, 'BaseOS/Packages')
+- if os.path.isdir(baseos_packages):
+- def is_rh_release_pkg(pkg_name):
+- return pkg_name.startswith('redhat-release') and 'eula' not in pkg_name
+
+- redhat_release_pkgs = [pkg for pkg in os.listdir(baseos_packages) if is_rh_release_pkg(pkg)]
++ if not os.path.isdir(baseos_packages):
++ return ''
+
+- if not redhat_release_pkgs:
+- return '' # We did not determine anything
++ target_distro = ipu_config.get_target_distro_id()
++ distro_iso_config = get_distro_iso_config(target_distro)
+
+- if len(redhat_release_pkgs) > 1:
+- api.current_logger().warning('Multiple packages with name redhat-release* found when '
+- 'determining RHEL version of the supplied installation ISO.')
++ def is_release_pkg(pkg_name):
++ return (
++ pkg_name.startswith(distro_iso_config.release_pkg_name_prefix)
++ and 'eula' not in pkg_name
++ )
+
+- redhat_release_pkg = redhat_release_pkgs[0]
++ distro_release_pkgs = [pkg for pkg in os.listdir(baseos_packages) if is_release_pkg(pkg)]
+
+- determined_rhel_ver = ''
+- try:
+- rh_release_pkg_path = os.path.join(baseos_packages, redhat_release_pkg)
+- # rpm2cpio is provided by rpm; cpio is a dependency of yum (rhel7) and a dependency of dracut which is
+- # a dependency for leapp (rhel8+)
+- cpio_archive = run(['rpm2cpio', rh_release_pkg_path])
+- etc_rh_release_contents = run(['cpio', '--extract', '--to-stdout', './etc/redhat-release'],
+- stdin=cpio_archive['stdout'])
++ if not distro_release_pkgs:
++ return '' # We did not determine anything
+
+- # 'Red Hat Enterprise Linux Server release 7.9 (Maipo)' -> ['Red Hat...', '7.9 (Maipo']
+- product_release_fragments = etc_rh_release_contents['stdout'].split('release')
+- if len(product_release_fragments) != 2:
+- return '' # Unlikely. Either way we failed to parse the release
++ if len(distro_release_pkgs) > 1:
++ api.current_logger().warning(
++ 'Multiple packages with name {}* found when determining target version of the supplied'
++ ' installation ISO.'.format(distro_iso_config.release_pkg_name_prefix)
++ )
+
+- if not product_release_fragments[0].startswith('Red Hat'):
+- return ''
++ distro_release_pkg = distro_release_pkgs[0]
+
+- determined_rhel_ver = product_release_fragments[1].strip().split(' ', 1)[0] # Remove release name (Maipo)
+- return determined_rhel_ver
+- except CalledProcessError:
+- return ''
+- return ''
++ try:
++ release_pkg_path = os.path.join(baseos_packages, distro_release_pkg)
++ # rpm2cpio is provided by rpm; cpio is a dependency of yum (rhel7) and a dependency of dracut which is
++ # a dependency for leapp (rhel8+)
++ cpio_archive = run(['rpm2cpio', release_pkg_path])
++ etc_release_contents = run(
++ [
++ 'cpio',
++ '--extract',
++ '--to-stdout',
++ f'.{distro_iso_config.etc_release_file}',
++ ],
++ stdin=cpio_archive['stdout'],
++ )
++
++ return etc_release_extract_version(etc_release_contents['stdout'], target_distro) or ''
++ except CalledProcessError:
++ # FIXME?: This might fail e.g. if the ISO isn't complete
++ # (download/scp/...) interrupted. Maybe we should at include
++ # info that in the report?
++ # Leaving an exact example from the logs (yes the empty line is there):
++ # error:
++ # /var/lib/leapp/iso_scan_mountpoint/BaseOS/Packages/centos-stream-release-9.0-26.el9.noarch.rpm:
++ # read failed: Input/output error (5)
++
++ # error reading header from package
++
++ return ''
+
+
+ def inform_ipu_about_request_to_use_target_iso():
+@@ -62,7 +111,7 @@ def inform_ipu_about_request_to_use_target_iso():
+ was_mounted_successfully=False))
+ return
+
+- # Mount the given ISO, extract the available repositories and determine provided RHEL version
++ # Mount the given ISO, extract the available repositories and determine provided target version
+ iso_scan_mountpoint = '/var/lib/leapp/iso_scan_mountpoint'
+ try:
+ with LoopMount(source=target_iso_path, target=iso_scan_mountpoint):
+@@ -80,12 +129,13 @@ def inform_ipu_about_request_to_use_target_iso():
+ api.produce(iso_repo)
+ iso_repos.append(iso_repo)
+
+- rhel_version = determine_rhel_version_from_iso_mountpoint(iso_scan_mountpoint)
++ os_version = determine_os_version_from_iso_mountpoint(iso_scan_mountpoint)
+
+ api.produce(TargetOSInstallationImage(path=target_iso_path,
+ repositories=iso_repos,
+ mountpoint=iso_mountpoint,
+- rhel_version=rhel_version,
++ rhel_version=os_version,
++ os_version=os_version,
+ was_mounted_successfully=True))
+ except MountError:
+ # Do not analyze the situation any further as ISO checks will be done by another actor
+diff --git a/repos/system_upgrade/common/actors/scantargetiso/tests/test_scan_target_iso.py b/repos/system_upgrade/common/actors/scantargetiso/tests/test_scan_target_iso.py
+index 8e235c6d..40ba5eba 100644
+--- a/repos/system_upgrade/common/actors/scantargetiso/tests/test_scan_target_iso.py
++++ b/repos/system_upgrade/common/actors/scantargetiso/tests/test_scan_target_iso.py
+@@ -15,7 +15,7 @@ def fail_if_called(fail_reason, *args, **kwargs):
+ assert False, fail_reason
+
+
+-def test_determine_rhel_version_determination_unexpected_iso_structure_or_invalid_mountpoint(monkeypatch):
++def test_determine_distro_version_determination_unexpected_iso_structure_or_invalid_mountpoint(monkeypatch):
+ iso_mountpoint = '/some/mountpoint'
+
+ run_mocked = partial(fail_if_called,
+@@ -28,11 +28,65 @@ def test_determine_rhel_version_determination_unexpected_iso_structure_or_invali
+
+ monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
+
+- determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
++ determined_version = scan_target_os_iso.determine_os_version_from_iso_mountpoint(iso_mountpoint)
+ assert not determined_version
+
+
+-def test_determine_rhel_version_valid_iso(monkeypatch):
++@pytest.mark.parametrize(
++ "distro, pkgs, etc_release_fname, etc_release_content, expect",
++ [
++ (
++ "rhel",
++ [
++ "redhat-release-8.7-0.3.el8.x86_64.rpm",
++ "redhat-release-eula-8.7-0.3.el8.x86_64.rpm", # test that this one isn't picked up
++ ],
++ "./etc/redhat-release",
++ "Red Hat Enterprise Linux Server release 7.9 (Maipo)",
++ "7.9",
++ ),
++ (
++ "centos",
++ ["centos-stream-release-9.0-34.el9.x86_64.rpm"],
++ "./etc/centos-release",
++ "CentOS Stream release 9",
++ "9"
++ ),
++ (
++ "centos",
++ ["centos-stream-release-10.0-19.el10.noarch.rpm"],
++ "./etc/centos-release",
++ "CentOS Stream release 10 (Coughlan)",
++ "10"
++ ),
++ (
++ "almalinux",
++ ["almalinux-release-9.7-1.el9.x86_64.rpm"],
++ "./etc/almalinux-release",
++ "AlmaLinux release 9.7 (Moss Jungle Cat)",
++ "9.7"
++ ),
++ (
++ "almalinux",
++ ["almalinux-release-10.1-16.el10.x86_64.rpm"],
++ "./etc/almalinux-release",
++ "AlmaLinux release 10.1 (Heliotrope Lion)",
++ "10.1"
++ ),
++ (
++ "rocky",
++ ["rocky-release-10.2-1.1.el10.noarch"],
++ "./etc/rocky-release",
++ "Rocky Linux release 10.2 (Red Quartz)",
++ "10.2"
++ ),
++
++ ],
++)
++def test_determine_distro_version_valid_iso(
++ monkeypatch, distro, pkgs, etc_release_fname, etc_release_content, expect
++):
++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(release_id=distro))
+ iso_mountpoint = '/some/mountpoint'
+
+ def isdir_mocked(path):
+@@ -40,31 +94,28 @@ def test_determine_rhel_version_valid_iso(monkeypatch):
+
+ def listdir_mocked(path):
+ assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
+- return ['xz-5.2.4-4.el8_6.x86_64.rpm',
+- 'libmodman-2.0.1-17.el8.i686.rpm',
+- 'redhat-release-8.7-0.3.el8.x86_64.rpm',
+- 'redhat-release-eula-8.7-0.3.el8.x86_64.rpm']
++ return ['xz-5.2.4-4.el8_6.x86_64.rpm', 'libmodman-2.0.1-17.el8.i686.rpm'] + pkgs
+
+ def run_mocked(cmd, *args, **kwargs):
+ rpm2cpio_output = 'rpm2cpio_output'
+ if cmd[0] == 'rpm2cpio':
+- assert cmd == ['rpm2cpio', '/some/mountpoint/BaseOS/Packages/redhat-release-8.7-0.3.el8.x86_64.rpm']
++ assert cmd == ['rpm2cpio', f'/some/mountpoint/BaseOS/Packages/{pkgs[0]}']
+ return {'stdout': rpm2cpio_output}
+ if cmd[0] == 'cpio':
+- assert cmd == ['cpio', '--extract', '--to-stdout', './etc/redhat-release']
++ assert cmd == ['cpio', '--extract', '--to-stdout', etc_release_fname]
+ assert kwargs['stdin'] == rpm2cpio_output
+- return {'stdout': 'Red Hat Enterprise Linux Server release 7.9 (Maipo)'}
++ return {'stdout': etc_release_content}
+ raise ValueError('Unexpected command has been called.')
+
+ monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
+ monkeypatch.setattr(os, 'listdir', listdir_mocked)
+ monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)
+
+- determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
+- assert determined_version == '7.9'
++ determined_version = scan_target_os_iso.determine_os_version_from_iso_mountpoint(iso_mountpoint)
++ assert determined_version == expect
+
+
+-def test_determine_rhel_version_valid_iso_no_rh_release(monkeypatch):
++def test_determine_distro_version_valid_iso_no_release_file(monkeypatch):
+ iso_mountpoint = '/some/mountpoint'
+
+ def isdir_mocked(path):
+@@ -81,12 +132,13 @@ def test_determine_rhel_version_valid_iso_no_rh_release(monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
+ monkeypatch.setattr(os, 'listdir', listdir_mocked)
+ monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)
++ monkeypatch.setattr(api, "current_actor", CurrentActorMocked())
+
+- determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
++ determined_version = scan_target_os_iso.determine_os_version_from_iso_mountpoint(iso_mountpoint)
+ assert determined_version == ''
+
+
+-def test_determine_rhel_version_rpm_extract_fails(monkeypatch):
++def test_determine_distro_version_rpm_extract_fails(monkeypatch):
+ iso_mountpoint = '/some/mountpoint'
+
+ def isdir_mocked(path):
+@@ -102,15 +154,17 @@ def test_determine_rhel_version_rpm_extract_fails(monkeypatch):
+ monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
+ monkeypatch.setattr(os, 'listdir', listdir_mocked)
+ monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)
++ monkeypatch.setattr(api, "current_actor", CurrentActorMocked())
+
+- determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
++ determined_version = scan_target_os_iso.determine_os_version_from_iso_mountpoint(iso_mountpoint)
+ assert determined_version == ''
+
+
+-@pytest.mark.parametrize('etc_rh_release_contents', ('',
+- 'Red Hat Enterprise Linux Server',
+- 'Fedora release 35 (Thirty Five)'))
+-def test_determine_rhel_version_unexpected_etc_rh_release_contents(monkeypatch, etc_rh_release_contents):
++@pytest.mark.parametrize(
++ "etc_rh_release_contents",
++ ("", "Red Hat Enterprise Linux Server", "Fedora release 35 (Thirty Five)"),
++)
++def test_determine_distro_version_unexpected_etc_distro_release_contents(monkeypatch, etc_rh_release_contents):
+ iso_mountpoint = '/some/mountpoint'
+
+ def isdir_mocked(path):
+@@ -130,8 +184,9 @@ def test_determine_rhel_version_unexpected_etc_rh_release_contents(monkeypatch,
+ monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
+ monkeypatch.setattr(os, 'listdir', listdir_mocked)
+ monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)
++ monkeypatch.setattr(api, "current_actor", CurrentActorMocked())
+
+- determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
++ determined_version = scan_target_os_iso.determine_os_version_from_iso_mountpoint(iso_mountpoint)
+ assert determined_version == ''
+
+
+@@ -192,7 +247,11 @@ def test_iso_repository_detection(monkeypatch, repodirs_in_iso, expected_repoids
+ monkeypatch.setattr(scan_target_os_iso, 'LoopMount', always_successful_loop_mount)
+ monkeypatch.setattr(os.path, 'exists', mocked_os_path_exits)
+ monkeypatch.setattr(os, 'listdir', mocked_os_listdir)
+- monkeypatch.setattr(scan_target_os_iso, 'determine_rhel_version_from_iso_mountpoint', lambda iso_mountpoint: '7.9')
++ monkeypatch.setattr(
++ scan_target_os_iso,
++ "determine_os_version_from_iso_mountpoint",
++ lambda iso_mountpoint: "7.9",
++ )
+
+ scan_target_os_iso.inform_ipu_about_request_to_use_target_iso()
+
+@@ -214,7 +273,7 @@ def test_iso_repository_detection(monkeypatch, repodirs_in_iso, expected_repoids
+ iso_mountpoint = target_iso.mountpoint
+
+ assert target_iso.was_mounted_successfully
+- assert target_iso.rhel_version == '7.9'
++ assert target_iso.os_version == '7.9'
+
+ expected_repos = {(repoid, 'file://' + os.path.join(iso_mountpoint, repoid)) for repoid in expected_repoids}
+ actual_repos = {(repo.repoid, repo.baseurl) for repo in produced_custom_repo_msgs}
+diff --git a/repos/system_upgrade/common/actors/scanthirdpartytargetpythonmodules/libraries/scanthirdpartytargetpythonmodules.py b/repos/system_upgrade/common/actors/scanthirdpartytargetpythonmodules/libraries/scanthirdpartytargetpythonmodules.py
+index 1329c50f..1b5f3f75 100644
+--- a/repos/system_upgrade/common/actors/scanthirdpartytargetpythonmodules/libraries/scanthirdpartytargetpythonmodules.py
++++ b/repos/system_upgrade/common/actors/scanthirdpartytargetpythonmodules/libraries/scanthirdpartytargetpythonmodules.py
+@@ -7,15 +7,10 @@ import rpm
+
+ from leapp.libraries.common.config.version import get_target_major_version
+ from leapp.libraries.common.rpms import has_package
+-from leapp.libraries.stdlib import api, run
++from leapp.libraries.stdlib import api, format_list, run
+ from leapp.models import DistributionSignedRPM, ThirdPartyTargetPythonModules
+
+ PYTHON_EXTENSIONS = (".py", ".so", ".pyc")
+-FMT_LIST_SEPARATOR = '\n - '
+-
+-
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in input_list]
+
+
+ def get_python_sys_paths(python_interpreter):
+@@ -175,14 +170,14 @@ def process():
+ if third_party_rpms:
+ api.current_logger().info(
+ 'Complete list of third-party RPM packages:{}'.format(
+- ''.join(_formatted_list_output(third_party_rpms))
++ format_list(third_party_rpms)
+ )
+ )
+
+ if all_third_party_files:
+ api.current_logger().info(
+ 'Complete list of third-party Python modules:{}'.format(
+- ''.join(_formatted_list_output(all_third_party_files))
++ format_list(all_third_party_files)
+ )
+ )
+
diff --git a/repos/system_upgrade/common/actors/scanvendorrepofiles/actor.py b/repos/system_upgrade/common/actors/scanvendorrepofiles/actor.py
new file mode 100644
index 00000000..a5e481cb
@@ -4378,6 +6161,113 @@ index 00000000..cb5c7ab7
+ msg = "The {} file exists, but is empty. Nothing to do.".format(scancustomrepofile.CUSTOM_REPO_PATH)
+ assert api.current_logger.infomsg == msg
+ assert not api.produce.called
+diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
+index 8fca5d76..5bf8005c 100644
+--- a/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
++++ b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
+@@ -2,11 +2,9 @@ import os
+
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _report_broken_symlinks(symlinks):
+ summary = (
+@@ -17,8 +15,8 @@ def _report_broken_symlinks(symlinks):
+ ' has not been properly modified.'
+ ' These symlinks will not be handled during the in-place upgrade'
+ ' as they are already broken.'
+- ' The list of detected broken systemd symlinks:{}{}'
+- .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(symlinks)))
++ ' The list of detected broken systemd symlinks:{}'
++ .format(format_list(symlinks))
+ )
+
+ command = ['/usr/bin/rm'] + symlinks
+@@ -44,8 +42,8 @@ def _report_enabled_services_broken_symlinks(symlinks):
+ ' to existing systemd units, but on different paths. This could lead'
+ ' in future to unexpected behaviour. Also, these symlinks will not be'
+ ' handled during the in-place upgrade as they are already broken.'
+- ' The list of detected broken symlinks:{}{}'
+- .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(symlinks)))
++ ' The list of detected broken symlinks:{}'
++ .format(format_list(symlinks))
+ )
+
+ hint = (
+diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
+index 4d1bcda7..e39a4264 100644
+--- a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
++++ b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
+@@ -1,16 +1,14 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import SystemdServicesTasks
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _inhibit_upgrade_with_conflicts(conflicts):
+ summary = (
+ 'The requested states for systemd services on the target system are in conflict.'
+ ' The following systemd services were requested to be both enabled and'
+- ' disabled on the target system:{}{}'
+- .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(conflicts)))
++ ' disabled on the target system:{}'
++ .format(format_list(conflicts))
+ )
+ report = [
+ reporting.Title('Conflicting requirements of systemd service states'),
+diff --git a/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
+index b21fe2b5..d4413ae0 100644
+--- a/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
++++ b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
+@@ -1,7 +1,7 @@
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.config import version
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import (
+ SystemdServicesInfoSource,
+ SystemdServicesInfoTarget,
+@@ -10,8 +10,6 @@ from leapp.models import (
+ SystemdServicesTasks
+ )
+
+-FMT_LIST_SEPARATOR = "\n - "
+-
+
+ def _get_desired_service_state(state_source, preset_source, preset_target):
+ """
+@@ -166,8 +164,8 @@ def _report_kept_enabled(tasks):
+ if tasks.to_enable:
+ summary += (
+ "The following services were originally disabled by preset on the"
+- " upgraded system and Leapp attempted to enable them:{}{}"
+- ).format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(tasks.to_enable)))
++ " upgraded system and Leapp attempted to enable them:{}"
++ ).format(format_list(tasks.to_enable))
+ # TODO(mmatuska): When post-upgrade reports are implemented in
+ # `setsystemdservicesstates actor, add a note here to check the reports
+ # if the enabling failed
+@@ -198,8 +196,8 @@ def _report_newly_enabled(newly_enabled):
+
+ summary = (
+ "The following services were disabled before the upgrade and were set"
+- " to enabled by a systemd preset after the upgrade:{}{}".format(
+- FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(newly_enabled))
++ " to enabled by a systemd preset after the upgrade:{}".format(
++ format_list(newly_enabled)
+ )
+ )
+
diff --git a/repos/system_upgrade/common/actors/systemfacts/actor.py b/repos/system_upgrade/common/actors/systemfacts/actor.py
index 59b12c87..85d4a09e 100644
--- a/repos/system_upgrade/common/actors/systemfacts/actor.py
@@ -4391,6 +6281,178 @@ index 59b12c87..85d4a09e 100644
def process(self):
self.produce(systemfacts.get_sysctls_status())
+diff --git a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
+index 578f96cb..f46592c3 100644
+--- a/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
++++ b/repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py
+@@ -315,20 +315,40 @@ def get_firewalls_status():
+
+
+ def _get_secure_boot_state():
++ """Determine the Secure Boot state using mokutil.
++
++ :returns: A tuple of (secureboot_enabled, efi_vars_accessible) where:
++
++ - *secureboot_enabled*:
++ - ``True`` -- Secure Boot is enabled.
++ - ``False`` -- mokutil is not available (OSError); Secure Boot state is unknown.
++ - ``None`` -- the system does not support Secure Boot, or EFI variables are inaccessible.
++ - *efi_vars_accessible*:
++ - ``True`` -- EFI variables are accessible (mokutil ran successfully or reported
++ unsupported Secure Boot).
++ - ``False`` -- EFI variables are not accessible.
++ - ``None`` -- mokutil is not available; EFI variable accessibility is unknown.
++ :rtype: tuple(bool or None, bool or None)
++ :raises StopActorExecutionError: When mokutil fails for an unexpected reason.
++ """
+ try:
+ stdout = run(['mokutil', '--sb-state'])['stdout']
+- return 'enabled' in stdout
++ return ('enabled' in stdout, True)
+ except CalledProcessError as e:
+ if "doesn't support Secure Boot" in e.stderr:
+- return None
++ return (None, True)
++ if "EFI variables are not supported" in e.stderr:
++ api.current_logger().warning(
++ 'EFI variables are not accessible: %s', e.stderr
++ )
++ return (None, False)
+
+ raise StopActorExecutionError('Failed to determine SecureBoot state: {}'.format(e))
+ except OSError as e:
+- # shim depends on mokutil, if it's not installed assume SecureBoot is disabled
+ api.current_logger().debug(
+ 'Failed to execute mokutil, assuming SecureBoot is disabled: {}'.format(e)
+ )
+- return False
++ return (False, None)
+
+
+ def get_firmware():
+@@ -339,10 +359,16 @@ def get_firmware():
+ ppc64le_opal = os.path.isdir('/sys/firmware/opal/')
+
+ is_secureboot = None
++ efi_vars_accessible = None
+ if firmware == 'efi':
+- is_secureboot = _get_secure_boot_state()
++ is_secureboot, efi_vars_accessible = _get_secure_boot_state()
+
+- return FirmwareFacts(firmware=firmware, ppc64le_opal=ppc64le_opal, secureboot_enabled=is_secureboot)
++ return FirmwareFacts(
++ firmware=firmware,
++ ppc64le_opal=ppc64le_opal,
++ secureboot_enabled=is_secureboot,
++ efi_vars_accessible=efi_vars_accessible,
++ )
+
+
+ @aslist
+diff --git a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts.py b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts.py
+index 22ee7b7b..14a8039b 100644
+--- a/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts.py
++++ b/repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts.py
+@@ -156,7 +156,7 @@ def test_get_secure_boot_state_ok(mocked_run: mock.MagicMock, is_enabled):
+
+ out = _get_secure_boot_state()
+
+- assert out == is_enabled
++ assert out == (is_enabled, True)
+ mocked_run.assert_called_once_with(['mokutil', '--sb-state'])
+
+
+@@ -166,7 +166,7 @@ def test_get_secure_boot_state_no_mokutil(mocked_run: mock.MagicMock):
+
+ out = _get_secure_boot_state()
+
+- assert out is False
++ assert out == (False, None)
+ mocked_run.assert_called_once_with(['mokutil', '--sb-state'])
+
+
+@@ -185,12 +185,12 @@ def test_get_secure_boot_state_not_supported(mocked_run: mock.MagicMock):
+
+ out = _get_secure_boot_state()
+
+- assert out is None
++ assert out == (None, True)
+ mocked_run.assert_called_once_with(cmd)
+
+
+ @mock.patch('leapp.libraries.actor.systemfacts.run')
+-def test_get_secure_boot_state_failed(mocked_run: mock.MagicMock):
++def test_get_secure_boot_state_efi_vars_unavailable(mocked_run: mock.MagicMock):
+ cmd = ['mokutil', '--sb-state']
+ result = {
+ 'stderr': 'EFI variables are not supported on this system',
+@@ -202,6 +202,25 @@ def test_get_secure_boot_state_failed(mocked_run: mock.MagicMock):
+ result
+ )
+
++ out = _get_secure_boot_state()
++
++ assert out == (None, False)
++ mocked_run.assert_called_once_with(cmd)
++
++
++@mock.patch('leapp.libraries.actor.systemfacts.run')
++def test_get_secure_boot_state_failed(mocked_run: mock.MagicMock):
++ cmd = ['mokutil', '--sb-state']
++ result = {
++ 'stderr': 'Unexpected mokutil error',
++ 'exit_code': 1,
++ }
++ mocked_run.side_effect = CalledProcessError(
++ "Command mokutil --sb-state failed with exit code 1.",
++ cmd,
++ result
++ )
++
+ with pytest.raises(
+ StopActorExecutionError,
+ match='Failed to determine SecureBoot state'
+@@ -211,11 +230,12 @@ def test_get_secure_boot_state_failed(mocked_run: mock.MagicMock):
+ mocked_run.assert_called_once_with(cmd)
+
+
+-def _ff(firmware, ppc64le_opal, is_secureboot):
++def _ff(firmware, ppc64le_opal, is_secureboot, efi_vars_accessible=None):
+ return FirmwareFacts(
+ firmware=firmware,
+ ppc64le_opal=ppc64le_opal,
+- secureboot_enabled=is_secureboot
++ secureboot_enabled=is_secureboot,
++ efi_vars_accessible=efi_vars_accessible,
+ )
+
+
+@@ -223,17 +243,19 @@ def _ff(firmware, ppc64le_opal, is_secureboot):
+ "has_sys_efi, has_sys_opal, is_ppc, secboot_state, expect",
+ [
+ # 1. Standard BIOS on x86
+- (False, False, False, None, _ff("bios", None, None)),
++ (False, False, False, None, _ff("bios", None, None, None)),
+ # 2. EFI on x86 with Secure Boot Enabled
+- (True, False, False, True, _ff("efi", None, True)),
++ (True, False, False, (True, True), _ff("efi", None, True, True)),
+ # 3. EFI on x86 with Secure Boot Disabled
+- (True, False, False, False, _ff("efi", None, False)),
++ (True, False, False, (False, True), _ff("efi", None, False, True)),
+ # 4. PPC64LE with OPAL (No EFI)
+- (False, True, True, None, _ff("bios", True, None)),
++ (False, True, True, None, _ff("bios", True, None, None)),
+ # 5. PPC64LE without OPAL (No EFI)
+- (False, False, True, None, _ff("bios", False, None)),
++ (False, False, True, None, _ff("bios", False, None, None)),
+ # 6. EFI on PPC64LE with OPAL
+- (True, True, True, True, _ff("efi", True, True)),
++ (True, True, True, (True, True), _ff("efi", True, True, True)),
++ # 7. EFI on x86 with EFI vars unavailable
++ (True, False, False, (None, False), _ff("efi", None, None, False)),
+ ]
+ )
+ def test_get_firmware_logic(
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/actor.py b/repos/system_upgrade/common/actors/targetcontentresolver/actor.py
index e132cc3a..ade95104 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/actor.py
@@ -4594,6 +6656,28 @@ index 2ea63f72..6a6ad698 100644
api.produce(rpm_tasks)
return pes_requested_repoids
+diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+index c77a1f13..d54953fa 100644
+--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
++++ b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/repositoriesblocklist.py
+@@ -1,7 +1,7 @@
+ from leapp import reporting
+ from leapp.libraries.common.config import get_target_distro_id
+ from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import RepositoriesBlocklisted
+
+
+@@ -15,7 +15,7 @@ def _report_excluded_repos(repos):
+ reporting.Summary(
+ 'The following repositories are not supported by '
+ 'Red Hat and are excluded from the list of repositories '
+- 'used during the upgrade.\n- {}'.format('\n- '.join(repos))
++ 'used during the upgrade.{}'.format(format_list(repos))
+ ),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
diff --git a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py b/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
index 4896bf9b..acd7b0bc 100644
--- a/repos/system_upgrade/common/actors/targetcontentresolver/libraries/setuptargetrepos.py
@@ -4725,18 +6809,28 @@ index 0ee05ee2..88ae5ed8 100644
cloud_provider = rhui_info.provider if rhui_info else ''
return RepoMapDataHandler(repomap_msg, cloud_provider=cloud_provider)
diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
-index a776deac..03bc933b 100644
+index a776deac..44952205 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
+++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py
-@@ -70,6 +70,7 @@ PROD_CERTS_FOLDER = 'prod-certs'
+@@ -21,7 +21,7 @@ from leapp.libraries.common.config.version import (
+ )
+ 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.libraries.stdlib import api, CalledProcessError, config, format_list, run
+ from leapp.models import RequiredTargetUserspacePackages # deprecated
+ from leapp.models import TMPTargetRepositoriesFacts # deprecated all the time
+ from leapp.models import (
+@@ -69,7 +69,7 @@ from leapp.utils.deprecation import suppress_deprecation
+ PROD_CERTS_FOLDER = 'prod-certs'
PERSISTENT_PACKAGE_CACHE_DIR = '/var/lib/leapp/persistent_package_cache'
DEDICATED_LEAPP_PART_URL = 'https://access.redhat.com/solutions/7011704'
- FMT_LIST_SEPARATOR = '\n - '
+-FMT_LIST_SEPARATOR = '\n - '
+CUSTOM_ROOTFS_PATH = '/etc/leapp/files/rootfs'
def _check_deprecated_rhsm_skip():
-@@ -166,9 +167,10 @@ def _import_gpg_keys(context, install_root_dir, target_major_version):
+@@ -166,9 +166,10 @@ def _import_gpg_keys(context, install_root_dir, target_major_version):
# installation of initial packages
try:
# Import also any other keys provided by the customer in the same directory
@@ -4750,7 +6844,7 @@ index a776deac..03bc933b 100644
except CalledProcessError as exc:
raise StopActorExecutionError(
message=(
-@@ -216,6 +218,27 @@ def prepare_target_userspace(context, userspace_dir, enabled_repos, packages):
+@@ -216,6 +217,27 @@ def prepare_target_userspace(context, userspace_dir, enabled_repos, packages):
run(['rm', '-rf', userspace_dir])
_create_target_userspace_directories(userspace_dir)
@@ -4778,7 +6872,7 @@ index a776deac..03bc933b 100644
target_major_version = get_target_major_version()
install_root_dir = '/el{}target'.format(target_major_version)
with mounting.BindMount(source=userspace_dir, target=os.path.join(context.base_dir, install_root_dir.lstrip('/'))):
-@@ -649,9 +672,10 @@ def _prep_repository_access(context, target_userspace):
+@@ -649,9 +671,10 @@ def _prep_repository_access(context, target_userspace):
# NOTE(dkubek): context.call(['update-ca-trust']) seems to not be working.
# I am not really sure why. The changes to files are not
# being written to disk.
@@ -4790,7 +6884,7 @@ index a776deac..03bc933b 100644
run(['rm', '-rf', os.path.join(target_etc, 'rhsm')])
context.copytree_from('/etc/rhsm', os.path.join(target_etc, 'rhsm'))
-@@ -773,6 +797,70 @@ def _create_target_userspace_directories(target_userspace):
+@@ -773,6 +796,70 @@ def _create_target_userspace_directories(target_userspace):
)
@@ -4861,7 +6955,30 @@ index a776deac..03bc933b 100644
def _inhibit_on_duplicate_repos(repofiles):
"""
Inhibit the upgrade if any repoid is defined multiple times.
-@@ -975,8 +1063,8 @@ def _get_distro_available_repoids(context, indata):
+@@ -786,18 +873,17 @@ def _inhibit_on_duplicate_repos(repofiles):
+
+ if not duplicates:
+ return
+- list_separator_fmt = '\n - '
+ api.current_logger().warning(
+- 'The following repoids are defined multiple times:{0}{1}'
+- .format(list_separator_fmt, list_separator_fmt.join(sorted(duplicates)))
++ 'The following repoids are defined multiple times:{}'
++ .format(format_list(duplicates))
+ )
+
+ reporting.create_report([
+ reporting.Title('A YUM/DNF repository defined multiple times'),
+ reporting.Summary(
+ 'The following repositories are defined multiple times inside the'
+- ' "upgrade" container:{0}{1}'
+- .format(list_separator_fmt, list_separator_fmt.join(sorted(duplicates)))
++ ' "upgrade" container:{}'
++ .format(format_list(duplicates))
+ ),
+ reporting.Severity(reporting.Severity.MEDIUM),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
+@@ -975,8 +1061,8 @@ def _get_distro_available_repoids(context, indata):
provider has itw own rpm).
On other: Repositories are provided in specific repofiles (e.g. centos.repo
and centos-addons.repo on CS)
@@ -4872,7 +6989,7 @@ index a776deac..03bc933b 100644
Conversions: Only custom repos - no distro repoids (all distros)
:return: A set of repoids provided by distribution
-@@ -988,10 +1076,14 @@ def _get_distro_available_repoids(context, indata):
+@@ -988,10 +1074,14 @@ def _get_distro_available_repoids(context, indata):
is_source_cs8 = (
get_source_distro_id() == "centos" and get_source_major_version() == '8'
)
@@ -4888,6 +7005,30 @@ index a776deac..03bc933b 100644
and (target_distro != "rhel" or rhel_and_rhsm)
):
_inhibit_if_no_base_repos(distro_repoids)
+@@ -1027,10 +1117,9 @@ def gather_target_repositories(context, indata):
+ distro_repoids = _get_distro_available_repoids(context, indata)
+ if distro_repoids:
+ api.current_logger().info(
+- "The following repoids are considered as provided by the '{}' distribution:{}{}".format(
++ "The following repoids are considered as provided by the '{}' distribution:{}".format(
+ get_target_distro_id(),
+- FMT_LIST_SEPARATOR,
+- FMT_LIST_SEPARATOR.join(sorted(distro_repoids)),
++ format_list(distro_repoids),
+ )
+ )
+ else:
+@@ -1110,8 +1199,8 @@ def gather_target_repositories(context, indata):
+ '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.\n'
+- 'The following repositories IDs could not be found in the target configuration:\n'
+- '- {}\n'.format('\n- '.join(sorted(missing_custom_repoids)))
++ 'The following repositories IDs could not be found in the target configuration:{}'
++ .format(format_list(missing_custom_repoids))
+ ),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
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 b49eff56..fa1a9e7f 100644
--- a/repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py
@@ -4929,6 +7070,26 @@ index 6377f767..4c5420f6 100644
return pubkeys
+diff --git a/repos/system_upgrade/common/actors/unsupportedupgradecheck/actor.py b/repos/system_upgrade/common/actors/unsupportedupgradecheck/actor.py
+index e8b3499a..fbbfde21 100644
+--- a/repos/system_upgrade/common/actors/unsupportedupgradecheck/actor.py
++++ b/repos/system_upgrade/common/actors/unsupportedupgradecheck/actor.py
+@@ -1,5 +1,6 @@
+ from leapp import reporting
+ from leapp.actors import Actor
++from leapp.libraries.stdlib import format_list
+ from leapp.models import Report
+ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+@@ -46,7 +47,7 @@ class UnsupportedUpgradeCheck(Actor):
+ 'guaranteed and the upgrade is unsupported.\n'
+ 'You can bypass this error by setting the LEAPP_UNSUPPORTED variable but by doing so, '
+ 'you continue at your own risk.\n'
+- 'Found development variables:\n- {}\n'.format('\n- '.join([v.name for v in devel_vars]))
++ 'Found development variables:{}'.format(format_list([v.name for v in devel_vars]))
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
diff --git a/repos/system_upgrade/common/actors/vendorreposignaturescanner/actor.py b/repos/system_upgrade/common/actors/vendorreposignaturescanner/actor.py
new file mode 100644
index 00000000..dbf86974
@@ -5360,10 +7521,17 @@ index 80f26839..b12da240 100644
"8": ["9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "9.6", "9.7", "9.8", "9.9"],
"9": ["10.0", "10.1", "10.2", "10.3"]
diff --git a/repos/system_upgrade/common/libraries/distro.py b/repos/system_upgrade/common/libraries/distro.py
-index ec3367af..4cbe090c 100644
+index ec3367af..83cf0ede 100644
--- a/repos/system_upgrade/common/libraries/distro.py
+++ b/repos/system_upgrade/common/libraries/distro.py
-@@ -8,6 +8,7 @@ from leapp.libraries.common.config import get_source_distro_id, get_target_distr
+@@ -1,5 +1,6 @@
+ import json
+ import os
++from collections import namedtuple
+ from collections.abc import Mapping
+
+ from leapp.exceptions import StopActorExecutionError
+@@ -8,6 +9,7 @@ from leapp.libraries.common.config import get_source_distro_id, get_target_distr
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
@@ -5371,7 +7539,7 @@ index ec3367af..4cbe090c 100644
def get_distribution_data(distribution):
-@@ -16,12 +17,19 @@ def get_distribution_data(distribution):
+@@ -16,12 +18,19 @@ def get_distribution_data(distribution):
distribution_config = os.path.join(distributions_path, distribution, 'gpg-signatures.json')
if os.path.exists(distribution_config):
with open(distribution_config) as distro_config_file:
@@ -5392,7 +7560,7 @@ index ec3367af..4cbe090c 100644
# distro -> major_version -> repofile -> tuple of architectures where it's present
_DISTRO_REPOFILES_MAP = {
-@@ -349,4 +357,10 @@ def get_distro_efidir_canon_path(distro_id):
+@@ -349,4 +358,25 @@ def get_distro_efidir_canon_path(distro_id):
if distro_id == "rhel":
return os.path.join(efi.EFI_MOUNTPOINT, "EFI", "redhat")
@@ -5403,6 +7571,21 @@ index ec3367af..4cbe090c 100644
+ return os.path.join(efi.EFI_MOUNTPOINT, "EFI", "centos")
+
return os.path.join(efi.EFI_MOUNTPOINT, "EFI", distro_id)
++
++
++DistroIsoConfig = namedtuple('DistroIsoConfig', ('release_pkg_name_prefix', 'etc_release_file'))
++"""
++Holds distro-specific information about ISO images.
++"""
++
++
++def get_distro_iso_config(distro_id):
++ return {
++ "rhel": DistroIsoConfig("redhat-release", "/etc/redhat-release"),
++ "centos": DistroIsoConfig("centos-stream-release", "/etc/centos-release"),
++ "almalinux": DistroIsoConfig("almalinux-release", "/etc/almalinux-release"),
++ "rocky": DistroIsoConfig("rocky-release", "/etc/rocky-release"),
++ }[distro_id]
diff --git a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py b/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
index cb810a2a..bd9e4596 100644
--- a/repos/system_upgrade/common/libraries/dnflibs/dnfplugin.py
@@ -5621,6 +7804,54 @@ index 00000000..2c2c9c94
+ )
+
+ return combined_repomapping
+diff --git a/repos/system_upgrade/common/libraries/rhsm.py b/repos/system_upgrade/common/libraries/rhsm.py
+index 3c7b7499..a55a8d35 100644
+--- a/repos/system_upgrade/common/libraries/rhsm.py
++++ b/repos/system_upgrade/common/libraries/rhsm.py
+@@ -8,7 +8,7 @@ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common import repofileutils
+ from leapp.libraries.common.config import get_env, get_target_distro_id
+-from leapp.libraries.stdlib import api, CalledProcessError
++from leapp.libraries.stdlib import api, CalledProcessError, format_list
+ from leapp.models import RHSMInfo
+
+ _RE_REPO_UID = re.compile(r'Repo ID:\s*([^\s]+)')
+@@ -195,10 +195,9 @@ def get_available_repo_ids(context):
+ rhsm_repos.sort()
+ break
+
+- list_separator_fmt = '\n - '
+ if rhsm_repos:
+- api.current_logger().info('The following repoids are available through RHSM:{0}{1}'
+- .format(list_separator_fmt, list_separator_fmt.join(rhsm_repos)))
++ api.current_logger().info('The following repoids are available through RHSM:{}'
++ .format(format_list(rhsm_repos)))
+ else:
+ api.current_logger().info('There are no repos available through RHSM.')
+ return rhsm_repos
+@@ -215,17 +214,16 @@ def _inhibit_on_duplicate_repos(repofiles):
+
+ if not duplicates:
+ return
+- list_separator_fmt = '\n - '
+ api.current_logger().warning(
+- 'The following repoids are defined multiple times:{0}{1}'
+- .format(list_separator_fmt, list_separator_fmt.join(duplicates))
++ 'The following repoids are defined multiple times:{}'
++ .format(format_list(duplicates))
+ )
+
+ reporting.create_report([
+ reporting.Title('A YUM/DNF repository defined multiple times'),
+ reporting.Summary(
+- 'The following repositories are defined multiple times:{0}{1}'
+- .format(list_separator_fmt, list_separator_fmt.join(duplicates))
++ 'The following repositories are defined multiple times:{}'
++ .format(format_list(duplicates))
+ ),
+ reporting.Severity(reporting.Severity.MEDIUM),
+ reporting.Groups([reporting.Groups.REPOSITORY]),
diff --git a/repos/system_upgrade/common/models/activevendorlist.py b/repos/system_upgrade/common/models/activevendorlist.py
new file mode 100644
index 00000000..de4056fb
@@ -5634,6 +7865,36 @@ index 00000000..de4056fb
+class ActiveVendorList(Model):
+ topic = VendorTopic
+ data = fields.List(fields.String())
+diff --git a/repos/system_upgrade/common/models/firmwarefacts.py b/repos/system_upgrade/common/models/firmwarefacts.py
+index 9627babd..6c3d11f7 100644
+--- a/repos/system_upgrade/common/models/firmwarefacts.py
++++ b/repos/system_upgrade/common/models/firmwarefacts.py
+@@ -13,8 +13,22 @@ class FirmwareFacts(Model):
+
+ secureboot_enabled = fields.Nullable(fields.Boolean())
+ """
+- Check whether SecureBoot is enabled, always False on BIOS systems
++ Check whether SecureBoot is enabled.
+
+- Note that some machines do not support SecureBoot at all - even for systems booted with UEFI.
+- For systems booted with UEFI that does not support SecureBoot set None.
++ The value can be None in these cases:
++ * on BIOS systems (mokutil is never called)
++ * on systems that do not support SecureBoot (even when booted with UEFI)
++ * on systems with disabled EFI variables (usually on Real Time systems due to effect on latency)
++ """
++
++ efi_vars_accessible = fields.Nullable(fields.Boolean())
++ """
++ True if EFI runtime variables are accessible via mokutil.
++
++ Checking this value is useful on systems booted with UEFI when the information about
++ the Secure Boot settings is not determined (`secureboot_enabled` is None).
++ Other values:
++
++ * False if mokutil fails with "EFI variables are not supported".
++ * None for BIOS systems or when mokutil is not installed.
+ """
diff --git a/repos/system_upgrade/common/models/repositoriesmap.py b/repos/system_upgrade/common/models/repositoriesmap.py
index 842cd807..17bd6bc9 100644
--- a/repos/system_upgrade/common/models/repositoriesmap.py
@@ -5693,6 +7954,38 @@ index 3043b499..b66794f6 100644
class TargetRepositories(Model):
"""
Repositories supposed to be used during the IPU process
+diff --git a/repos/system_upgrade/common/models/upgradeiso.py b/repos/system_upgrade/common/models/upgradeiso.py
+index da612bec..631a08ea 100644
+--- a/repos/system_upgrade/common/models/upgradeiso.py
++++ b/repos/system_upgrade/common/models/upgradeiso.py
+@@ -5,10 +5,27 @@ from leapp.topics import SystemFactsTopic
+ class TargetOSInstallationImage(Model):
+ """
+ An installation image of a target OS requested to be the source of target OS packages.
++
++ Note: `rhel_version` is deprecated, use `os_version` instead.
+ """
+ topic = SystemFactsTopic
+ path = fields.String()
+ mountpoint = fields.String()
+ repositories = fields.List(fields.Model(CustomTargetRepository))
+ rhel_version = fields.String(default='')
++ """
++ The RHEL version provided by the ISO
++
++ DEPRECATED - use os_version instead.
++ """
++
++ os_version = fields.String(default='')
++ """
++ The OS version provided by the ISO
++
++ The version is the full version available in /etc/-release,
++ i.e. it is in the versioning schema used by the distribution, which is
++ usually MAJOR.MINOR except for CentOS Stream where it's MAJOR.
++ """
++
+ was_mounted_successfully = fields.Boolean(default=False)
diff --git a/repos/system_upgrade/common/models/vendorsignatures.py b/repos/system_upgrade/common/models/vendorsignatures.py
new file mode 100644
index 00000000..f456aec5
@@ -5736,3 +8029,556 @@ index 00000000..014b7afb
+
+class VendorTopic(Topic):
+ name = 'vendor_topic'
+diff --git a/repos/system_upgrade/el8toel9/actors/checkdeprecatedrpmsignature/libraries/checkdeprecatedrpmsignature.py b/repos/system_upgrade/el8toel9/actors/checkdeprecatedrpmsignature/libraries/checkdeprecatedrpmsignature.py
+index 3e8bdbe7..f4448157 100644
+--- a/repos/system_upgrade/el8toel9/actors/checkdeprecatedrpmsignature/libraries/checkdeprecatedrpmsignature.py
++++ b/repos/system_upgrade/el8toel9/actors/checkdeprecatedrpmsignature/libraries/checkdeprecatedrpmsignature.py
+@@ -1,10 +1,8 @@
+ from leapp import reporting
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import CryptoPolicyInfo, InstalledRPM
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+ # FIXME(pstodulk): Adding the links to the summary as information inside has
+ # serious impact. This will create a duplication of links for Satellite and
+ # Cockpit UI when reading the report, but we need to be sure they are printed
+@@ -62,10 +60,9 @@ def process():
+ bad_rpms = _get_rpms_with_sha1_sig()
+ cpi = next(api.consume(CryptoPolicyInfo), None)
+ if bad_rpms:
+- bad_rpms_str = ''.join([
+- '{prefix}{pkgname} ({sig})'.format(prefix=FMT_LIST_SEPARATOR, pkgname=pkg.name, sig=pkg.pgpsig)
+- for pkg in bad_rpms
+- ])
++ bad_rpms_str = format_list(
++ ['{} ({})'.format(pkg.name, pkg.pgpsig) for pkg in bad_rpms]
++ )
+ report = [
+ reporting.Title('Detected RPMs with RSA/SHA1 signature'),
+ reporting.Summary(SUMMARY_FMT.format(
+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 79ede81e..22339f2e 100644
+--- a/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py
++++ b/repos/system_upgrade/el8toel9/actors/checkifcfg/libraries/checkifcfg_ifcfg.py
+@@ -3,17 +3,9 @@ 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.libraries.stdlib import api, format_list
+ 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']
+@@ -87,7 +79,7 @@ def process():
+ " 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),
++ bad_files=format_list(bad_type_files),
+ **DISTRO_REPORT_NAMES,
+ )
+ )
+@@ -121,7 +113,7 @@ def process():
+ ' prohibit NetworkManager from loading it.'
+ ' Files with the problematic configuration:{bad_files}'
+ ).format(
+- bad_files=_format_files_list(not_controlled_files),
++ bad_files=format_list(not_controlled_files),
+ **DISTRO_REPORT_NAMES,
+ )
+ remediation = ('Ensure the ifcfg files comply with format described in'
+diff --git a/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py b/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
+index d4f2106b..ff2b4f38 100644
+--- a/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
++++ b/repos/system_upgrade/el8toel9/actors/networkdeprecations/libraries/networkdeprecations.py
+@@ -1,9 +1,7 @@
+ from leapp import reporting
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import IfCfg, NetworkManagerConnection, SystemdServicesInfoSource
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def process():
+ wep_files = []
+@@ -59,7 +57,7 @@ def process():
+ ' that use the phased out WEP algorithm is present in the system'
+ ' and will not work after the upgrade.'
+ ' Files with the problematic configuration:{}').format(
+- ''.join(['{}{}'.format(FMT_LIST_SEPARATOR, bfile) for bfile in wep_files])
++ format_list(wep_files)
+ )
+ remediation = ('Remove configuration for networks that use WEP or'
+ ' upgrade the networks to use more secure encryption'
+@@ -87,7 +85,7 @@ def process():
+ ' 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])
++ format_list(nm_controlled_files)
+ )
+ remediation = (
+ 'Either enable the NetworkManager service before upgrading, or add'
+diff --git a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
+index 5014a8db..e97ebea0 100644
+--- a/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
++++ b/repos/system_upgrade/el8toel9/actors/rocecheck/libraries/rocecheck.py
+@@ -2,10 +2,9 @@ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ 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.libraries.stdlib import api, format_list
+ from leapp.models import KernelCmdline, RoceDetected
+
+-FMT_LIST_SEPARATOR = '\n - {}'
+ DOC_URL = 'https://red.ht/predictable-network-interface-device-names-on-the-system-z-platform'
+
+
+@@ -34,10 +33,6 @@ def is_kernel_arg_set():
+ return False
+
+
+-def _fmt_list(items):
+- return ''.join([FMT_LIST_SEPARATOR.format(i) for i in items])
+-
+-
+ def _report_wrong_setup(roce):
+ roce_nics = roce.roce_nics_connected + roce.roce_nics_connecting
+
+@@ -50,7 +45,7 @@ def _report_wrong_setup(roce):
+ ' For more information, see: {url}'
+ '\n\nRoCE detected on the following NICs:{nics}'
+ ).format(
+- nics=_fmt_list(roce_nics),
++ nics=format_list(roce_nics),
+ url=DOC_URL,
+ **DISTRO_REPORT_NAMES,
+ )
+diff --git a/repos/system_upgrade/el8toel9/actors/xorgdrvcheck/actor.py b/repos/system_upgrade/el8toel9/actors/xorgdrvcheck/actor.py
+index 2531e4c7..b9df0a82 100644
+--- a/repos/system_upgrade/el8toel9/actors/xorgdrvcheck/actor.py
++++ b/repos/system_upgrade/el8toel9/actors/xorgdrvcheck/actor.py
+@@ -1,5 +1,6 @@
+ from leapp import reporting
+ from leapp.actors import Actor
++from leapp.libraries.stdlib import format_list
+ from leapp.models import XorgDrvFacts
+ from leapp.reporting import create_report, Report
+ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+@@ -8,7 +9,7 @@ SUMMARY_XORG_DEPRECATE_DRIVERS_FMT = (
+ 'Leapp has detected the use of some deprecated Xorg drivers. '
+ 'Using these drivers could lead to a broken graphical session after the upgrade. '
+ 'Any custom configuration related to these drivers will be ignored. '
+- 'The list of used deprecated drivers: {}')
++ 'The list of used deprecated drivers:{}')
+
+ SUMMARY_XORG_DEPRECATE_DRIVERS_HINT = (
+ 'Please uninstall the Xorg driver and remove the corresponding driver '
+@@ -16,17 +17,17 @@ SUMMARY_XORG_DEPRECATE_DRIVERS_HINT = (
+ 'such as `/etc/X11/xorg.conf` and `/etc/X11/xorg.conf.d/` and reboot before '
+ 'upgrading to make sure you have a graphical session after upgrading.'
+ )
+-FMT_LIST_SEPARATOR = '\n - {}'
+
+
+ def _printable_drv(facts):
+- output = ''
++ items = []
+ for fact in facts:
+ for driver in fact.xorg_drivers:
+- output += FMT_LIST_SEPARATOR.format(driver.driver)
++ item = driver.driver
+ if driver.has_options:
+- output += ' (with custom driver options)'
+- return output
++ item += ' (with custom driver options)'
++ items.append(item)
++ return format_list(items, callback_sort=None)
+
+
+ class XorgDrvCheck8to9(Actor):
+diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py
+index 8b35744b..5ca41a13 100644
+--- a/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py
++++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py
+@@ -1,15 +1,9 @@
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import XFSInfoFacts
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+-
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in input_list]
+-
+
+ def process():
+ xfs_info_facts = _get_xfs_info_facts()
+@@ -78,7 +72,7 @@ def _report_bigtime(invalid_bigtime):
+ ' failures.'
+ ' Following XFS file systems have not enabled the "bigtime" feature:{fs_list}'.format(
+ distro=DISTRO_REPORT_NAMES.target,
+- fs_list=''.join(_formatted_list_output(invalid_bigtime))
++ fs_list=format_list(invalid_bigtime)
+ )
+ )
+
+@@ -116,7 +110,7 @@ def _inhibit_crc(invalid_crc):
+ ' the target kernel. Such filesystems cannot be mounted by target'
+ ' system kernel and so the upgrade cannot proceed successfully.'
+ ' Following XFS filesystems have v4 format:{}'
+- .format(''.join(_formatted_list_output(invalid_crc)))
++ .format(format_list(invalid_crc))
+ )
+ remediation_hint = (
+ 'Migrate XFS v4 filesystems to new XFS v5 format.'
+diff --git a/repos/system_upgrade/el9toel10/actors/krb5conf/checkkrb5conf/libraries/checkkrb5conf.py b/repos/system_upgrade/el9toel10/actors/krb5conf/checkkrb5conf/libraries/checkkrb5conf.py
+index 406141cd..afb8a16d 100644
+--- a/repos/system_upgrade/el9toel10/actors/krb5conf/checkkrb5conf/libraries/checkkrb5conf.py
++++ b/repos/system_upgrade/el9toel10/actors/krb5conf/checkkrb5conf/libraries/checkkrb5conf.py
+@@ -1,17 +1,9 @@
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import OutdatedKrb5conf
+
+-FMT_LIST_SEPARATOR = "\n - "
+-
+-
+-def __human_readable_list(unmanaged_files):
+- if unmanaged_files:
+- return FMT_LIST_SEPARATOR + FMT_LIST_SEPARATOR.join(unmanaged_files)
+- return ''
+-
+
+ def process():
+ msg = next(api.consume(OutdatedKrb5conf), None)
+@@ -27,7 +19,7 @@ def process():
+ 'the location of the reference X.509 CA bundle '
+ 'file was modified. The following unmanaged MIT krb5 '
+ 'configuration files have to be updated to point to the new '
+- 'bundle file:' + __human_readable_list(msg.unmanaged_files)),
++ 'bundle file:' + format_list(msg.unmanaged_files)),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.SECURITY, reporting.Groups.AUTHENTICATION])
+ ])
+@@ -46,7 +38,7 @@ def process():
+ 'RPMs were updated to reflect this change, or you may be '
+ 'unable to complete Kerberos PKINIT pre-authentication (e.g. '
+ 'using user certificates, or smartcards). The following files '
+- 'are affected:' + __human_readable_list(file_paths_from_rpm)),
++ 'are affected:' + format_list(file_paths_from_rpm)),
+ reporting.Severity(reporting.Severity.MEDIUM),
+ reporting.Groups([reporting.Groups.SECURITY, reporting.Groups.AUTHENTICATION])
+ ])
+diff --git a/repos/system_upgrade/el9toel10/actors/mysql/checkmysql/libraries/checkmysql.py b/repos/system_upgrade/el9toel10/actors/mysql/checkmysql/libraries/checkmysql.py
+index 19a36354..b3e3007c 100644
+--- a/repos/system_upgrade/el9toel10/actors/mysql/checkmysql/libraries/checkmysql.py
++++ b/repos/system_upgrade/el9toel10/actors/mysql/checkmysql/libraries/checkmysql.py
+@@ -3,23 +3,17 @@ from typing import TYPE_CHECKING
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+
+ if TYPE_CHECKING:
+ from repos.system_upgrade.el9toel10.models.mysql import MySQLConfiguration
+ else:
+ from leapp.models import MySQLConfiguration
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+ # Link URL for mysql-server report
+ REPORT_SERVER_INST_LINK_URL = 'https://access.redhat.com/articles/7099234'
+
+
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in input_list]
+-
+-
+ def _generate_mysql_present_report() -> None:
+ """
+ Create report on mysql-server package installation detection.
+@@ -73,7 +67,7 @@ def _generate_deprecated_config_report(found_options: list,
+ summary_list.append(
+ 'Following incompatible configuration options have been detected:{}'
+ '\nDefault configuration file is present at `/etc/my.cnf`'
+- .format(''.join(_formatted_list_output(found_options)))
++ .format(format_list(found_options, callback_sort=None))
+ )
+ remedy_list.append('Drop all deprecated configuration options before the upgrade.')
+
+@@ -83,7 +77,7 @@ def _generate_deprecated_config_report(found_options: list,
+ 'will not work with the new MySQL after upgrading:{}\n'
+ 'Default service override file is present at '
+ '`/etc/systemd/system/mysqld.service.d/override.conf`'
+- .format(''.join(_formatted_list_output(found_arguments)))
++ .format(format_list(found_arguments, callback_sort=None))
+ )
+ remedy_list.append(
+ 'Drop all detected problematic startup arguments from '
+@@ -108,7 +102,7 @@ def _generate_deprecated_config_report(found_options: list,
+ reporting.RelatedResource('file', '/etc/systemd/system/mysqld.service.d/override.conf'),
+ reporting.Remediation(hint=(
+ 'To ensure smooth upgrade process it is strongly recommended to:{}'
+- .format(''.join(_formatted_list_output(remedy_list)))
++ .format(format_list(remedy_list, callback_sort=None))
+ )),
+ ])
+
+diff --git a/repos/system_upgrade/el9toel10/actors/networkdeprecations/actor.py b/repos/system_upgrade/el9toel10/actors/networkdeprecations/actor.py
+index 337ea915..52903a1a 100644
+--- a/repos/system_upgrade/el9toel10/actors/networkdeprecations/actor.py
++++ b/repos/system_upgrade/el9toel10/actors/networkdeprecations/actor.py
+@@ -3,15 +3,10 @@ import os
+ from leapp import reporting
+ from leapp.actors import Actor
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
++from leapp.libraries.stdlib import format_list
+ from leapp.models import IfCfg, NetworkManagerConfig, Report
+ from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+-
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in sorted(input_list)]
+-
+
+ class CheckNetworkDeprecations9to10(Actor):
+ """
+@@ -63,7 +58,7 @@ class CheckNetworkDeprecations9to10(Actor):
+ ' natively and therefore can not be migrated automatically.'
+ ' The following configuration files were found:{files}'
+ .format(
+- files=''.join(_formatted_list_output(conn.values())),
++ files=format_list(conn.values()),
+ target_distro=DISTRO_REPORT_NAMES.target
+ )
+ ),
+@@ -97,7 +92,7 @@ class CheckNetworkDeprecations9to10(Actor):
+ 'Files that used to accompany legacy network configuration in "ifcfg"'
+ ' format are present, even though the configuration itself is not'
+ ' longer there. These files will be ignored:{}'
+- .format(''.join(_formatted_list_output(conn.values())))
++ .format(format_list(conn.values()))
+ ),
+ reporting.Remediation(hint='Verify that the files were not left behind by incomplete'
+ ' migration, fix up configuration if necessary, and remove'
+@@ -119,7 +114,7 @@ class CheckNetworkDeprecations9to10(Actor):
+ 'In {target_distro} 10, support for these files is no longer'
+ ' enabled and the configuration will be ignored. The following files'
+ ' were found:{conns}'.format(
+- conns=''.join(_formatted_list_output(conn.values())),
++ conns=format_list(conn.values()),
+ target_distro=DISTRO_REPORT_NAMES.target,
+ )
+ ),
+diff --git a/repos/system_upgrade/el9toel10/actors/opensslenginescheck/libraries/opensslenginescheck.py b/repos/system_upgrade/el9toel10/actors/opensslenginescheck/libraries/opensslenginescheck.py
+index 06819162..2a5584ea 100644
+--- a/repos/system_upgrade/el9toel10/actors/opensslenginescheck/libraries/opensslenginescheck.py
++++ b/repos/system_upgrade/el9toel10/actors/opensslenginescheck/libraries/opensslenginescheck.py
+@@ -1,18 +1,13 @@
+ from leapp import reporting
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+
+-FMT_LIST_SEPARATOR = '\n - '
+ RESOURCES = [
+ reporting.RelatedResource('package', 'openssl'),
+ reporting.RelatedResource('file', '/etc/pki/tls/openssl.cnf')
+ ]
+
+
+-def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
+- return ['{}{}'.format(sep, item) for item in input_list]
+-
+-
+ # NOTE: This is taken from the el8toel9 library in
+ # repos/system_upgrade/el8toel9/actors/opensslconfigcheck/libraries/opensslconfigcheck.py
+ def _normalize_key(key):
+@@ -117,7 +112,7 @@ def check_openssl_engines(config):
+ ' The following OpenSSL engines are configured inside the'
+ ' /etc/pki/tls/openssl.cnf file:{engines}'.format(
+ target=DISTRO_REPORT_NAMES.target,
+- engines=''.join(_formatted_list_output(enabled_engines)),
++ engines=format_list(enabled_engines),
+ )
+ ),
+ reporting.Remediation(hint=(
+diff --git a/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py
+index 58b47f58..e962cc3e 100644
+--- a/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py
++++ b/repos/system_upgrade/el9toel10/actors/pamuserdb/checkpamuserdb/libraries/checkpamuserdb.py
+@@ -1,11 +1,9 @@
+ from leapp import reporting
+ from leapp.exceptions import StopActorExecutionError
+ from leapp.libraries.common.distro import DISTRO_REPORT_NAMES
+-from leapp.libraries.stdlib import api
++from leapp.libraries.stdlib import api, format_list
+ from leapp.models import PamUserDbLocation
+
+-FMT_LIST_SEPARATOR = "\n - "
+-
+
+ def process():
+ msg = next(api.consume(PamUserDbLocation), None)
+@@ -19,9 +17,8 @@ def process():
+ 'On {target_distro} 10, GDMB is used by pam_userdb as it\'s backend database,'
+ ' replacing BerkeleyDB. Existing pam_userdb databases will be'
+ ' converted to GDBM. The following databases will be converted:'
+- '{sep}{locations}'.format(
+- sep=FMT_LIST_SEPARATOR,
+- locations=FMT_LIST_SEPARATOR.join(msg.locations),
++ '{locations}'.format(
++ locations=format_list(msg.locations),
+ target_distro=DISTRO_REPORT_NAMES.target,
+ )
+ ),
+diff --git a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
+index 0453ca05..10518a9b 100644
+--- a/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
++++ b/repos/system_upgrade/el9toel10/actors/pulseaudiocheck/checkpulseaudio/libraries/checkpulseaudio.py
+@@ -1,11 +1,9 @@
+ 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.libraries.stdlib import api, format_list
+ from leapp.models import DistributionSignedRPM, PulseAudioConfiguration
+
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _report_custom_pulseaudio_config(modified_defaults, dropin_dirs, user_config_dirs):
+ """
+@@ -21,24 +19,21 @@ def _report_custom_pulseaudio_config(modified_defaults, dropin_dirs, user_config
+ 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),
++ 'The following default PulseAudio configuration files have been modified:{}'.format(
++ format_list(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),
++ 'fragments:{}'.format(
++ format_list(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),
++ 'Per-user PulseAudio configuration was found in:{}'.format(
++ format_list(user_config_dirs),
+ )
+ )
+
+diff --git a/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/libraries/sssdchecks.py b/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/libraries/sssdchecks.py
+index cb95026c..16ac8fe5 100644
+--- a/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/libraries/sssdchecks.py
++++ b/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/libraries/sssdchecks.py
+@@ -1,6 +1,5 @@
+ from leapp import reporting
+-
+-FMT_LIST_SEPARATOR = '\n - '
++from leapp.libraries.stdlib import format_list
+
+
+ def check_config(model):
+@@ -17,9 +16,8 @@ def check_config(model):
+ 'to reflect this by updating every mention of sss_ssh_knownhostsproxy by '
+ 'the corresponding mention of sss_ssh_knownhosts, even those commented out. '
+ 'SSSD\'s ssh service will be enabled if not already done.\n\n'
+- 'The following files will be updated:{}{}'.format(
+- FMT_LIST_SEPARATOR,
+- FMT_LIST_SEPARATOR.join(model.sssd_config_files + model.ssh_config_files)
++ 'The following files will be updated:{}'.format(
++ format_list(model.sssd_config_files + model.ssh_config_files, callback_sort=None)
+ )
+ )
+
+diff --git a/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/tests/component_test_sssdchecks.py b/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/tests/component_test_sssdchecks.py
+index 08aa309d..b4160a55 100644
+--- a/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/tests/component_test_sssdchecks.py
++++ b/repos/system_upgrade/el9toel10/actors/sssd/sssdchecks/tests/component_test_sssdchecks.py
+@@ -1,3 +1,4 @@
++from leapp.libraries.stdlib import format_list
+ from leapp.models import KnownHostsProxyConfig, Report
+
+
+@@ -25,5 +26,4 @@ def test_sssdchecks__files(current_actor_context):
+ assert report['title'] == 'The sss_ssh_knownhostsproxy will be replaced by sss_ssh_knownhosts'
+ assert 'sss_ssh_knownhosts tool.' in report['summary']
+
+- FMT_LIST_SEPARATOR = '\n - '
+- assert "{}{}".format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(all_files)) in report['summary']
++ assert format_list(all_files, callback_sort=None) in report['summary']
+diff --git a/repos/system_upgrade/el9toel10/actors/xorgcheck/libraries/xorgcheck.py b/repos/system_upgrade/el9toel10/actors/xorgcheck/libraries/xorgcheck.py
+index e70b4d5c..113410a2 100644
+--- a/repos/system_upgrade/el9toel10/actors/xorgcheck/libraries/xorgcheck.py
++++ b/repos/system_upgrade/el9toel10/actors/xorgcheck/libraries/xorgcheck.py
+@@ -1,6 +1,7 @@
+ 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 format_list
+ from leapp.models import DistributionSignedRPM
+
+ # List of Xorg server packages to check
+@@ -14,9 +15,6 @@ _XORG_PACKAGES = [
+ 'xorg-x11-utils',
+ ]
+
+-# Separator for list formatting in reports
+-FMT_LIST_SEPARATOR = '\n - '
+-
+
+ def _report_xorg_installed(packages):
+ """
+@@ -33,11 +31,10 @@ def _report_xorg_installed(packages):
+ "Xorg server packages have been detected on your system. The Xorg server is no longer available "
+ "in {distro} 10. Applications and services that depend on Xorg server packages will "
+ "not work after the upgrade. Migrate to Wayland or maintain the Xorg packages through alternative means. "
+- "The following Xorg server packages have been detected and are not available in {distro} 10:{sep}{list}"
++ "The following Xorg server packages have been detected and are not available in {distro} 10:{list}"
+ ).format(
+ distro=DISTRO_REPORT_NAMES.target,
+- sep=FMT_LIST_SEPARATOR,
+- list=FMT_LIST_SEPARATOR.join(packages),
++ list=format_list(packages),
+ )
+
+ reporting.create_report([
diff --git a/SPECS/leapp-repository.spec b/SPECS/leapp-repository.spec
index 5a8c178..727dce2 100644
--- a/SPECS/leapp-repository.spec
+++ b/SPECS/leapp-repository.spec
@@ -53,7 +53,7 @@ py2_byte_compile "%1" "%2"}
Epoch: 1
Name: leapp-repository
Version: 0.25.0
-Release: 1%{?dist}.elevate.2
+Release: 1%{?dist}.elevate.3
Summary: Repositories for leapp
License: ASL 2.0
@@ -352,6 +352,9 @@ fi
%changelog
+* Thu Sep 03 2026 Yuriy Kohut - 0.25.0-1.elevate.3
+- ELevate vendors support for upstream 0.25.0-1 version (9c3f807ea918e18ba63a4b8210021399783edea2)
+
* Sat Aug 29 2026 Yuriy Kohut - 0.25.0-1.elevate.2
- Drop inherited rd.lvm.lv args from the upgrade boot entry so that all LVs get activated in the upgrade initramfs