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