diff --git a/0054-linter-Fix-line-too-long-in-postgresqlcheck.patch b/0054-linter-Fix-line-too-long-in-postgresqlcheck.patch new file mode 100644 index 0000000..665a6b8 --- /dev/null +++ b/0054-linter-Fix-line-too-long-in-postgresqlcheck.patch @@ -0,0 +1,26 @@ +From 57fa7a5781f6cff6ab1632d83e971c4bc395fc20 Mon Sep 17 00:00:00 2001 +From: Matej Matuska +Date: Wed, 22 Jan 2025 14:50:06 +0100 +Subject: [PATCH 54/63] linter: Fix line too long in postgresqlcheck + +--- + .../actors/postgresqlcheck/libraries/postgresqlcheck.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/repos/system_upgrade/el8toel9/actors/postgresqlcheck/libraries/postgresqlcheck.py b/repos/system_upgrade/el8toel9/actors/postgresqlcheck/libraries/postgresqlcheck.py +index eefe583b..1cc5362d 100644 +--- a/repos/system_upgrade/el8toel9/actors/postgresqlcheck/libraries/postgresqlcheck.py ++++ b/repos/system_upgrade/el8toel9/actors/postgresqlcheck/libraries/postgresqlcheck.py +@@ -9,7 +9,8 @@ report_server_inst_summary = ( + ' PostgreSQL server 13 by default, which is incompatible with 9.6, 10 and 12' + ' included in RHEL-8, in those cases, it is necessary to proceed with additional steps' + ' for the complete upgrade of the PostgreSQL data.' +- 'If the database has already been upgraded, meaning the system is already using PostgreSQL 13, then no further actions are required.' ++ 'If the database has already been upgraded, meaning the system is already using PostgreSQL 13,' ++ ' then no further actions are required.' + ) + + report_server_inst_hint = ( +-- +2.48.1 + diff --git a/0055-Fix-typos-in-comments-to-make-spellchecker-happy.patch b/0055-Fix-typos-in-comments-to-make-spellchecker-happy.patch new file mode 100644 index 0000000..3ddd062 --- /dev/null +++ b/0055-Fix-typos-in-comments-to-make-spellchecker-happy.patch @@ -0,0 +1,39 @@ +From bfcf59e5f78aa7500a6524094bdebf28d359d9d5 Mon Sep 17 00:00:00 2001 +From: Petr Stodulka +Date: Thu, 23 Jan 2025 11:44:59 +0100 +Subject: [PATCH 55/63] Fix typos in comments to make spellchecker happy + +--- + .../common/actors/filterrpmtransactionevents/actor.py | 2 +- + .../selinuxcontentscanner/libraries/selinuxcontentscanner.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py b/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py +index 5ccdb35b..582a5821 100644 +--- a/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py ++++ b/repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py +@@ -14,7 +14,7 @@ class FilterRpmTransactionTasks(Actor): + + In order to calculate a working DNF Upgrade transaction, Leapp can collect data from multiple + sources and find workarounds for possible problems. This actor will filter all collected +- workarounds and keep only those relevants to current system based on installed packages. ++ workarounds and keep only those relevant to current system based on installed packages. + """ + + name = 'check_rpm_transaction_events' +diff --git a/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/libraries/selinuxcontentscanner.py b/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/libraries/selinuxcontentscanner.py +index 8f5e31ab..1ef69fe6 100644 +--- a/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/libraries/selinuxcontentscanner.py ++++ b/repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/libraries/selinuxcontentscanner.py +@@ -126,7 +126,7 @@ def get_selinux_modules(): + + for (name, priority) in modules: + # Udica templates should not be transferred, we only need a list of their +- # names and priorities so that we can reinstall their latest verisions ++ # names and priorities so that we can reinstall their latest versions + if name in UDICA_TEMPLATES: + template_list.append( + SELinuxModule( +-- +2.48.1 + diff --git a/0056-fix-add_upgrade_boot_entry-convert-arg-list-into-a-t.patch b/0056-fix-add_upgrade_boot_entry-convert-arg-list-into-a-t.patch new file mode 100644 index 0000000..613fa90 --- /dev/null +++ b/0056-fix-add_upgrade_boot_entry-convert-arg-list-into-a-t.patch @@ -0,0 +1,43 @@ +From c8614161017104559d224c33a52648e2c317b8e0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Michal=20He=C4=8Dko?= +Date: Tue, 28 Jan 2025 15:08:39 +0100 +Subject: [PATCH 56/63] fix(add_upgrade_boot_entry): convert arg list into a + tuple (#1313) + +Convert collected rd.lvm args into a tuple before trying to make +a set with one of the elements being the args. As list is not hashable, +this causes the actor to crash. +--- + .../actors/addupgradebootentry/libraries/addupgradebootentry.py | 2 +- + .../addupgradebootentry/tests/unit_test_addupgradebootentry.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py +index b236e39b..981c9401 100644 +--- a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py +@@ -303,7 +303,7 @@ def _get_rdlvm_arg_values(): + api.current_logger().debug('Collected the following rd.lvm.lv args that are undesired for the squashfs: %s', + rd_lvm_values) + +- return rd_lvm_values ++ return tuple(rd_lvm_values) + + + def construct_cmdline_args_for_livemode(): +diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py +index 2f58ba9e..dde18782 100644 +--- a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py +@@ -273,7 +273,7 @@ def test_get_rdlvm_arg_values(monkeypatch): + + args = addupgradebootentry._get_rdlvm_arg_values() + +- assert args == ['A', 'B'] ++ assert args == ('A', 'B') + + + def test_get_device_uuid(monkeypatch): +-- +2.48.1 + diff --git a/0057-fix-arm-bootloader-efi-patch-grub.cfg-used-for-upgra.patch b/0057-fix-arm-bootloader-efi-patch-grub.cfg-used-for-upgra.patch new file mode 100644 index 0000000..0978488 --- /dev/null +++ b/0057-fix-arm-bootloader-efi-patch-grub.cfg-used-for-upgra.patch @@ -0,0 +1,251 @@ +From 1207dececb3911efacc4ba548b2d173c0f604a41 Mon Sep 17 00:00:00 2001 +From: Michal Hecko +Date: Thu, 16 Jan 2025 18:54:34 +0100 +Subject: [PATCH 57/63] fix(arm,bootloader,efi): patch grub.cfg used for + upgrading + +Use the grub.cfg bundled within leapp if we detect that +system's grub.cfg contains problematic configuration which +will not load grubenv of the upgrade BLS entry. We need +to ensure that this grubenv is loaded, as without it we +cannot guarantee a successful boot into upgrade environment. +--- + .../files/grub2_config_template | 26 +++++ + .../libraries/addupgradebootloader.py | 102 +++++++++++++++++- + .../tests/test_addarmbootloaderworkaround.py | 29 +++++ + .../libraries/removeupgradeefientry.py | 4 +- + 4 files changed, 158 insertions(+), 3 deletions(-) + create mode 100644 repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/files/grub2_config_template + +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/files/grub2_config_template b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/files/grub2_config_template +new file mode 100644 +index 00000000..83de1417 +--- /dev/null ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/files/grub2_config_template +@@ -0,0 +1,26 @@ ++set timeout=0 ++ ++# Make sure to load EFI/leapp/grubenv and not system's default path ++if [ -f ${config_directory}/grubenv ]; then ++ load_env -f ${config_directory}/grubenv ++elif [ -s $prefix/grubenv ]; then ++ load_env ++fi ++ ++# EFI/leapp/grubenv contains our upgrade BLS entry as saved_entry ++if [ "${next_entry}" ] ; then ++ set default="${next_entry}" ++ set next_entry= ++ save_env next_entry ++ set boot_once=true ++else ++ set default="${saved_entry}" ++fi ++ ++search --no-floppy --set=root --fs-uuid LEAPP_BOOT_UUID ++set boot=${root} ++function load_video { ++ insmod all_video ++} ++${serial}${terminal_input}${terminal_output} ++blscfg +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +index 5e9bf5c6..01db9bbf 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +@@ -6,6 +6,7 @@ from leapp.libraries.common import mounting + from leapp.libraries.common.grub import ( + canonical_path_to_efi_format, + EFIBootInfo, ++ get_boot_partition, + get_device_number, + get_efi_device, + get_efi_partition, +@@ -26,6 +27,15 @@ RHEL_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/redhat/') + + CONTAINER_DOWNLOAD_DIR = '/tmp_pkg_download_dir' + ++LEAPP_GRUB2_CFG_TEMPLATE = 'grub2_config_template' ++""" ++Our grub configuration file template that is used in case the system's grubcfg would not load our grubenv. ++ ++The template contains placeholders named with LEAPP_*, that need to be replaced in order to ++obtain a valid config. ++ ++""" ++ + + def _copy_file(src_path, dst_path): + if os.path.exists(dst_path): +@@ -49,11 +59,14 @@ def process(): + context.copytree_from(RHEL_EFIDIR_CANONICAL_PATH, LEAPP_EFIDIR_CANONICAL_PATH) + + _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg']) +- _link_grubenv_to_upgrade_entry() + + efibootinfo = EFIBootInfo() + current_boot_entry = efibootinfo.entries[efibootinfo.current_bootnum] + upgrade_boot_entry = _add_upgrade_boot_entry(efibootinfo) ++ ++ leapp_efi_grubenv = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv') ++ patch_efi_redhat_grubcfg_to_load_correct_grubenv() ++ + _set_bootnext(upgrade_boot_entry.boot_number) + + efibootentry_fields = ['boot_number', 'label', 'active', 'efi_bin_source'] +@@ -183,3 +196,90 @@ def _set_bootnext(boot_number): + run(['/usr/sbin/efibootmgr', '--bootnext', boot_number]) + except CalledProcessError: + raise StopActorExecutionError('Could not set boot entry {} as BootNext.'.format(boot_number)) ++ ++ ++def _notify_user_to_check_grub2_cfg(): ++ # Or maybe rather ask a question in a dialog? But this is rare, so maybe continuing is fine. ++ pass ++ ++ ++def _will_grubcfg_read_our_grubenv(grubcfg_path): ++ with open(grubcfg_path) as grubcfg: ++ config_lines = grubcfg.readlines() ++ ++ will_read = False ++ for line in config_lines: ++ if line.strip() == 'load_env -f ${config_directory}/grubenv': ++ will_read = True ++ break ++ ++ return will_read ++ ++ ++def _get_boot_device_uuid(): ++ boot_device = get_boot_partition() ++ try: ++ raw_device_info_lines = run(['blkid', boot_device], split=True)['stdout'] ++ raw_device_info = raw_device_info_lines[0] # There is only 1 output line ++ ++ uuid_needle_start_pos = raw_device_info.index('UUID') ++ raw_device_info = raw_device_info[uuid_needle_start_pos:] # results in: "UUID="..." .... ++ ++ uuid = raw_device_info.split(' ', 1)[0] # UUID cannot contain spaces ++ uuid = uuid[len('UUID='):] # Remove UUID= ++ uuid = uuid.strip('"') ++ return uuid ++ ++ except CalledProcessError as error: ++ details = {'details': 'blkid failed with error: {}'.format(error)} ++ raise StopActorExecutionError('Failed to obtain UUID of /boot partition', details=details) ++ ++ ++def _prepare_config_contents(): ++ config_template_path = api.get_actor_file_path(LEAPP_GRUB2_CFG_TEMPLATE) ++ with open(config_template_path) as config_template_handle: ++ config_template = config_template_handle.read() ++ ++ substitutions = { ++ 'LEAPP_BOOT_UUID': _get_boot_device_uuid() ++ } ++ ++ api.current_logger().debug( ++ 'Applying the following substitution map to grub config template: {}'.format(substitutions) ++ ) ++ ++ for placeholder, placeholder_value in substitutions.items(): ++ config_template = config_template.replace(placeholder, placeholder_value) ++ ++ return config_template ++ ++ ++def _write_config(config_path, config_contents): ++ with open(config_path, 'w') as grub_cfg_handle: ++ grub_cfg_handle.write(config_contents) ++ ++ ++def patch_efi_redhat_grubcfg_to_load_correct_grubenv(): ++ """ ++ Replaces /boot/efi/EFI/redhat/grub2.cfg with a patched grub2.cfg shipped in leapp. ++ ++ The grub2.cfg shipped on some AWS images omits the section that loads grubenv different ++ EFI entries. Thus, we need to replace it with our own that will load grubenv shipped ++ of our UEFI boot entry. ++ """ ++ leapp_grub_cfg_path = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grub.cfg') ++ ++ if not os.path.isfile(leapp_grub_cfg_path): ++ msg = 'The file {} does not exists, cannot check whether bootloader is configured properly.' ++ raise StopActorExecutionError(msg.format(leapp_grub_cfg_path)) ++ ++ if _will_grubcfg_read_our_grubenv(leapp_grub_cfg_path): ++ api.current_logger().debug('The current grub.cfg will read our grubenv without any modifications.') ++ return ++ ++ api.current_logger().info('Current grub2.cfg is likely faulty (would not read our grubenv), patching.') ++ ++ config_contents = _prepare_config_contents() ++ _write_config(leapp_grub_cfg_path, config_contents) ++ ++ api.current_logger().info('New upgrade grub.cfg has been written to {}'.format(leapp_grub_cfg_path)) +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +index d2015272..9ab3b7d0 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +@@ -298,6 +298,9 @@ def test_process(monkeypatch): + monkeypatch.setattr(addupgradebootloader, '_add_upgrade_boot_entry', mock_add_upgrade_boot_entry) + monkeypatch.setattr(addupgradebootloader, '_set_bootnext', lambda _: None) + ++ monkeypatch.setattr(addupgradebootloader, 'patch_efi_redhat_grubcfg_to_load_correct_grubenv', ++ lambda: None) ++ + addupgradebootloader.process() + + assert api.produce.called == 1 +@@ -307,6 +310,32 @@ def test_process(monkeypatch): + expected = ArmWorkaroundEFIBootloaderInfo( + original_entry=EFIBootEntry(**{f: getattr(TEST_RHEL_EFI_ENTRY, f) for f in efibootentry_fields}), + upgrade_entry=EFIBootEntry(**{f: getattr(TEST_UPGRADE_EFI_ENTRY, f) for f in efibootentry_fields}), ++ upgrade_bls_dir='/boot/upgrade-loader/entries', ++ upgrade_entry_efi_path='/boot/efi/EFI/leapp/', + ) + actual = api.produce.model_instances[0] + assert actual == expected ++ ++ ++@pytest.mark.parametrize('is_config_ok', (True, False)) ++def test_patch_grubcfg(is_config_ok, monkeypatch): ++ ++ expected_grubcfg_path = os.path.join(addupgradebootloader.EFI_MOUNTPOINT, ++ addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH, ++ 'grub.cfg') ++ def isfile_mocked(path): ++ assert expected_grubcfg_path == path ++ return True ++ ++ def prepare_config_contents_mocked(): ++ return 'config contents' ++ ++ def write_config(path, contents): ++ assert not is_config_ok # We should write only when the config is not OK ++ assert path == expected_grubcfg_path ++ assert contents == 'config contents' ++ ++ monkeypatch.setattr(os.path, 'isfile', isfile_mocked) ++ monkeypatch.setattr(addupgradebootloader, '_will_grubcfg_read_our_grubenv', lambda cfg_path: is_config_ok) ++ monkeypatch.setattr(addupgradebootloader, '_prepare_config_contents', prepare_config_contents_mocked) ++ monkeypatch.setattr(addupgradebootloader, '_write_config', write_config) +diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +index 3ff3ead9..97ede80a 100644 +--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +@@ -35,8 +35,8 @@ def remove_upgrade_efi_entry(): + + bootloader_info = get_workaround_efi_info() + +- _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg']) +- _link_grubenv_to_rhel_entry() ++ # _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg']) ++ # _link_grubenv_to_rhel_entry() + + upgrade_boot_number = bootloader_info.upgrade_entry.boot_number + try: +-- +2.48.1 + diff --git a/0058-feat-arm-bootloader-efi-use-separate-BLS-directory-f.patch b/0058-feat-arm-bootloader-efi-use-separate-BLS-directory-f.patch new file mode 100644 index 0000000..7c27781 --- /dev/null +++ b/0058-feat-arm-bootloader-efi-use-separate-BLS-directory-f.patch @@ -0,0 +1,524 @@ +From b71b666594043ef6076c0c6220aeb54e7ee3a2a4 Mon Sep 17 00:00:00 2001 +From: Michal Hecko +Date: Mon, 20 Jan 2025 14:59:37 +0100 +Subject: [PATCH 58/63] feat(arm,bootloader,efi): use separate BLS directory + for upgrades + +Use a separate BLS directory '/boot/upgrade-loader/entries' +that mimics '/boot/loader/entries'. This allows very fine +control of what boot entries are available when booting +into upgrade environment via a separate EFI entry. +--- + .../actors/addupgradebootentry/actor.py | 2 + + .../libraries/addupgradebootentry.py | 97 +++++++++++++++++++ + .../tests/unit_test_addupgradebootentry.py | 53 ++++++++++ + .../actors/removeupgradebootentry/actor.py | 4 +- + .../libraries/removeupgradebootentry.py | 10 +- + .../tests/unit_test_removeupgradebootentry.py | 41 +++++--- + .../libraries/addupgradebootloader.py | 4 +- + .../tests/test_addarmbootloaderworkaround.py | 3 +- + .../libraries/removeupgradeefientry.py | 14 ++- + .../tests/test_removeupgradeefientry.py | 11 ++- + .../el8toel9/models/upgradeefientry.py | 16 +++ + 11 files changed, 232 insertions(+), 23 deletions(-) + +diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/actor.py b/repos/system_upgrade/common/actors/addupgradebootentry/actor.py +index e4ecf39e..9698f3c2 100644 +--- a/repos/system_upgrade/common/actors/addupgradebootentry/actor.py ++++ b/repos/system_upgrade/common/actors/addupgradebootentry/actor.py +@@ -4,6 +4,7 @@ from leapp.actors import Actor + from leapp.exceptions import StopActorExecutionError + from leapp.libraries.actor.addupgradebootentry import add_boot_entry, fix_grub_config_error + from leapp.models import ( ++ ArmWorkaroundEFIBootloaderInfo, + BootContent, + FirmwareFacts, + GrubConfigError, +@@ -28,6 +29,7 @@ class AddUpgradeBootEntry(Actor): + + name = 'add_upgrade_boot_entry' + consumes = ( ++ ArmWorkaroundEFIBootloaderInfo, + BootContent, + GrubConfigError, + FirmwareFacts, +diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py +index 981c9401..53b57e95 100644 +--- a/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/addupgradebootentry/libraries/addupgradebootentry.py +@@ -1,11 +1,13 @@ + import itertools + import os + import re ++import shutil + + from leapp.exceptions import StopActorExecutionError + from leapp.libraries.common.config import architecture, get_env + from leapp.libraries.stdlib import api, CalledProcessError, run + from leapp.models import ( ++ ArmWorkaroundEFIBootloaderInfo, + BootContent, + KernelCmdline, + KernelCmdlineArg, +@@ -197,6 +199,8 @@ def add_boot_entry(configs=None): + details={'details': '{}: {}'.format(str(e), e.stderr)} + ) + ++ apply_arm_specific_modifications() ++ + + def _remove_old_upgrade_boot_entry(kernel_dst_path, configs=None): + """ +@@ -357,3 +361,96 @@ def construct_cmdline_args_for_livemode(): + api.current_logger().info('The use of live mode image implies the following cmdline args: %s', args) + + return args ++ ++ ++def _list_grubenv_variables(): ++ try: ++ output_lines = run(['grub2-editenv', 'list'], split=True)['stdout'] ++ except CalledProcessError: ++ raise StopActorExecutionError('Failed to list grubenv variables used by the system') ++ ++ vars_with_values = {} ++ for line in output_lines: ++ var_with_value = line.split('=', 1) ++ if len(var_with_value) <= 1: ++ api.current_logger().warning( ++ 'Skipping \'{}\' in grub2-editenv output, the line does not have the form =' ++ ) ++ continue ++ vars_with_values[var_with_value[0]] = var_with_value[1] ++ ++ return vars_with_values ++ ++ ++def apply_arm_specific_modifications(): ++ arm_efi_info = next(api.consume(ArmWorkaroundEFIBootloaderInfo), None) ++ if not arm_efi_info: ++ return ++ ++ modify_our_grubenv_to_have_separate_blsdir(arm_efi_info) ++ ++ ++def modify_our_grubenv_to_have_separate_blsdir(efi_info): ++ """ Create a new blsdir for the upgrade entry if using a separate EFI entry. """ ++ leapp_efi_grubenv_path = os.path.join(efi_info.upgrade_entry_efi_path, 'grubenv') ++ ++ api.current_logger().debug( ++ 'Setting up separate blsdir for the upgrade using grubenv: {}'.format(leapp_efi_grubenv_path) ++ ) ++ ++ grubenv_vars = _list_grubenv_variables() ++ system_bls_dir = grubenv_vars.get('blsdir', '/loader/entries').lstrip('/') ++ ++ # BLS dir is relative to /boot, prepend it so we can list its contents ++ system_bls_dir = os.path.join('/boot', system_bls_dir) ++ ++ # Find our loader entry ++ try: ++ bls_entries = os.listdir(system_bls_dir) ++ except IOError: # Technically, we want FileNotFoundError, but that is only Python3.3+, so this is fine ++ details = { ++ 'details': 'Failed to list {}.'.format(system_bls_dir) ++ } ++ raise StopActorExecutionError('Failed to set up bootloader for the upgrade.', details=details) ++ ++ leapp_bls_entry = None ++ for bls_entry in bls_entries: ++ if bls_entry.endswith('upgrade.aarch64.conf'): ++ leapp_bls_entry = bls_entry ++ break ++ ++ if not leapp_bls_entry: ++ details = { ++ 'details': 'Failed to identify BLS entry that belongs to leapp in {}'.format(system_bls_dir) ++ } ++ raise StopActorExecutionError('Failed to set up bootloader for the upgrade.') ++ ++ # The 'blsdir' grubenv variable specifies location of bls directory relative to /boot ++ if os.path.exists(efi_info.upgrade_bls_dir): ++ msg = 'The {} directory exists, probably a left-over from previous executions. Removing.' ++ api.current_logger().debug(msg.format(efi_info.upgrade_bls_dir)) ++ shutil.rmtree(efi_info.upgrade_bls_dir) ++ ++ os.makedirs(efi_info.upgrade_bls_dir) ++ api.current_logger().debug('Successfully created upgrade BLS directory: {}'.format(efi_info.upgrade_bls_dir)) ++ ++ leapp_bls_entry_fullpath = os.path.join(system_bls_dir, leapp_bls_entry) ++ bls_entry_dst = os.path.join(efi_info.upgrade_bls_dir, leapp_bls_entry) ++ api.current_logger().debug( ++ 'Moving leapp\'s BLS entry ({}) into a separate BLS dir located at {}'.format( ++ leapp_bls_entry, efi_info.upgrade_bls_dir ++ ) ++ ) ++ ++ shutil.move(leapp_bls_entry_fullpath, bls_entry_dst) ++ ++ upgrade_bls_dir_rel_to_boot = efi_info.upgrade_bls_dir[len('/boot'):] ++ ++ # Modify leapp's grubenv to define our own BLSDIR ++ try: ++ run(['grub2-editenv', leapp_efi_grubenv_path, 'set', 'blsdir={}'.format(upgrade_bls_dir_rel_to_boot)]) ++ except CalledProcessError as error: ++ details = { ++ 'details': 'Failed to modify upgrade grubenv to contain a custom blsdir definition. Error {}'.format(error) ++ } ++ raise StopActorExecutionError('Failed to set up bootloader for the upgrade.', details=details) +diff --git a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py +index dde18782..2a0b3f0f 100644 +--- a/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/addupgradebootentry/tests/unit_test_addupgradebootentry.py +@@ -1,4 +1,5 @@ + import os ++import shutil + from collections import namedtuple + + import pytest +@@ -9,7 +10,9 @@ from leapp.libraries.common.config.architecture import ARCH_S390X, ARCH_X86_64 + from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked + from leapp.libraries.stdlib import api + from leapp.models import ( ++ ArmWorkaroundEFIBootloaderInfo, + BootContent, ++ EFIBootEntry, + KernelCmdline, + KernelCmdlineArg, + LateTargetKernelCmdlineArgTasks, +@@ -326,3 +329,53 @@ def test_get_device_uuid(monkeypatch): + uuid = addupgradebootentry._get_device_uuid(path) + + assert uuid == 'MY_UUID1' ++ ++ ++def test_modify_grubenv_to_have_separate_blsdir(monkeypatch): ++ efi_info = ArmWorkaroundEFIBootloaderInfo( ++ original_entry=EFIBootEntry( ++ boot_number='0001', ++ label='Redhat', ++ active=True, ++ efi_bin_source="HD(.*)/File(\\EFI\\redhat\\shimx64.efi)", ++ ), ++ upgrade_entry=EFIBootEntry( ++ boot_number='0002', ++ label='Leapp', ++ active=True, ++ efi_bin_source="HD(.*)/File(\\EFI\\leapp\\shimx64.efi)", ++ ), ++ upgrade_bls_dir='/boot/upgrade-loader/entries', ++ upgrade_entry_efi_path='/boot/efi/EFI/leapp' ++ ) ++ ++ def list_grubenv_variables_mock(): ++ return { ++ 'blsdir': '/blsdir' ++ } ++ ++ def listdir_mock(dir_path): ++ assert dir_path == '/boot/blsdir' ++ return [ ++ '4a9c76478b98444fb5e0fbf533950edf-6.12.5-200.fc41.x86_64.conf', ++ '4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf', ++ ] ++ ++ def assert_path_correct(path): ++ assert path == efi_info.upgrade_bls_dir ++ ++ def move_mocked(src, dst): ++ assert src == '/boot/blsdir/4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf' ++ assert dst == '/boot/upgrade-loader/entries/4a9c76478b98444fb5e0fbf533950edf-upgrade.aarch64.conf' ++ ++ def run_mocked(cmd, *arg, **kwargs): ++ assert cmd == ['grub2-editenv', '/boot/efi/EFI/leapp/grubenv', 'set', 'blsdir=/upgrade-loader/entries'] ++ ++ monkeypatch.setattr(addupgradebootentry, '_list_grubenv_variables', list_grubenv_variables_mock) ++ monkeypatch.setattr(os, 'listdir', listdir_mock) ++ monkeypatch.setattr(os.path, 'exists', assert_path_correct) ++ monkeypatch.setattr(os, 'makedirs', assert_path_correct) ++ monkeypatch.setattr(shutil, 'move', move_mocked) ++ monkeypatch.setattr(addupgradebootentry, 'run', run_mocked) ++ ++ addupgradebootentry.modify_our_grubenv_to_have_separate_blsdir(efi_info) +diff --git a/repos/system_upgrade/common/actors/removeupgradebootentry/actor.py b/repos/system_upgrade/common/actors/removeupgradebootentry/actor.py +index 32759e77..6a0a1081 100644 +--- a/repos/system_upgrade/common/actors/removeupgradebootentry/actor.py ++++ b/repos/system_upgrade/common/actors/removeupgradebootentry/actor.py +@@ -1,6 +1,6 @@ + from leapp.actors import Actor + from leapp.libraries.actor.removeupgradebootentry import remove_boot_entry +-from leapp.models import BootContent, FirmwareFacts ++from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts + from leapp.tags import InitRamStartPhaseTag, IPUWorkflowTag + + +@@ -12,7 +12,7 @@ class RemoveUpgradeBootEntry(Actor): + """ + + name = 'remove_upgrade_boot_entry' +- consumes = (BootContent, FirmwareFacts) ++ consumes = (ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts) + produces = () + tags = (IPUWorkflowTag, InitRamStartPhaseTag) + +diff --git a/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py b/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py +index ee8e1ecd..7434e48c 100644 +--- a/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py +@@ -1,7 +1,7 @@ + from leapp.exceptions import StopActorExecutionError + from leapp.libraries.common.config import architecture + from leapp.libraries.stdlib import api, CalledProcessError, run +-from leapp.models import BootContent, FirmwareFacts ++from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, FirmwareFacts + + + def remove_boot_entry(): +@@ -25,6 +25,14 @@ def remove_boot_entry(): + # partitions have been most likely already mounted + pass + kernel_filepath = get_upgrade_kernel_filepath() ++ ++ arm_bootloader_workaround_info = next(api.consume(ArmWorkaroundEFIBootloaderInfo), None) ++ if arm_bootloader_workaround_info and arm_bootloader_workaround_info.upgrade_bls_dir: ++ # Leapp has a separate BLS dir and grubby will not know about it. We don't need to call ++ # grubby here - we are removing the entire BLS dir in another actor. ++ api.current_logger().debug('Skipping removal of upgrade kernel entry since we are using a separate BLS dir.') ++ return ++ + run([ + '/usr/sbin/grubby', + '--remove-kernel={0}'.format(kernel_filepath) +diff --git a/repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py b/repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py +index 54eec552..c84d3085 100644 +--- a/repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py ++++ b/repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py +@@ -5,11 +5,12 @@ from leapp.libraries.actor import removeupgradebootentry + from leapp.libraries.common.config import architecture + from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked + from leapp.libraries.stdlib import api +-from leapp.models import BootContent, FirmwareFacts ++from leapp.models import ArmWorkaroundEFIBootloaderInfo, BootContent, EFIBootEntry, FirmwareFacts + + + class run_mocked(object): +- args = [] ++ def __init__(self): ++ self.args = [] + + def __call__(self, args, split=True): + self.args.append(args) +@@ -17,17 +18,25 @@ class run_mocked(object): + + @pytest.mark.parametrize('firmware', ['bios', 'efi']) + @pytest.mark.parametrize('arch', [architecture.ARCH_X86_64, architecture.ARCH_S390X]) +-def test_remove_boot_entry(firmware, arch, monkeypatch): ++@pytest.mark.parametrize('has_separate_bls_dir', [True, False]) ++def test_remove_boot_entry(firmware, arch, has_separate_bls_dir, monkeypatch): + def get_upgrade_kernel_filepath_mocked(): + return '/abc' + +- def consume_systemfacts_mocked(*models): +- yield FirmwareFacts(firmware=firmware) +- +- monkeypatch.setattr(removeupgradebootentry, 'get_upgrade_kernel_filepath', get_upgrade_kernel_filepath_mocked, ) +- monkeypatch.setattr(api, 'consume', consume_systemfacts_mocked) ++ messages = [FirmwareFacts(firmware=firmware)] ++ if has_separate_bls_dir: ++ some_efi_entry = EFIBootEntry(boot_number='0001', label='entry', active=True, efi_bin_source='') ++ workaround_info = ArmWorkaroundEFIBootloaderInfo( ++ original_entry=some_efi_entry, ++ upgrade_entry=some_efi_entry, ++ upgrade_bls_dir='/boot/upgrade-loader/entries', ++ upgrade_entry_efi_path='/boot/efi/EFI/leapp/' ++ ) ++ messages.append(workaround_info) ++ ++ monkeypatch.setattr(removeupgradebootentry, 'get_upgrade_kernel_filepath', get_upgrade_kernel_filepath_mocked) + monkeypatch.setattr(removeupgradebootentry, 'run', run_mocked()) +- monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch)) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch, msgs=messages)) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + + removeupgradebootentry.remove_boot_entry() +@@ -36,16 +45,16 @@ def test_remove_boot_entry(firmware, arch, monkeypatch): + if firmware == 'efi': + boot_mounts.append(['/bin/mount', '/boot/efi']) + +- calls = boot_mounts + [['/usr/sbin/grubby', '--remove-kernel=/abc']] +- if arch == architecture.ARCH_S390X: +- calls.append(['/usr/sbin/zipl']) +- calls.append(['/bin/mount', '-a']) ++ calls = boot_mounts ++ if not has_separate_bls_dir: ++ # If we are using a separate BLS dir (ARM specific), then do not call anything ++ calls += [['/usr/sbin/grubby', '--remove-kernel=/abc']] ++ if arch == architecture.ARCH_S390X: ++ calls.append(['/usr/sbin/zipl']) ++ calls.append(['/bin/mount', '-a']) + + assert removeupgradebootentry.run.args == calls + +- # clear args for next run +- del removeupgradebootentry.run.args[:] +- + + def test_get_upgrade_kernel_filepath(monkeypatch): + # BootContent message available +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +index 01db9bbf..27621185 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +@@ -24,6 +24,7 @@ ARM_GRUB_PACKAGE_NAME = 'grub2-efi-aa64' + EFI_MOUNTPOINT = '/boot/efi/' + LEAPP_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/leapp/') + RHEL_EFIDIR_CANONICAL_PATH = os.path.join(EFI_MOUNTPOINT, 'EFI/redhat/') ++UPGRADE_BLS_DIR = '/boot/upgrade-loader' + + CONTAINER_DOWNLOAD_DIR = '/tmp_pkg_download_dir' + +@@ -64,7 +65,6 @@ def process(): + current_boot_entry = efibootinfo.entries[efibootinfo.current_bootnum] + upgrade_boot_entry = _add_upgrade_boot_entry(efibootinfo) + +- leapp_efi_grubenv = os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv') + patch_efi_redhat_grubcfg_to_load_correct_grubenv() + + _set_bootnext(upgrade_boot_entry.boot_number) +@@ -74,6 +74,8 @@ def process(): + ArmWorkaroundEFIBootloaderInfo( + original_entry=EFIBootEntry(**{f: getattr(current_boot_entry, f) for f in efibootentry_fields}), + upgrade_entry=EFIBootEntry(**{f: getattr(upgrade_boot_entry, f) for f in efibootentry_fields}), ++ upgrade_bls_dir=UPGRADE_BLS_DIR, ++ upgrade_entry_efi_path=os.path.join(EFI_MOUNTPOINT, LEAPP_EFIDIR_CANONICAL_PATH), + ) + ) + +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +index 9ab3b7d0..7017e645 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +@@ -310,7 +310,7 @@ def test_process(monkeypatch): + expected = ArmWorkaroundEFIBootloaderInfo( + original_entry=EFIBootEntry(**{f: getattr(TEST_RHEL_EFI_ENTRY, f) for f in efibootentry_fields}), + upgrade_entry=EFIBootEntry(**{f: getattr(TEST_UPGRADE_EFI_ENTRY, f) for f in efibootentry_fields}), +- upgrade_bls_dir='/boot/upgrade-loader/entries', ++ upgrade_bls_dir=addupgradebootloader.UPGRADE_BLS_DIR, + upgrade_entry_efi_path='/boot/efi/EFI/leapp/', + ) + actual = api.produce.model_instances[0] +@@ -323,6 +323,7 @@ def test_patch_grubcfg(is_config_ok, monkeypatch): + expected_grubcfg_path = os.path.join(addupgradebootloader.EFI_MOUNTPOINT, + addupgradebootloader.LEAPP_EFIDIR_CANONICAL_PATH, + 'grub.cfg') ++ + def isfile_mocked(path): + assert expected_grubcfg_path == path + return True +diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +index 97ede80a..3a32ddcc 100644 +--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +@@ -54,6 +54,8 @@ def remove_upgrade_efi_entry(): + except CalledProcessError: + api.current_logger().warning('Unable to remove Leapp upgrade efi files.') + ++ _remove_upgrade_blsdir(bootloader_info) ++ + original_boot_number = bootloader_info.original_entry.boot_number + run(['/usr/sbin/efibootmgr', '--bootnext', original_boot_number]) + +@@ -82,7 +84,7 @@ def _copy_file(src_path, dst_path): + + def _copy_grub_files(required, optional): + """ +- Copy grub files from redhat/ dir to the /boot/efi/EFI/leapp/ dir. ++ Copy grub files from /boot/efi/EFI/leapp/ dir to the /boot/efi/EFI/redhat/ dir. + """ + + all_files = required + optional +@@ -98,3 +100,13 @@ def _copy_grub_files(required, optional): + continue + + _copy_file(src_path, dst_path) ++ ++ ++def _remove_upgrade_blsdir(bootloader_info): ++ api.current_logger().debug('Removing upgrade BLS directory: {}'.format(bootloader_info.upgrade_bls_dir)) ++ try: ++ shutil.rmtree(bootloader_info.upgrade_bls_dir) ++ except OSError as error: ++ # I tried, no can do at this point ++ msg = 'Failed to remove upgrade BLS directory: {} with error {}' ++ api.current_logger().debug(msg.format(bootloader_info.upgrade_bls_dir, error)) +diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py +index 1af3cd1e..30fde2da 100644 +--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py +@@ -1,4 +1,5 @@ + import os ++import shutil + + import pytest + +@@ -20,7 +21,9 @@ TEST_EFI_INFO = ArmWorkaroundEFIBootloaderInfo( + label='Leapp', + active=True, + efi_bin_source="HD(.*)/File(\\EFI\\leapp\\shimx64.efi)", +- ) ++ ), ++ upgrade_bls_dir='/boot/upgrade-loaders/entries', ++ upgrade_entry_efi_path='/boot/efi/EFI/leapp' + ) + + +@@ -89,9 +92,14 @@ def test_remove_upgrade_efi_entry(monkeypatch): + def mock_copy_grub_files(required, optional): + copy_grub_files_calls.append((required, optional)) + ++ def rmtree_mocked(tree, *args): ++ run_calls.append('shutil.rmtree') ++ assert tree == TEST_EFI_INFO.upgrade_bls_dir ++ + monkeypatch.setattr(removeupgradeefientry, '_copy_grub_files', mock_copy_grub_files) + monkeypatch.setattr(removeupgradeefientry, '_link_grubenv_to_rhel_entry', lambda: None) + monkeypatch.setattr(removeupgradeefientry, 'run', mock_run) ++ monkeypatch.setattr(shutil, 'rmtree', rmtree_mocked) + + removeupgradeefientry.remove_upgrade_efi_entry() + +@@ -100,6 +108,7 @@ def test_remove_upgrade_efi_entry(monkeypatch): + ['/bin/mount', '/boot/efi'], + ['/usr/sbin/efibootmgr', '--delete-bootnum', '--bootnum', '0002'], + ['rm', '-rf', removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH], ++ 'shutil.rmtree', + ['/usr/sbin/efibootmgr', '--bootnext', '0001'], + ['/bin/mount', '-a'], + ] +diff --git a/repos/system_upgrade/el8toel9/models/upgradeefientry.py b/repos/system_upgrade/el8toel9/models/upgradeefientry.py +index 877cdc8f..f29fc88f 100644 +--- a/repos/system_upgrade/el8toel9/models/upgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/models/upgradeefientry.py +@@ -12,3 +12,19 @@ class ArmWorkaroundEFIBootloaderInfo(Model): + original_entry = fields.Model(EFIBootEntry) + + upgrade_entry = fields.Model(EFIBootEntry) ++ ++ upgrade_bls_dir = fields.String() ++ """ ++ Path to custom BLS dir used by the upgrade EFI bootloader ++ ++ The path is absolute w.r.t. '/'. The actual value of the 'blsdir' variable ++ that is set in the upgrade grubenv will be relative to '/boot/'. ++ """ ++ ++ upgrade_entry_efi_path = fields.String() ++ """ ++ Full path to the folder containing EFI binaries for the upgrade entry. ++ ++ Example: ++ /boot/efi/EFI/leapp ++ """ +-- +2.48.1 + diff --git a/0059-fix-models-move-arm-bootloader-workaround-model-into.patch b/0059-fix-models-move-arm-bootloader-workaround-model-into.patch new file mode 100644 index 0000000..3d31819 --- /dev/null +++ b/0059-fix-models-move-arm-bootloader-workaround-model-into.patch @@ -0,0 +1,21 @@ +From 8fe49982ee048d6b74aec4f4537ea9f1b4a7e021 Mon Sep 17 00:00:00 2001 +From: Michal Hecko +Date: Mon, 27 Jan 2025 10:58:48 +0100 +Subject: [PATCH 59/63] fix(models): move arm bootloader workaround model into + common + +Move model used to implement arm bootloader workarounds to common +as this model will be also used when adding/removing kernel entries +to use custom blsdir. +--- + .../system_upgrade/{el8toel9 => common}/models/upgradeefientry.py | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + rename repos/system_upgrade/{el8toel9 => common}/models/upgradeefientry.py (100%) + +diff --git a/repos/system_upgrade/el8toel9/models/upgradeefientry.py b/repos/system_upgrade/common/models/upgradeefientry.py +similarity index 100% +rename from repos/system_upgrade/el8toel9/models/upgradeefientry.py +rename to repos/system_upgrade/common/models/upgradeefientry.py +-- +2.48.1 + diff --git a/0060-cleanup-8to9-efi-do-not-use-symlinks-or-copy-grub-fi.patch b/0060-cleanup-8to9-efi-do-not-use-symlinks-or-copy-grub-fi.patch new file mode 100644 index 0000000..4718a27 --- /dev/null +++ b/0060-cleanup-8to9-efi-do-not-use-symlinks-or-copy-grub-fi.patch @@ -0,0 +1,184 @@ +From 31af8f485f6bb78b4aed665857daa956aa79adf1 Mon Sep 17 00:00:00 2001 +From: Michal Hecko +Date: Tue, 28 Jan 2025 15:09:12 +0100 +Subject: [PATCH 60/63] cleanup(8to9,efi): do not use symlinks or copy grub + files + +--- + .../libraries/addupgradebootloader.py | 10 +---- + .../tests/test_addarmbootloaderworkaround.py | 1 - + .../libraries/removeupgradeefientry.py | 41 ------------------- + .../tests/test_removeupgradeefientry.py | 35 ---------------- + 4 files changed, 1 insertion(+), 86 deletions(-) + +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +index 27621185..c076fe6b 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/libraries/addupgradebootloader.py +@@ -9,9 +9,7 @@ from leapp.libraries.common.grub import ( + get_boot_partition, + get_device_number, + get_efi_device, +- get_efi_partition, +- GRUB2_BIOS_ENTRYPOINT, +- GRUB2_BIOS_ENV_FILE ++ get_efi_partition + ) + from leapp.libraries.stdlib import api, CalledProcessError, run + from leapp.models import ArmWorkaroundEFIBootloaderInfo, EFIBootEntry, TargetUserSpaceInfo +@@ -118,12 +116,6 @@ def _copy_grub_files(required, optional): + _copy_file(src_path, dst_path) + + +-def _link_grubenv_to_upgrade_entry(): +- upgrade_env_file = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, 'grubenv') +- upgrade_env_file_relpath = os.path.relpath(upgrade_env_file, GRUB2_BIOS_ENTRYPOINT) +- run(['ln', '--symbolic', '--force', upgrade_env_file_relpath, GRUB2_BIOS_ENV_FILE]) +- +- + def _add_upgrade_boot_entry(efibootinfo): + """ + Create a new UEFI bootloader entry with a upgrade label and bin file. +diff --git a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +index 7017e645..4f990e00 100644 +--- a/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py ++++ b/repos/system_upgrade/el8toel9/actors/addarmbootloaderworkaround/tests/test_addarmbootloaderworkaround.py +@@ -287,7 +287,6 @@ def test_process(monkeypatch): + monkeypatch.setattr(addupgradebootloader.mounting, 'NspawnActions', lambda *args, **kwargs: context_mock) + + monkeypatch.setattr(addupgradebootloader, '_copy_grub_files', lambda optional, required: None) +- monkeypatch.setattr(addupgradebootloader, '_link_grubenv_to_upgrade_entry', lambda: None) + + efibootinfo_mock = MockEFIBootInfo([TEST_RHEL_EFI_ENTRY]) + monkeypatch.setattr(addupgradebootloader, 'EFIBootInfo', lambda: efibootinfo_mock) +diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +index 3a32ddcc..daa7b2ca 100644 +--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/libraries/removeupgradeefientry.py +@@ -2,7 +2,6 @@ import os + import shutil + + from leapp.exceptions import StopActorExecutionError +-from leapp.libraries.common.grub import GRUB2_BIOS_ENTRYPOINT, GRUB2_BIOS_ENV_FILE + from leapp.libraries.stdlib import api, CalledProcessError, run + from leapp.models import ArmWorkaroundEFIBootloaderInfo + +@@ -35,9 +34,6 @@ def remove_upgrade_efi_entry(): + + bootloader_info = get_workaround_efi_info() + +- # _copy_grub_files(['grubenv', 'grub.cfg'], ['user.cfg']) +- # _link_grubenv_to_rhel_entry() +- + upgrade_boot_number = bootloader_info.upgrade_entry.boot_number + try: + run([ +@@ -65,43 +61,6 @@ def remove_upgrade_efi_entry(): + run(['/bin/mount', '-a']) + + +-def _link_grubenv_to_rhel_entry(): +- rhel_env_file = os.path.join(RHEL_EFIDIR_CANONICAL_PATH, 'grubenv') +- rhel_env_file_relpath = os.path.relpath(rhel_env_file, GRUB2_BIOS_ENTRYPOINT) +- run(['ln', '--symbolic', '--force', rhel_env_file_relpath, GRUB2_BIOS_ENV_FILE]) +- +- +-def _copy_file(src_path, dst_path): +- if os.path.exists(dst_path): +- api.current_logger().debug("The {} file already exists and its content will be overwritten.".format(dst_path)) +- +- api.current_logger().info("Copying {} to {}".format(src_path, dst_path)) +- try: +- shutil.copy2(src_path, dst_path) +- except (OSError, IOError) as err: +- raise StopActorExecutionError('I/O error({}): {}'.format(err.errno, err.strerror)) +- +- +-def _copy_grub_files(required, optional): +- """ +- Copy grub files from /boot/efi/EFI/leapp/ dir to the /boot/efi/EFI/redhat/ dir. +- """ +- +- all_files = required + optional +- for filename in all_files: +- src_path = os.path.join(LEAPP_EFIDIR_CANONICAL_PATH, filename) +- dst_path = os.path.join(RHEL_EFIDIR_CANONICAL_PATH, filename) +- +- if not os.path.exists(src_path): +- if filename in required: +- msg = 'Required file {} does not exists. Aborting.'.format(filename) +- raise StopActorExecutionError(msg) +- +- continue +- +- _copy_file(src_path, dst_path) +- +- + def _remove_upgrade_blsdir(bootloader_info): + api.current_logger().debug('Removing upgrade BLS directory: {}'.format(bootloader_info.upgrade_bls_dir)) + try: +diff --git a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py +index 30fde2da..11cd3126 100644 +--- a/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py ++++ b/repos/system_upgrade/el8toel9/actors/removeupgradeefientry/tests/test_removeupgradeefientry.py +@@ -1,4 +1,3 @@ +-import os + import shutil + + import pytest +@@ -52,52 +51,18 @@ def test_get_workaround_efi_info_no_entry(monkeypatch): + removeupgradeefientry.get_workaround_efi_info() + + +-def test_copy_grub_files(monkeypatch): +- copy_file_calls = [] +- +- def mock_copy_file(src, dst): +- copy_file_calls.append((src, dst)) +- +- monkeypatch.setattr(removeupgradeefientry, '_copy_file', mock_copy_file) +- monkeypatch.setattr(os.path, 'exists', lambda path: True) +- +- removeupgradeefientry._copy_grub_files(['required'], ['optional']) +- +- assert ( +- os.path.join(removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH, 'required'), +- os.path.join(removeupgradeefientry.RHEL_EFIDIR_CANONICAL_PATH, 'required'), +- ) in copy_file_calls +- assert ( +- os.path.join(removeupgradeefientry.LEAPP_EFIDIR_CANONICAL_PATH, 'optional'), +- os.path.join(removeupgradeefientry.RHEL_EFIDIR_CANONICAL_PATH, 'optional'), +- ) in copy_file_calls +- +- +-def test_copy_grub_files_missing_required(monkeypatch): +- monkeypatch.setattr(os.path, 'exists', lambda path: False) +- +- with pytest.raises(StopActorExecutionError, match='Required file required does not exists'): +- removeupgradeefientry._copy_grub_files(['required'], []) +- +- + def test_remove_upgrade_efi_entry(monkeypatch): + run_calls = [] +- copy_grub_files_calls = [] + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[TEST_EFI_INFO])) + + def mock_run(command, checked=False): + run_calls.append(command) + return {'exit_code': 0} + +- def mock_copy_grub_files(required, optional): +- copy_grub_files_calls.append((required, optional)) +- + def rmtree_mocked(tree, *args): + run_calls.append('shutil.rmtree') + assert tree == TEST_EFI_INFO.upgrade_bls_dir + +- monkeypatch.setattr(removeupgradeefientry, '_copy_grub_files', mock_copy_grub_files) +- monkeypatch.setattr(removeupgradeefientry, '_link_grubenv_to_rhel_entry', lambda: None) + monkeypatch.setattr(removeupgradeefientry, 'run', mock_run) + monkeypatch.setattr(shutil, 'rmtree', rmtree_mocked) + +-- +2.48.1 + diff --git a/0061-Introduce-deprecated-IPUPaths-msg-temporary-solution.patch b/0061-Introduce-deprecated-IPUPaths-msg-temporary-solution.patch new file mode 100644 index 0000000..3d6f547 --- /dev/null +++ b/0061-Introduce-deprecated-IPUPaths-msg-temporary-solution.patch @@ -0,0 +1,303 @@ +From a6438828415c094c600de80e2e05409a4ccd5822 Mon Sep 17 00:00:00 2001 +From: Petr Stodulka +Date: Wed, 29 Jan 2025 04:43:33 +0100 +Subject: [PATCH 61/63] Introduce deprecated IPUPaths msg (temporary solution) + +This is hackish precursor to move checking of the specified +target system versions into actors to be able to create report +when unsupported target version is specified. + +The problem is that currently there is no information about the +supported upgrade paths in messages. Also, the information about +supported source and target versions are stored in two different +places (in system upgrade common repo): + * shared configs.version library + * files/upgrade_paths.json + +As a temporary solution let's introduce IPUPaths message which +will contain filtered data from the json file based on: + * the upgrade flavour (default, saphana) + * and source major version + +There is no value to print information to users about different +upgrade paths for other flavours and OS major versions. The model +is marked as deprecated so we can remove it in the next release when +we redesign this solution to unify how actors get this data +(and define them just in one place). + +jira: RHEL-51072 +--- + .../actors/scandefinedipupaths/actor.py | 31 ++++++ + .../libraries/scandefinedipupaths.py | 43 ++++++++ + .../tests/files/upgrade_paths.json | 15 +++ + .../tests/test_scandefinedipupaths.py | 97 +++++++++++++++++++ + .../system_upgrade/common/models/ipupaths.py | 43 ++++++++ + 5 files changed, 229 insertions(+) + create mode 100644 repos/system_upgrade/common/actors/scandefinedipupaths/actor.py + create mode 100644 repos/system_upgrade/common/actors/scandefinedipupaths/libraries/scandefinedipupaths.py + create mode 100644 repos/system_upgrade/common/actors/scandefinedipupaths/tests/files/upgrade_paths.json + create mode 100644 repos/system_upgrade/common/actors/scandefinedipupaths/tests/test_scandefinedipupaths.py + create mode 100644 repos/system_upgrade/common/models/ipupaths.py + +diff --git a/repos/system_upgrade/common/actors/scandefinedipupaths/actor.py b/repos/system_upgrade/common/actors/scandefinedipupaths/actor.py +new file mode 100644 +index 00000000..a84c85f2 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/scandefinedipupaths/actor.py +@@ -0,0 +1,31 @@ ++from leapp.actors import Actor ++from leapp.libraries.actor import scandefinedipupaths ++from leapp.models import IPUPaths ++from leapp.tags import FactsPhaseTag, IPUWorkflowTag ++ ++ ++class ScanDefinedIPUPaths(Actor): ++ """ ++ Load defined IPU paths for the current major source system version ++ and defined upgrade flavour. ++ ++ The upgrade paths are defined inside `files/upgrade_paths.json`. ++ Based on the defined upgrade flavour (default, saphana, ..) loads particular ++ definitions and filter out all upgrade paths from other system major versions. ++ I.e. for RHEL 8.10 system with the default upgrade flavour, load all upgrade ++ paths from any RHEL 8 system defined under the 'default' flavour. ++ ++ The code is mostly taken from the CLI command_utils. The duplicate solution ++ is not so problematic now as it will be unified next time. ++ ++ Note the deprecation suppression is expected here as this is considered as ++ temporary solution now. ++ """ ++ ++ name = 'scan_defined_ipu_paths' ++ consumes = () ++ produces = (IPUPaths,) ++ tags = (IPUWorkflowTag, FactsPhaseTag) ++ ++ def process(self): ++ scandefinedipupaths.process() +diff --git a/repos/system_upgrade/common/actors/scandefinedipupaths/libraries/scandefinedipupaths.py b/repos/system_upgrade/common/actors/scandefinedipupaths/libraries/scandefinedipupaths.py +new file mode 100644 +index 00000000..1e39f2c8 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/scandefinedipupaths/libraries/scandefinedipupaths.py +@@ -0,0 +1,43 @@ ++import json ++ ++from leapp.libraries.common.config.version import get_source_major_version ++from leapp.libraries.stdlib import api ++from leapp.models import IPUPath, IPUPaths ++from leapp.utils.deprecation import suppress_deprecation ++ ++ ++def load_ipu_paths_for_flavour(flavour, _filename='upgrade_paths.json'): ++ """ ++ Load defined IPU paths from the upgrade_paths.json file for the specified ++ flavour. ++ ++ Note the file is required to be always present, so skipping any test ++ for the missing file. Crash hard and terribly if the file is missing ++ or the content is invalid. ++ ++ We expect the flavour to be always good as it is under our control ++ (already sanitized in IPUConfig), but return empty dict and log it if missing. ++ """ ++ with open(api.get_common_file_path(_filename)) as fp: ++ data = json.loads(fp.read()) ++ if flavour not in data: ++ api.current_logger().warning( ++ 'Cannot discover any upgrade paths for flavour: {}' ++ .format(flavour) ++ ) ++ return data.get(flavour, {}) ++ ++ ++def get_filtered_ipu_paths(ipu_paths, src_major_version): ++ result = [] ++ for src_version, tgt_versions in ipu_paths.items(): ++ if src_version.split('.')[0] == src_major_version: ++ result.append(IPUPath(source_version=src_version, target_versions=tgt_versions)) ++ return result ++ ++ ++@suppress_deprecation(IPUPaths) ++def process(): ++ flavour = api.current_actor().configuration.flavour ++ ipu_paths = load_ipu_paths_for_flavour(flavour) ++ api.produce(IPUPaths(data=get_filtered_ipu_paths(ipu_paths, get_source_major_version()))) +diff --git a/repos/system_upgrade/common/actors/scandefinedipupaths/tests/files/upgrade_paths.json b/repos/system_upgrade/common/actors/scandefinedipupaths/tests/files/upgrade_paths.json +new file mode 100644 +index 00000000..edd32224 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/scandefinedipupaths/tests/files/upgrade_paths.json +@@ -0,0 +1,15 @@ ++{ ++ "default": { ++ "8.10": ["9.4", "9.5", "9.6"], ++ "8.4": ["9.2"], ++ "9.6": ["10.0"], ++ "8": ["9.4", "9.5", "9.6"], ++ "9": ["10.0"] ++ }, ++ "saphana": { ++ "8.10": ["9.6", "9.4"], ++ "8": ["9.6", "9.4"], ++ "9.6": ["10.0"], ++ "9": ["10.0"] ++ } ++} +diff --git a/repos/system_upgrade/common/actors/scandefinedipupaths/tests/test_scandefinedipupaths.py b/repos/system_upgrade/common/actors/scandefinedipupaths/tests/test_scandefinedipupaths.py +new file mode 100644 +index 00000000..9ffc9829 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/scandefinedipupaths/tests/test_scandefinedipupaths.py +@@ -0,0 +1,97 @@ ++import json ++import os ++ ++import pytest ++ ++from leapp.libraries.actor import scandefinedipupaths ++from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked ++from leapp.models import IPUPath, IPUPaths ++from leapp.utils.deprecation import suppress_deprecation ++ ++CUR_DIR = os.path.dirname(os.path.abspath(__file__)) ++ ++ ++class CurrentActorMockedModified(CurrentActorMocked): ++ def get_common_file_path(self, fname): ++ fpath = os.path.join(CUR_DIR, 'files', fname) ++ assert os.path.exists(fpath) ++ if os.path.exists(fpath): ++ return fpath ++ return None ++ ++ ++@pytest.mark.parametrize(('flavour', 'expected_result'), ( ++ ('nonsense', {}), ++ ( ++ 'default', ++ { ++ '8.10': ['9.4', '9.5', '9.6'], ++ '8.4': ['9.2'], ++ '9.6': ['10.0'], ++ '8': ['9.4', '9.5', '9.6'], ++ '9': ['10.0'] ++ } ++ ), ++ ( ++ 'saphana', ++ { ++ '8.10': ['9.6', '9.4'], ++ '8': ['9.6', '9.4'], ++ '9.6': ['10.0'], ++ '9': ['10.0'] ++ } ++ ), ++)) ++def test_load_ipu_paths_for_flavour(monkeypatch, flavour, expected_result): ++ monkeypatch.setattr(scandefinedipupaths.api, 'current_actor', CurrentActorMockedModified()) ++ ++ result = scandefinedipupaths.load_ipu_paths_for_flavour(flavour=flavour) ++ assert result == expected_result ++ ++ ++_DATA_IPU_PATHS = { ++ '8.10': ['9.4', '9.5', '9.6'], ++ '8.4': ['9.2'], ++ '9.6': ['10.0'], ++ '8': ['9.4', '9.5', '9.6'], ++ '80.0': ['81.0'] ++} ++ ++ ++@suppress_deprecation(IPUPaths) ++@pytest.mark.parametrize(('maj_version', 'expected_result'), ( ++ ('7', []), ++ ( ++ '8', ++ [ ++ IPUPath(source_version='8.10', target_versions=['9.4', '9.5', '9.6']), ++ IPUPath(source_version='8.4', target_versions=['9.2']), ++ IPUPath(source_version='8', target_versions=['9.4', '9.5', '9.6']), ++ ] ++ ), ++ ( ++ '80', ++ [ ++ IPUPath(source_version='80.0', target_versions=['81.0']), ++ ] ++ ), ++ ++ ++)) ++def test_get_filtered_ipu_paths(monkeypatch, maj_version, expected_result): ++ result = scandefinedipupaths.get_filtered_ipu_paths(_DATA_IPU_PATHS, maj_version) ++ result = sorted(result, key=lambda x: x.source_version) ++ assert result == sorted(expected_result, key=lambda x: x.source_version) ++ ++ ++def test_scan_defined_ipu_paths(monkeypatch): ++ # let's try one 'full' happy run ++ monkeypatch.setattr(scandefinedipupaths.api, 'current_actor', CurrentActorMockedModified(src_ver='9.6')) ++ monkeypatch.setattr(scandefinedipupaths.api, 'produce', produce_mocked()) ++ scandefinedipupaths.process() ++ ++ assert scandefinedipupaths.api.produce.called == 1 ++ msg = scandefinedipupaths.api.produce.model_instances[0] ++ assert isinstance(msg, IPUPaths) ++ assert len(msg.data) == 2 ++ assert {i.source_version for i in msg.data} == {'9', '9.6'} +diff --git a/repos/system_upgrade/common/models/ipupaths.py b/repos/system_upgrade/common/models/ipupaths.py +new file mode 100644 +index 00000000..5469f25e +--- /dev/null ++++ b/repos/system_upgrade/common/models/ipupaths.py +@@ -0,0 +1,43 @@ ++from leapp.models import fields, Model ++from leapp.topics import SystemInfoTopic ++from leapp.utils.deprecation import deprecated ++ ++ ++class IPUPath(Model): ++ """ ++ Represent upgrade paths from a source system version. ++ ++ This model is not supposed to be produced nor consumed directly by any actor. ++ See `IPUPaths` instead. ++ """ ++ topic = SystemInfoTopic ++ ++ source_version = fields.String() ++ """Version of a particular source system.""" ++ ++ target_versions = fields.List(fields.String()) ++ """List of defined target system versions for the `source_version` system.""" ++ ++ ++@deprecated( ++ since="2025-02-01", ++ message="This model is temporary and not assumed to be used in any actors." ++) ++class IPUPaths(Model): ++ """ ++ Defined Upgrade paths from the source system major version and used upgrade flavour. ++ ++ In example for the RHEL 8.10 system with the 'default' upgrade flavour it will ++ contain information about all defined upgrade paths from any RHEL 8 system ++ for the 'default' flavour (other flavour can be e.g. 'saphana' for systems ++ with SAP HANA installed. ++ ++ Note this model is marked as deprecated now as it is considered as a temporary ++ solution. It can be removed in any future release! ++ """ ++ topic = SystemInfoTopic ++ ++ data = fields.List(fields.Model(IPUPath)) ++ """ ++ List of defined (filtered) upgrade paths. ++ """ +-- +2.48.1 + diff --git a/0062-Verify-supported-target-OS-version-in-actors.patch b/0062-Verify-supported-target-OS-version-in-actors.patch new file mode 100644 index 0000000..9fb4ba9 --- /dev/null +++ b/0062-Verify-supported-target-OS-version-in-actors.patch @@ -0,0 +1,385 @@ +From 0a5f66e7d04e41f25a87781cc2e8fb1601cfe70e Mon Sep 17 00:00:00 2001 +From: tomasfratrik +Date: Tue, 14 Jan 2025 14:59:04 +0100 +Subject: [PATCH 62/63] Verify supported target OS version in actors + +Originally when user specified the target system release using +`--target` CLI option the verification has been performed immediately +as only supported releases have been listed as possible choices for +this option. The benefit of this solution was that users did not have +to wait for all other checks to realize they execute leapp probably +incorrectly. Unfortunately, + * number of users do not understand why only some versions are supported + * users upgrading with via various webUIs presenting only leapp reports + could not see the error message available in terminal + +To resolve this problem the checks are moved into actors so in case +of specified unsupported target version the information is present +in generated leapp reports. + +Current behaviour is like this: + * in case of invalid input (incorrect format of input data) the hard + error is raised as before immediately. Malformed input data will + not be processed anyhow by any actors + * report error when the specified target major version is not direct + successor of the current system version. I.e. specify 10.0 when + upgrading from RHEL 8 (only RHEL 9 is acceptable). + * this prevents number of cryptic errors as actors are not prepared + for this situation + * report standard inhibitor if the target release is not in the defined + upgrade path, unless LEAPP_UNSUPPORTED=1 + * running leapp in unsupported (devel) mode skips the inhibitor and + entire report + +Additional changes: +* Update error message when format of target version is incorrect to + clarify the expected version format + +jira: RHEL-51072 + +Co-authored-by: Petr Stodulk +--- + commands/command_utils.py | 13 +-- + commands/preupgrade/__init__.py | 3 +- + commands/upgrade/__init__.py | 3 +- + .../common/actors/checktargetversion/actor.py | 22 +++++ + .../libraries/checktargetversion.py | 86 ++++++++++++++++++ + .../tests/test_checktargetversion.py | 90 +++++++++++++++++++ + .../libraries/ipuworkflowconfig.py | 26 +++++- + 7 files changed, 229 insertions(+), 14 deletions(-) + create mode 100644 repos/system_upgrade/common/actors/checktargetversion/actor.py + create mode 100644 repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py + create mode 100644 repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py + +diff --git a/commands/command_utils.py b/commands/command_utils.py +index 190f5f03..84b9de1b 100644 +--- a/commands/command_utils.py ++++ b/commands/command_utils.py +@@ -28,7 +28,10 @@ def check_version(version): + :return: release tuple + """ + if not re.match(VERSION_REGEX, version): +- raise CommandError('Unexpected format of target version: {}'.format(version)) ++ raise CommandError( ++ "Unexpected format of target version: {}. " ++ "The required format is 'X.Y' (major and minor version).".format(version) ++ ) + return version.split('.') + + +@@ -126,7 +129,6 @@ def vet_upgrade_path(args): + Make sure the user requested upgrade_path is a supported one. + If LEAPP_DEVEL_TARGET_RELEASE is set then it's value is not vetted against upgrade_paths_map but used as is. + +- :raises: `CommandError` if the specified upgrade_path is not supported + :return: `tuple` (target_release, flavor) + """ + flavor = get_upgrade_flavour() +@@ -135,13 +137,6 @@ def vet_upgrade_path(args): + check_version(env_version_override) + return (env_version_override, flavor) + target_release = args.target or get_target_version(flavor) +- supported_target_versions = get_supported_target_versions(flavor) +- if target_release not in supported_target_versions: +- raise CommandError( +- "Upgrade to {to} for {flavor} upgrade path is not supported, possible choices are {choices}".format( +- to=target_release, +- flavor=flavor, +- choices=','.join(supported_target_versions))) + return (target_release, flavor) + + +diff --git a/commands/preupgrade/__init__.py b/commands/preupgrade/__init__.py +index 631eca6b..c1fabbbd 100644 +--- a/commands/preupgrade/__init__.py ++++ b/commands/preupgrade/__init__.py +@@ -28,8 +28,7 @@ from leapp.utils.output import beautify_actor_exception, report_errors, report_i + choices=['ga', 'e4s', 'eus', 'aus'], + value_type=str.lower) # This allows the choices to be case insensitive + @command_opt('iso', help='Use provided target RHEL installation image to perform the in-place upgrade.') +-@command_opt('target', choices=command_utils.get_supported_target_versions(), +- help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( ++@command_opt('target', help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( + command_utils.get_upgrade_flavour())) + @command_opt('report-schema', help='Specify report schema version for leapp-report.json', + choices=['1.0.0', '1.1.0', '1.2.0'], default=get_config().get('report', 'schema')) +diff --git a/commands/upgrade/__init__.py b/commands/upgrade/__init__.py +index 3dedd438..608099ac 100644 +--- a/commands/upgrade/__init__.py ++++ b/commands/upgrade/__init__.py +@@ -34,8 +34,7 @@ from leapp.utils.output import beautify_actor_exception, report_errors, report_i + choices=['ga', 'e4s', 'eus', 'aus'], + value_type=str.lower) # This allows the choices to be case insensitive + @command_opt('iso', help='Use provided target RHEL installation image to perform the in-place upgrade.') +-@command_opt('target', choices=command_utils.get_supported_target_versions(), +- help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( ++@command_opt('target', help='Specify RHEL version to upgrade to for {} detected upgrade flavour'.format( + command_utils.get_upgrade_flavour())) + @command_opt('report-schema', help='Specify report schema version for leapp-report.json', + choices=['1.0.0', '1.1.0', '1.2.0'], default=get_config().get('report', 'schema')) +diff --git a/repos/system_upgrade/common/actors/checktargetversion/actor.py b/repos/system_upgrade/common/actors/checktargetversion/actor.py +new file mode 100644 +index 00000000..291ce3da +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checktargetversion/actor.py +@@ -0,0 +1,22 @@ ++from leapp.actors import Actor ++from leapp.libraries.actor import checktargetversion ++from leapp.models import IPUPaths ++from leapp.reporting import Report ++from leapp.tags import ChecksPhaseTag, IPUWorkflowTag ++ ++ ++class CheckTargetVersion(Actor): ++ """ ++ Check that the target system version is supported by the upgrade process. ++ ++ Invoke inhibitor if the target system is not supported. ++ Allow unsupported target if `LEAPP_UNSUPPORTED=1` is set. ++ """ ++ ++ name = 'check_target_version' ++ consumes = (IPUPaths,) ++ produces = (Report,) ++ tags = (ChecksPhaseTag, IPUWorkflowTag) ++ ++ def process(self): ++ checktargetversion.process() +diff --git a/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py +new file mode 100644 +index 00000000..0df1ece2 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checktargetversion/libraries/checktargetversion.py +@@ -0,0 +1,86 @@ ++from leapp import reporting ++from leapp.exceptions import StopActorExecutionError ++from leapp.libraries.common.config import get_env, version ++from leapp.libraries.stdlib import api ++from leapp.models import IPUPaths ++from leapp.utils.deprecation import suppress_deprecation ++ ++FMT_LIST_SEPARATOR = '\n - ' ++ ++ ++@suppress_deprecation(IPUPaths) ++def get_supported_target_versions(): ++ ipu_paths = next(api.consume(IPUPaths), None) ++ src_version = version.get_source_version() ++ if not ipu_paths: ++ # NOTE: missing unit-tests. Unexpected situation and the solution ++ # is possibly temporary ++ raise StopActorExecutionError('Missing the IPUPaths message. Cannot determine defined upgrade paths.') ++ for ipu_path in ipu_paths.data: ++ if ipu_path.source_version == src_version: ++ return ipu_path.target_versions ++ ++ # Nothing discovered. Current src_version is not already supported or not yet. ++ # Problem of supported source versions is handled now separately in other ++ # actors. Fallbak from X.Y versioning to major version only. ++ api.current_logger().warning( ++ 'Cannot discover support upgrade path for this system release: {}' ++ .format(src_version) ++ ) ++ maj_version = version.get_source_major_version() ++ for ipu_path in ipu_paths.data: ++ if ipu_path.source_version == maj_version: ++ return ipu_path.target_versions ++ ++ # Completely unknown ++ api.current_logger().warning( ++ 'Cannot discover supported upgrade path for this system major version: {}' ++ .format(maj_version) ++ ) ++ return [] ++ ++ ++def process(): ++ target_version = version.get_target_version() ++ supported_target_versions = get_supported_target_versions() ++ ++ if target_version in supported_target_versions: ++ api.current_logger().info('Target version is supported. Continue.') ++ return ++ ++ if get_env('LEAPP_UNSUPPORTED', '0') == '1': ++ api.current_logger().warning( ++ 'Upgrading to an unsupported version of the target system but LEAPP_UNSUPPORTED=1. Continue.' ++ ) ++ return ++ ++ # inhibit the upgrade - unsupported target and leapp running in production mode ++ hint = ( ++ 'Choose a supported version of the target OS for the upgrade.' ++ ' Alternatively, if you require to upgrade using an unsupported upgrade path,' ++ ' set the `LEAPP_UNSUPPORTED=1` environment variable to confirm you' ++ ' want to upgrade on your own risk.' ++ ) ++ ++ reporting.create_report([ ++ reporting.Title('Specified version of the target system is not supported'), ++ reporting.Summary( ++ 'The in-place upgrade to the specified version ({tgt_ver}) of the target system' ++ ' 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}' ++ .format( ++ sep=FMT_LIST_SEPARATOR, ++ ver_list=FMT_LIST_SEPARATOR.join(supported_target_versions), ++ tgt_ver=target_version ++ ) ++ ), ++ reporting.Groups([reporting.Groups.INHIBITOR]), ++ reporting.Severity(reporting.Severity.HIGH), ++ reporting.Remediation(hint=hint), ++ reporting.ExternalLink( ++ url='https://access.redhat.com/articles/4263361', ++ title='Supported in-place upgrade paths for Red Hat Enterprise Linux' ++ ) ++ ]) +diff --git a/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py b/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py +new file mode 100644 +index 00000000..07391e7a +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checktargetversion/tests/test_checktargetversion.py +@@ -0,0 +1,90 @@ ++import os ++ ++import pytest ++ ++from leapp import reporting ++from leapp.libraries.actor import checktargetversion ++from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked ++from leapp.libraries.stdlib import api ++from leapp.models import IPUPath, IPUPaths ++from leapp.utils.deprecation import suppress_deprecation ++from leapp.utils.report import is_inhibitor ++ ++ ++# It must be in a function so we can suppress the deprecation warning in tests. ++@suppress_deprecation(IPUPaths) ++def _get_upgrade_paths_data(): ++ return IPUPaths(data=[ ++ IPUPath(source_version='7.9', target_versions=['8.10']), ++ IPUPath(source_version='8.10', target_versions=['9.4', '9.5', '9.6']), ++ IPUPath(source_version='9.6', target_versions=['10.0']), ++ IPUPath(source_version='7', target_versions=['8.10']), ++ IPUPath(source_version='8', target_versions=['9.4', '9.5', '9.6']), ++ IPUPath(source_version='9', target_versions=['10.0']) ++ ]) ++ ++ ++@pytest.fixture ++def setup_monkeypatch(monkeypatch): ++ """Fixture to set up common monkeypatches.""" ++ ++ def _setup(source_version, target_version, leapp_unsupported='0'): ++ curr_actor_mocked = CurrentActorMocked( ++ src_ver=source_version, ++ dst_ver=target_version, ++ envars={'LEAPP_UNSUPPORTED': leapp_unsupported}, ++ msgs=[_get_upgrade_paths_data()] ++ ) ++ monkeypatch.setattr(api, 'current_actor', curr_actor_mocked) ++ monkeypatch.setattr(api, 'current_logger', logger_mocked()) ++ monkeypatch.setattr(reporting, 'create_report', create_report_mocked()) ++ return _setup ++ ++ ++@pytest.mark.parametrize(('source_version', 'target_version', 'leapp_unsupported'), [ ++ # LEAPP_UNSUPPORTED=0 ++ ('7.9', '9.0', '0'), ++ ('8.10', '9.0', '0'), ++ ('9.6', '10.1', '0'), ++ ('7', '9.0', '0'), ++ ('8', '9.0', '0'), ++ ('9', '10.1', '0'), ++ # LEAPP_UNSUPPORTED=1 ++ ('7.9', '9.0', '1'), ++ ('8.10', '9.0', '1'), ++ ('9.6', '10.1', '1'), ++ ('7', '9.0', '1'), ++ ('8', '9.0', '1'), ++ ('9', '10.1', '1'), ++]) ++def test_unsuppoted_paths(setup_monkeypatch, source_version, target_version, leapp_unsupported): ++ setup_monkeypatch(source_version, target_version, leapp_unsupported) ++ ++ if leapp_unsupported == '1': ++ checktargetversion.process() ++ assert reporting.create_report.called == 0 ++ assert api.current_logger.warnmsg ++ else: ++ checktargetversion.process() ++ assert reporting.create_report.called == 1 ++ assert is_inhibitor(reporting.create_report.report_fields) ++ ++ ++@pytest.mark.parametrize(('source_version', 'target_version'), [ ++ ('7.9', '8.10'), ++ ('8.10', '9.4'), ++ ('8.10', '9.5'), ++ ('8.10', '9.6'), ++ ('9.6', '10.0'), ++ ('7', '8.10'), ++ ('8', '9.4'), ++ ('8', '9.5'), ++ ('8', '9.6'), ++ ('9', '10.0'), ++]) ++def test_supported_paths(setup_monkeypatch, source_version, target_version): ++ setup_monkeypatch(source_version, target_version, leapp_unsupported='0') ++ ++ checktargetversion.process() ++ assert reporting.create_report.called == 0 ++ assert api.current_logger.infomsg +diff --git a/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py b/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py +index 9e213f64..749b3347 100644 +--- a/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py ++++ b/repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py +@@ -64,17 +64,41 @@ def get_os_release(path): + details={'details': str(e)}) + + ++def check_target_major_version(curr_version, target_version): ++ required_major_version = int(curr_version.split('.')[0]) + 1 ++ specified_major_version = int(target_version.split('.')[0]) ++ if specified_major_version != required_major_version: ++ raise StopActorExecutionError( ++ message='Specified invalid major version of the target system', ++ details={ ++ 'Specified target major version': str(specified_major_version), ++ 'Required target major version': str(required_major_version), ++ 'hint': ( ++ 'The in-place upgrade is possible only to the next system' ++ ' major version: {ver}. Specify a valid version of the' ++ ' target system when running leapp.' ++ ' For more information about supported in-place upgrade paths' ++ ' follow: https://access.redhat.com/articles/4263361' ++ .format(ver=required_major_version) ++ ) ++ } ++ ) ++ ++ + def produce_ipu_config(actor): + flavour = os.environ.get('LEAPP_UPGRADE_PATH_FLAVOUR') + target_version = os.environ.get('LEAPP_UPGRADE_PATH_TARGET_RELEASE') + os_release = get_os_release('/etc/os-release') ++ source_version = os_release.version_id ++ ++ check_target_major_version(source_version, target_version) + + actor.produce(IPUConfig( + leapp_env_vars=get_env_vars(), + os_release=os_release, + architecture=platform.machine(), + version=Version( +- source=os_release.version_id, ++ source=source_version, + target=target_version + ), + kernel=get_booted_kernel(), +-- +2.48.1 + diff --git a/0063-Add-inhibitor-for-unsupported-XFS.patch b/0063-Add-inhibitor-for-unsupported-XFS.patch new file mode 100644 index 0000000..54ae280 --- /dev/null +++ b/0063-Add-inhibitor-for-unsupported-XFS.patch @@ -0,0 +1,1402 @@ +From c5accf4fc657f31ecb70148bf34d2875d1633a9c Mon Sep 17 00:00:00 2001 +From: David Kubek +Date: Sun, 8 Dec 2024 20:33:48 +0100 +Subject: [PATCH 63/63] Add inhibitor for unsupported XFS + +Kernel in RHEL 10 drops support of XFS v4 format file systems and such +file systems will not be possible to mount there anymore. Also, RHEL 10 +systems will be challenged by Y2K38 problem. XFS file system resolves +that by `bigtime` feature, however, this feature could be manually +disabled when creating the file system and mainly it's available since +RHEL 9. So all XFS file systems created earlier will not have this +enabled - unless users do it on RHEL 9 manually. Note that this will be +problem for all systems with XFS which upgraded from previous RHEL +versions. + +For this reason, inhibit the upgrade if any mounted XFS file systems +have old XFS v4 format (detect `crc=0`). + +Instead of inhibiting upgrade when the `bigtime` feature is disabled +(`bigtime=0` or missing) a low severity report is created as there is +still time until this issue will be present and other solutions are +being worked on. + +Introduces new model `XFSInfoFacts` which collects parsed information +about all mounted XFS file systems. Note that as we use +only a few values from `xfs_info` utility, models specify now just this +limited amount of values as well to limit the burden of maintanance and +risk of issues. However expected design of the model is already prepared +and other expected fields are commented out in the code to make the +further extension in future more simple for others. All values of XFS +attributes are now represented as strings. + +JIRA: RHELMISC-8212, RHEL-60034, RHEL-52309 +--- + .../common/actors/xfsinfoscanner/actor.py | 24 +- + .../libraries/xfsinfoscanner.py | 202 ++++++-- + .../tests/unit_test_xfsinfoscanner.py | 432 +++++++++++++----- + repos/system_upgrade/common/models/xfsinfo.py | 147 ++++++ + .../el9toel10/actors/checkoldxfs/actor.py | 34 ++ + .../checkoldxfs/libraries/checkoldxfs.py | 141 ++++++ + .../checkoldxfs/tests/test_checkoldxfs.py | 214 +++++++++ + 7 files changed, 1027 insertions(+), 167 deletions(-) + create mode 100644 repos/system_upgrade/common/models/xfsinfo.py + create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py + create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py + create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py + +diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py b/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py +index ebc7e17e..a97e51fa 100644 +--- a/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py ++++ b/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py +@@ -1,22 +1,32 @@ + from leapp.actors import Actor + from leapp.libraries.actor.xfsinfoscanner import scan_xfs +-from leapp.models import StorageInfo, XFSPresence ++from leapp.models import StorageInfo, XFSInfoFacts, XFSPresence + from leapp.tags import FactsPhaseTag, IPUWorkflowTag + + + class XFSInfoScanner(Actor): + """ +- This actor scans all mounted mountpoints for XFS information ++ This actor scans all mounted mountpoints for XFS information. ++ ++ The actor checks the `StorageInfo` message, which contains details about ++ the system's storage. For each mountpoint reported, it determines whether ++ the filesystem is XFS and collects information about its configuration. ++ Specifically, it identifies whether the XFS filesystem is using `ftype=0`, ++ which requires special handling for overlay filesystems. ++ ++ The actor produces two types of messages: ++ ++ - `XFSPresence`: Indicates whether any XFS use `ftype=0`, and lists the ++ mountpoints where `ftype=0` is used. ++ ++ - `XFSInfoFacts`: Contains detailed metadata about all XFS mountpoints. ++ This includes sections parsed from the `xfs_info` command. + +- The actor will check each mountpoint reported in the StorageInfo message, if the mountpoint is a partition with XFS +- using ftype = 0. The actor will produce a message with the findings. +- It will contain a list of all XFS mountpoints with ftype = 0 so that those mountpoints can be handled appropriately +- for the overlayfs that is going to be created. + """ + + name = 'xfs_info_scanner' + consumes = (StorageInfo,) +- produces = (XFSPresence,) ++ produces = (XFSPresence, XFSInfoFacts,) + tags = (FactsPhaseTag, IPUWorkflowTag,) + + def process(self): +diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py b/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py +index fafe456e..7e4de355 100644 +--- a/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py ++++ b/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py +@@ -1,13 +1,74 @@ + import os ++import re + + from leapp.libraries.stdlib import api, CalledProcessError, run +-from leapp.models import StorageInfo, XFSPresence ++from leapp.models import ( ++ StorageInfo, ++ XFSInfo, ++ XFSInfoData, ++ XFSInfoFacts, ++ XFSInfoLog, ++ XFSInfoMetaData, ++ XFSInfoNaming, ++ XFSInfoRealtime, ++ XFSPresence ++) ++ ++ ++def scan_xfs(): ++ storage_info_msgs = api.consume(StorageInfo) ++ storage_info = next(storage_info_msgs, None) ++ ++ if list(storage_info_msgs): ++ api.current_logger().warning( ++ 'Unexpectedly received more than one StorageInfo message.' ++ ) ++ ++ fstab_data = set() ++ mount_data = set() ++ if storage_info: ++ fstab_data = scan_xfs_fstab(storage_info.fstab) ++ mount_data = scan_xfs_mount(storage_info.mount) ++ ++ mountpoints = fstab_data | mount_data ++ ++ xfs_infos = {} ++ for mountpoint in mountpoints: ++ content = read_xfs_info(mountpoint) ++ if content is None: ++ continue ++ ++ xfs_info = parse_xfs_info(content) ++ xfs_infos[mountpoint] = xfs_info ++ ++ mountpoints_ftype0 = [ ++ mountpoint ++ for mountpoint in xfs_infos ++ if is_without_ftype(xfs_infos[mountpoint]) ++ ] ++ ++ # By now, we only have XFS mountpoints and check whether or not it has ++ # ftype = 0 ++ api.produce(XFSPresence( ++ present=len(mountpoints) > 0, ++ without_ftype=len(mountpoints_ftype0) > 0, ++ mountpoints_without_ftype=mountpoints_ftype0, ++ )) ++ ++ api.produce( ++ XFSInfoFacts( ++ mountpoints=[ ++ generate_xfsinfo_for_mountpoint(xfs_infos[mountpoint], mountpoint) ++ for mountpoint in xfs_infos ++ ] ++ ) ++ ) + + + def scan_xfs_fstab(data): + mountpoints = set() + for entry in data: +- if entry.fs_vfstype == "xfs": ++ if entry.fs_vfstype == 'xfs': + mountpoints.add(entry.fs_file) + + return mountpoints +@@ -16,49 +77,116 @@ def scan_xfs_fstab(data): + def scan_xfs_mount(data): + mountpoints = set() + for entry in data: +- if entry.tp == "xfs": ++ if entry.tp == 'xfs': + mountpoints.add(entry.mount) + + return mountpoints + + +-def is_xfs_without_ftype(mp): +- if not os.path.ismount(mp): +- # Check if mp is actually a mountpoint +- api.current_logger().warning('{} is not mounted'.format(mp)) +- return False ++def read_xfs_info(mp): ++ if not is_mountpoint(mp): ++ return None ++ + try: +- xfs_info = run(['/usr/sbin/xfs_info', '{}'.format(mp)], split=True) ++ result = run(['/usr/sbin/xfs_info', '{}'.format(mp)], split=True) + except CalledProcessError as err: +- api.current_logger().warning('Error during command execution: {}'.format(err)) +- return False +- +- for l in xfs_info['stdout']: +- if 'ftype=0' in l: +- return True +- +- return False +- ++ api.current_logger().warning( ++ 'Error during command execution: {}'.format(err) ++ ) ++ return None + +-def scan_xfs(): +- storage_info_msgs = api.consume(StorageInfo) +- storage_info = next(storage_info_msgs, None) ++ return result['stdout'] + +- if list(storage_info_msgs): +- api.current_logger().warning('Unexpectedly received more than one StorageInfo message.') + +- fstab_data = set() +- mount_data = set() +- if storage_info: +- fstab_data = scan_xfs_fstab(storage_info.fstab) +- mount_data = scan_xfs_mount(storage_info.mount) +- +- mountpoints = fstab_data | mount_data +- mountpoints_ftype0 = list(filter(is_xfs_without_ftype, mountpoints)) ++def is_mountpoint(mp): ++ if not os.path.ismount(mp): ++ # Check if mp is actually a mountpoint ++ api.current_logger().warning('{} is not mounted'.format(mp)) ++ return False + +- # By now, we only have XFS mountpoints and check whether or not it has ftype = 0 +- api.produce(XFSPresence( +- present=len(mountpoints) > 0, +- without_ftype=len(mountpoints_ftype0) > 0, +- mountpoints_without_ftype=mountpoints_ftype0, +- )) ++ return True ++ ++ ++def parse_xfs_info(content): ++ """ ++ This parser reads the output of the ``xfs_info`` command. ++ ++ In general the pattern is:: ++ ++ section =sectionkey key1=value1 key2=value2, key3=value3 ++ = key4=value4 ++ nextsec =sectionkey sectionvalue key=value otherkey=othervalue ++ ++ Sections are continued over lines as per RFC822. The first equals ++ sign is column-aligned, and the first key=value is too, but the ++ rest seems to be comma separated. Specifiers come after the first ++ equals sign, and sometimes have a value property, but sometimes not. ++ ++ NOTE: This function is adapted from [1] ++ ++ [1]: https://github.com/RedHatInsights/insights-core/blob/master/insights/parsers/xfs_info.py ++ """ ++ ++ xfs_info = {} ++ ++ info_re = re.compile(r'^(?P
[\w-]+)?\s*' + ++ r'=(?:(?P\S+)(?:\s(?P\w+))?)?' + ++ r'\s+(?P\w.*\w)$' ++ ) ++ keyval_re = re.compile(r'(?P[\w-]+)=(?P\d+(?: blks)?)') ++ ++ sect_info = None ++ ++ for line in content: ++ match = info_re.search(line) ++ if match: ++ if match.group('section'): ++ # Change of section - make new sect_info dict and link ++ sect_info = {} ++ xfs_info[match.group('section')] = sect_info ++ if match.group('specifier'): ++ sect_info['specifier'] = match.group('specifier') ++ if match.group('specval'): ++ sect_info['specifier_value'] = match.group('specval') ++ for key, value in keyval_re.findall(match.group('keyvaldata')): ++ sect_info[key] = value ++ ++ # Normalize strings ++ xfs_info = { ++ str(section): { ++ str(attr): str(value) ++ for attr, value in sect_info.items() ++ } ++ for section, sect_info in xfs_info.items() ++ } ++ ++ return xfs_info ++ ++ ++def is_without_ftype(xfs_info): ++ return xfs_info['naming'].get('ftype', '') == '0' ++ ++ ++def generate_xfsinfo_for_mountpoint(xfs_info, mountpoint): ++ result = XFSInfo( ++ mountpoint=mountpoint, ++ meta_data=XFSInfoMetaData( ++ device=xfs_info['meta-data']['specifier'], ++ bigtime=xfs_info['meta-data'].get('bigtime'), ++ crc=xfs_info['meta-data'].get('crc'), ++ ), ++ data=XFSInfoData( ++ bsize=xfs_info['data']['bsize'], ++ blocks=xfs_info['data']['blocks'] ++ ), ++ naming=XFSInfoNaming( ++ ftype=xfs_info['naming']['ftype'] ++ ), ++ log=XFSInfoLog( ++ bsize=xfs_info['log']['bsize'], ++ blocks=xfs_info['log']['blocks'] ++ ), ++ realtime=XFSInfoRealtime(), ++ ) ++ ++ return result +diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py b/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py +index 4ac6a0d1..71f46b47 100644 +--- a/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py ++++ b/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py +@@ -3,7 +3,147 @@ import os + from leapp.libraries.actor import xfsinfoscanner + from leapp.libraries.common.testutils import produce_mocked + from leapp.libraries.stdlib import api, CalledProcessError +-from leapp.models import FstabEntry, MountEntry, StorageInfo, SystemdMountEntry, XFSPresence ++from leapp.models import ( ++ FstabEntry, ++ MountEntry, ++ StorageInfo, ++ SystemdMountEntry, ++ XFSInfo, ++ XFSInfoData, ++ XFSInfoFacts, ++ XFSInfoLog, ++ XFSInfoMetaData, ++ XFSInfoNaming, ++ XFSInfoRealtime, ++ XFSPresence ++) ++ ++TEST_XFS_INFO_FTYPE1 = """ ++meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks ++ = sectsz=512 attr=2, projid32bit=1 ++ = crc=1 finobt=0 spinodes=0 ++data = bsize=4096 blocks=524288, imaxpct=25 ++ = sunit=0 swidth=0 blks ++naming =version 2 bsize=4096 ascii-ci=0 ftype=1 ++log =internal bsize=4096 blocks=2560, version=2 ++ = sectsz=512 sunit=0 blks, lazy-count=1 ++realtime =none extsz=4096 blocks=0, rtextents=0 ++""" ++TEST_XFS_INFO_FTYPE1_PARSED = { ++ 'meta-data': { ++ 'agcount': '4', ++ 'agsize': '131072 blks', ++ 'attr': '2', ++ 'crc': '1', ++ 'finobt': '0', ++ 'isize': '512', ++ 'projid32bit': '1', ++ 'sectsz': '512', ++ 'specifier': '/dev/loop0', ++ 'spinodes': '0' ++ }, ++ 'data': { ++ 'blocks': '524288', ++ 'bsize': '4096', ++ 'imaxpct': '25', ++ 'sunit': '0', ++ 'swidth': '0 blks' ++ }, ++ 'naming': { ++ 'ascii-ci': '0', ++ 'bsize': '4096', ++ 'ftype': '1', ++ 'specifier': 'version', ++ 'specifier_value': '2' ++ }, ++ 'log': { ++ 'blocks': '2560', ++ 'bsize': '4096', ++ 'lazy-count': '1', ++ 'sectsz': '512', ++ 'specifier': 'internal', ++ 'sunit': '0 blks', ++ 'version': '2' ++ }, ++ 'realtime': { ++ 'blocks': '0', ++ 'extsz': '4096', ++ 'rtextents': '0', ++ 'specifier': 'none' ++ }, ++} ++TEST_XFS_INFO_FTYPE1_MODEL = XFSInfo( ++ mountpoint='/', ++ meta_data=XFSInfoMetaData(device='/dev/loop0', bigtime=None, crc='1'), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(ftype='1'), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime() ++) ++ ++ ++TEST_XFS_INFO_FTYPE0 = """ ++meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks ++ = sectsz=512 attr=2, projid32bit=1 ++ = crc=1 finobt=0 spinodes=0 ++data = bsize=4096 blocks=524288, imaxpct=25 ++ = sunit=0 swidth=0 blks ++naming =version 2 bsize=4096 ascii-ci=0 ftype=0 ++log =internal bsize=4096 blocks=2560, version=2 ++ = sectsz=512 sunit=0 blks, lazy-count=1 ++realtime =none extsz=4096 blocks=0, rtextents=0 ++""" ++TEST_XFS_INFO_FTYPE0_PARSED = { ++ 'meta-data': { ++ 'agcount': '4', ++ 'agsize': '131072 blks', ++ 'attr': '2', ++ 'crc': '1', ++ 'finobt': '0', ++ 'isize': '512', ++ 'projid32bit': '1', ++ 'sectsz': '512', ++ 'specifier': '/dev/loop0', ++ 'spinodes': '0' ++ }, ++ 'data': { ++ 'blocks': '524288', ++ 'bsize': '4096', ++ 'imaxpct': '25', ++ 'sunit': '0', ++ 'swidth': '0 blks' ++ }, ++ 'naming': { ++ 'ascii-ci': '0', ++ 'bsize': '4096', ++ 'ftype': '0', ++ 'specifier': 'version', ++ 'specifier_value': '2' ++ }, ++ 'log': { ++ 'blocks': '2560', ++ 'bsize': '4096', ++ 'lazy-count': '1', ++ 'sectsz': '512', ++ 'specifier': 'internal', ++ 'sunit': '0 blks', ++ 'version': '2' ++ }, ++ 'realtime': { ++ 'blocks': '0', ++ 'extsz': '4096', ++ 'rtextents': '0', ++ 'specifier': 'none' ++ } ++} ++TEST_XFS_INFO_FTYPE0_MODEL = XFSInfo( ++ mountpoint='/var', ++ meta_data=XFSInfoMetaData(device='/dev/loop0', bigtime=None, crc='1'), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(ftype='0'), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime() ++) + + + class run_mocked(object): +@@ -15,29 +155,10 @@ class run_mocked(object): + self.called += 1 + self.args = args + +- with_ftype = {'stdout': [ +- "meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks", +- " = sectsz=512 attr=2, projid32bit=1", +- " = crc=1 finobt=0 spinodes=0", +- "data = bsize=4096 blocks=524288, imaxpct=25", +- " = sunit=0 swidth=0 blks", +- "naming =version 2 bsize=4096 ascii-ci=0 ftype=1", +- "log =internal bsize=4096 blocks=2560, version=2", +- " = sectsz=512 sunit=0 blks, lazy-count=1", +- "realtime =none extsz=4096 blocks=0, rtextents=0"]} +- +- without_ftype = {'stdout': [ +- "meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks", +- " = sectsz=512 attr=2, projid32bit=1", +- " = crc=1 finobt=0 spinodes=0", +- "data = bsize=4096 blocks=524288, imaxpct=25", +- " = sunit=0 swidth=0 blks", +- "naming =version 2 bsize=4096 ascii-ci=0 ftype=0", +- "log =internal bsize=4096 blocks=2560, version=2", +- " = sectsz=512 sunit=0 blks, lazy-count=1", +- "realtime =none extsz=4096 blocks=0, rtextents=0"]} +- +- if "/var" in self.args: ++ with_ftype = {'stdout': TEST_XFS_INFO_FTYPE1.splitlines()} ++ without_ftype = {'stdout': TEST_XFS_INFO_FTYPE0.splitlines()} ++ ++ if '/var' in self.args: + return without_ftype + + return with_ftype +@@ -45,163 +166,228 @@ class run_mocked(object): + + def test_scan_xfs_fstab(monkeypatch): + fstab_data_no_xfs = { +- "fs_spec": "/dev/mapper/fedora-home", +- "fs_file": "/home", +- "fs_vfstype": "ext4", +- "fs_mntops": "defaults,x-systemd.device-timeout=0", +- "fs_freq": "1", +- "fs_passno": "2"} ++ 'fs_spec': '/dev/mapper/fedora-home', ++ 'fs_file': '/home', ++ 'fs_vfstype': 'ext4', ++ 'fs_mntops': 'defaults,x-systemd.device-timeout=0', ++ 'fs_freq': '1', ++ 'fs_passno': '2'} + + mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_no_xfs)]) + assert not mountpoints + + fstab_data_xfs = { +- "fs_spec": "/dev/mapper/rhel-root", +- "fs_file": "/", +- "fs_vfstype": "xfs", +- "fs_mntops": "defaults", +- "fs_freq": "0", +- "fs_passno": "0"} ++ 'fs_spec': '/dev/mapper/rhel-root', ++ 'fs_file': '/', ++ 'fs_vfstype': 'xfs', ++ 'fs_mntops': 'defaults', ++ 'fs_freq': '0', ++ 'fs_passno': '0'} + + mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_xfs)]) +- assert mountpoints == {"/"} ++ assert mountpoints == {'/'} + + + def test_scan_xfs_mount(monkeypatch): + mount_data_no_xfs = { +- "name": "tmpfs", +- "mount": "/run/snapd/ns", +- "tp": "tmpfs", +- "options": "rw,nosuid,nodev,seclabel,mode=755"} ++ 'name': 'tmpfs', ++ 'mount': '/run/snapd/ns', ++ 'tp': 'tmpfs', ++ 'options': 'rw,nosuid,nodev,seclabel,mode=755'} + + mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_no_xfs)]) + assert not mountpoints + + mount_data_xfs = { +- "name": "/dev/vda1", +- "mount": "/boot", +- "tp": "xfs", +- "options": "rw,relatime,seclabel,attr2,inode64,noquota"} ++ 'name': '/dev/vda1', ++ 'mount': '/boot', ++ 'tp': 'xfs', ++ 'options': 'rw,relatime,seclabel,attr2,inode64,noquota'} + + mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_xfs)]) +- assert mountpoints == {"/boot"} +- ++ assert mountpoints == {'/boot'} + +-def test_is_xfs_without_ftype(monkeypatch): +- monkeypatch.setattr(xfsinfoscanner, "run", run_mocked()) +- monkeypatch.setattr(os.path, "ismount", lambda _: True) + +- assert xfsinfoscanner.is_xfs_without_ftype("/var") +- assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /var" ++def test_is_without_ftype(monkeypatch): ++ assert xfsinfoscanner.is_without_ftype(TEST_XFS_INFO_FTYPE0_PARSED) ++ assert not xfsinfoscanner.is_without_ftype(TEST_XFS_INFO_FTYPE1_PARSED) ++ assert not xfsinfoscanner.is_without_ftype({'naming': {}}) + +- assert not xfsinfoscanner.is_xfs_without_ftype("/boot") +- assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /boot" + +- +-def test_is_xfs_command_failed(monkeypatch): ++def test_read_xfs_info_failed(monkeypatch): + def _run_mocked_exception(*args, **kwargs): +- raise CalledProcessError(message="No such file or directory", command=["xfs_info", "/nosuchmountpoint"], ++ raise CalledProcessError(message='No such file or directory', command=['xfs_info', '/nosuchmountpoint'], + result=1) + # not a mountpoint +- monkeypatch.setattr(os.path, "ismount", lambda _: False) +- monkeypatch.setattr(xfsinfoscanner, "run", _run_mocked_exception) +- assert not xfsinfoscanner.is_xfs_without_ftype("/nosuchmountpoint") ++ monkeypatch.setattr(os.path, 'ismount', lambda _: False) ++ monkeypatch.setattr(xfsinfoscanner, 'run', _run_mocked_exception) ++ assert xfsinfoscanner.read_xfs_info('/nosuchmountpoint') is None + # a real mountpoint but something else caused command to fail +- monkeypatch.setattr(os.path, "ismount", lambda _: True) +- assert not xfsinfoscanner.is_xfs_without_ftype("/nosuchmountpoint") ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) ++ assert xfsinfoscanner.read_xfs_info('/nosuchmountpoint') is None + + +-def test_scan_xfs(monkeypatch): +- monkeypatch.setattr(xfsinfoscanner, "run", run_mocked()) +- monkeypatch.setattr(os.path, "ismount", lambda _: True) ++def test_scan_xfs_no_xfs(monkeypatch): ++ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) + + def consume_no_xfs_message_mocked(*models): + yield StorageInfo() + +- monkeypatch.setattr(api, "consume", consume_no_xfs_message_mocked) +- monkeypatch.setattr(api, "produce", produce_mocked()) ++ monkeypatch.setattr(api, 'consume', consume_no_xfs_message_mocked) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) + + xfsinfoscanner.scan_xfs() +- assert api.produce.called == 1 +- assert len(api.produce.model_instances) == 1 +- assert isinstance(api.produce.model_instances[0], XFSPresence) +- assert not api.produce.model_instances[0].present +- assert not api.produce.model_instances[0].without_ftype +- assert not api.produce.model_instances[0].mountpoints_without_ftype ++ ++ assert api.produce.called == 2 ++ assert len(api.produce.model_instances) == 2 ++ ++ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence)) ++ assert not xfs_presence.present ++ assert not xfs_presence.without_ftype ++ assert not xfs_presence.mountpoints_without_ftype ++ ++ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts)) ++ assert xfs_info_facts.mountpoints == [] ++ ++ ++def test_scan_xfs_ignored_xfs(monkeypatch): ++ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) + + def consume_ignored_xfs_message_mocked(*models): + mount_data = { +- "name": "/dev/vda1", +- "mount": "/boot", +- "tp": "xfs", +- "options": "rw,relatime,seclabel,attr2,inode64,noquota"} ++ 'name': '/dev/vda1', ++ 'mount': '/boot', ++ 'tp': 'xfs', ++ 'options': 'rw,relatime,seclabel,attr2,inode64,noquota' ++ } + yield StorageInfo(mount=[MountEntry(**mount_data)]) + +- monkeypatch.setattr(api, "consume", consume_ignored_xfs_message_mocked) +- monkeypatch.setattr(api, "produce", produce_mocked()) ++ monkeypatch.setattr(api, 'consume', consume_ignored_xfs_message_mocked) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) + + xfsinfoscanner.scan_xfs() +- assert api.produce.called == 1 +- assert len(api.produce.model_instances) == 1 +- assert isinstance(api.produce.model_instances[0], XFSPresence) +- assert api.produce.model_instances[0].present +- assert not api.produce.model_instances[0].without_ftype +- assert not api.produce.model_instances[0].mountpoints_without_ftype ++ ++ assert api.produce.called == 2 ++ assert len(api.produce.model_instances) == 2 ++ ++ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence)) ++ assert xfs_presence.present ++ assert not xfs_presence.without_ftype ++ assert not xfs_presence.mountpoints_without_ftype ++ ++ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts)) ++ assert len(xfs_info_facts.mountpoints) == 1 ++ assert xfs_info_facts.mountpoints[0].mountpoint == '/boot' ++ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE1_MODEL.meta_data ++ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE1_MODEL.data ++ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE1_MODEL.naming ++ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE1_MODEL.log ++ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE1_MODEL.realtime ++ ++ ++def test_scan_xfs_with_ftype(monkeypatch): ++ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) + + def consume_xfs_with_ftype_message_mocked(*models): + fstab_data = { +- "fs_spec": "/dev/mapper/rhel-root", +- "fs_file": "/", +- "fs_vfstype": "xfs", +- "fs_mntops": "defaults", +- "fs_freq": "0", +- "fs_passno": "0"} ++ 'fs_spec': '/dev/mapper/rhel-root', ++ 'fs_file': '/', ++ 'fs_vfstype': 'xfs', ++ 'fs_mntops': 'defaults', ++ 'fs_freq': '0', ++ 'fs_passno': '0'} + yield StorageInfo(fstab=[FstabEntry(**fstab_data)]) + +- monkeypatch.setattr(api, "consume", consume_xfs_with_ftype_message_mocked) +- monkeypatch.setattr(api, "produce", produce_mocked()) ++ monkeypatch.setattr(api, 'consume', consume_xfs_with_ftype_message_mocked) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) + + xfsinfoscanner.scan_xfs() +- assert api.produce.called == 1 +- assert len(api.produce.model_instances) == 1 +- assert isinstance(api.produce.model_instances[0], XFSPresence) +- assert api.produce.model_instances[0].present +- assert not api.produce.model_instances[0].without_ftype +- assert not api.produce.model_instances[0].mountpoints_without_ftype ++ ++ assert api.produce.called == 2 ++ assert len(api.produce.model_instances) == 2 ++ ++ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence)) ++ assert xfs_presence.present ++ assert not xfs_presence.without_ftype ++ assert not xfs_presence.mountpoints_without_ftype ++ ++ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts)) ++ assert len(xfs_info_facts.mountpoints) == 1 ++ assert xfs_info_facts.mountpoints[0].mountpoint == '/' ++ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE1_MODEL.meta_data ++ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE1_MODEL.data ++ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE1_MODEL.naming ++ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE1_MODEL.log ++ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE1_MODEL.realtime ++ ++ ++def test_scan_xfs_without_ftype(monkeypatch): ++ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) + + def consume_xfs_without_ftype_message_mocked(*models): + fstab_data = { +- "fs_spec": "/dev/mapper/rhel-root", +- "fs_file": "/var", +- "fs_vfstype": "xfs", +- "fs_mntops": "defaults", +- "fs_freq": "0", +- "fs_passno": "0"} ++ 'fs_spec': '/dev/mapper/rhel-root', ++ 'fs_file': '/var', ++ 'fs_vfstype': 'xfs', ++ 'fs_mntops': 'defaults', ++ 'fs_freq': '0', ++ 'fs_passno': '0'} + yield StorageInfo(fstab=[FstabEntry(**fstab_data)]) + +- monkeypatch.setattr(api, "consume", consume_xfs_without_ftype_message_mocked) +- monkeypatch.setattr(api, "produce", produce_mocked()) ++ monkeypatch.setattr(api, 'consume', consume_xfs_without_ftype_message_mocked) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) + + xfsinfoscanner.scan_xfs() +- assert api.produce.called == 1 +- assert len(api.produce.model_instances) == 1 +- assert isinstance(api.produce.model_instances[0], XFSPresence) +- assert api.produce.model_instances[0].present +- assert api.produce.model_instances[0].without_ftype +- assert api.produce.model_instances[0].mountpoints_without_ftype +- assert len(api.produce.model_instances[0].mountpoints_without_ftype) == 1 +- assert api.produce.model_instances[0].mountpoints_without_ftype[0] == '/var' ++ ++ assert api.produce.called == 2 ++ assert len(api.produce.model_instances) == 2 ++ ++ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence)) ++ assert xfs_presence.present ++ assert xfs_presence.without_ftype ++ assert xfs_presence.mountpoints_without_ftype ++ ++ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts)) ++ assert len(xfs_info_facts.mountpoints) == 1 ++ assert xfs_info_facts.mountpoints[0].mountpoint == '/var' ++ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE0_MODEL.meta_data ++ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE0_MODEL.data ++ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE0_MODEL.naming ++ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE0_MODEL.log ++ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE0_MODEL.realtime ++ ++ ++def test_scan_xfs_no_message(monkeypatch): ++ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) ++ monkeypatch.setattr(os.path, 'ismount', lambda _: True) + + def consume_no_message_mocked(*models): + yield None + +- monkeypatch.setattr(api, "consume", consume_no_message_mocked) +- monkeypatch.setattr(api, "produce", produce_mocked()) ++ monkeypatch.setattr(api, 'consume', consume_no_message_mocked) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) + + xfsinfoscanner.scan_xfs() +- assert api.produce.called == 1 +- assert len(api.produce.model_instances) == 1 +- assert isinstance(api.produce.model_instances[0], XFSPresence) +- assert not api.produce.model_instances[0].present +- assert not api.produce.model_instances[0].without_ftype +- assert not api.produce.model_instances[0].mountpoints_without_ftype ++ ++ assert api.produce.called == 2 ++ assert len(api.produce.model_instances) == 2 ++ ++ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence)) ++ assert not xfs_presence.present ++ assert not xfs_presence.without_ftype ++ assert not xfs_presence.mountpoints_without_ftype ++ ++ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts)) ++ assert not xfs_info_facts.mountpoints ++ ++ ++def test_parse_xfs_info(monkeypatch): ++ xfs_info = xfsinfoscanner.parse_xfs_info(TEST_XFS_INFO_FTYPE0.splitlines()) ++ assert xfs_info == TEST_XFS_INFO_FTYPE0_PARSED ++ ++ xfs_info = xfsinfoscanner.parse_xfs_info(TEST_XFS_INFO_FTYPE1.splitlines()) ++ assert xfs_info == TEST_XFS_INFO_FTYPE1_PARSED +diff --git a/repos/system_upgrade/common/models/xfsinfo.py b/repos/system_upgrade/common/models/xfsinfo.py +new file mode 100644 +index 00000000..276ca287 +--- /dev/null ++++ b/repos/system_upgrade/common/models/xfsinfo.py +@@ -0,0 +1,147 @@ ++from leapp.models import fields, Model ++from leapp.topics import SystemInfoTopic ++ ++ ++class XFSInfoSection(Model): ++ """ ++ Represents a section of `xfs_info`. ++ """ ++ ++ topic = SystemInfoTopic ++ ++ ++class XFSInfoMetaData(XFSInfoSection): ++ """ ++ Represents the `meta-data` section of `xfs_info`. ++ """ ++ ++ device = fields.String() ++ bigtime = fields.Nullable(fields.String()) ++ crc = fields.Nullable(fields.String()) ++ ++ # NOTE(dkubek): meta-data section might also contain the following fields ++ # which are not being used right now ++ ++ # isize = fields.String() ++ # agcount = fields.String() ++ # agsize = fields.String() ++ # sectsz = fields.String() ++ # attr = fields.String() ++ # projid32bit = fields.String() ++ # finobt = fields.String() ++ # sparse = fields.String() ++ # rmapbt = fields.String() ++ # reflink = fields.String() ++ # inobtcount = fields.String() ++ # nrext64 = fields.String() ++ ++ ++class XFSInfoData(XFSInfoSection): ++ """ ++ Represents the `data` section of `xfs_info`. ++ """ ++ ++ bsize = fields.String() ++ blocks = fields.String() ++ ++ # NOTE(dkubek): data section might also contain the following fields ++ # which are not being used right now ++ ++ # imaxpct = fields.String() ++ # sunit = fields.String() ++ # swidth = fields.String() ++ ++ ++class XFSInfoNaming(XFSInfoSection): ++ """ ++ Represents the `naming` section of `xfs_info`. ++ """ ++ ++ ftype = fields.Nullable(fields.String()) ++ ++ # NOTE(dkubek): naming section might also contain the following fields ++ # which are not being used right now ++ ++ # version = fields.String() ++ # bsize = fields.String() ++ # ascii_ci = fields.String() ++ ++ ++class XFSInfoLog(XFSInfoSection): ++ """ ++ Represents the `log` section of `xfs_info`. ++ """ ++ ++ bsize = fields.String() ++ blocks = fields.String() ++ ++ # NOTE(dkubek): log section might also contain the following fields ++ # which are not being used right now ++ ++ # internal = fields.String() ++ # version = fields.String() ++ # sectsz = fields.String() ++ # sunit = fields.String() ++ # lazy_count = fields.String() ++ ++ ++class XFSInfoRealtime(XFSInfoSection): ++ """ ++ Represents the `realtime` section of `xfs_info`. ++ """ ++ ++ # NOTE(dkubek): realtime section might also contain the following fields ++ # which are not being used right now ++ ++ # extsz = fields.String() ++ # blocks = fields.String() ++ # rtextents = fields.String() ++ ++ ++class XFSInfo(Model): ++ """ ++ A message containing the parsed results from `xfs_info` command for given mountpoint. ++ ++ Attributes are stored as key-value pairs. Optional section attribute is ++ stored under the identifier 'specifier'. ++ """ ++ topic = SystemInfoTopic ++ ++ mountpoint = fields.String() ++ """ ++ Mountpoint containing the XFS filesystem. ++ """ ++ ++ meta_data = fields.Model(XFSInfoMetaData) ++ """ ++ Attributes of 'meta-data' section. ++ """ ++ ++ data = fields.Model(XFSInfoData) ++ """ ++ Attributes of 'data' section. ++ """ ++ ++ naming = fields.Model(XFSInfoNaming) ++ """ ++ Attributes of 'naming' section. ++ """ ++ ++ log = fields.Model(XFSInfoLog) ++ """ ++ Attributes of 'log' section. ++ """ ++ ++ realtime = fields.Model(XFSInfoRealtime) ++ """ ++ Attributes of 'realtime' section. ++ """ ++ ++ ++class XFSInfoFacts(Model): ++ """ ++ Message containing the xfs info for all mounted XFS filesystems. ++ """ ++ topic = SystemInfoTopic ++ ++ mountpoints = fields.List(fields.Model(XFSInfo)) +diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py +new file mode 100644 +index 00000000..42974108 +--- /dev/null ++++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py +@@ -0,0 +1,34 @@ ++import leapp.libraries.actor.checkoldxfs as checkoldxfs ++from leapp.actors import Actor ++from leapp.models import XFSInfoFacts ++from leapp.reporting import Report ++from leapp.tags import ChecksPhaseTag, IPUWorkflowTag ++ ++ ++class CheckOldXFS(Actor): ++ """ ++ Check mounted XFS file systems. ++ ++ RHEL 10 requires XFS filesystems to use the v5 format (crc=1 is a good ++ indicator). XFS v4 format filesystems will be incompatible with the target ++ kernel and it will not be possible to mount them. If any such filesystem is ++ detected, the upgrade will be inhibited. ++ ++ Also, RHEL 10 is going to address the Y2K38 problem, which requires bigger ++ timestamps to support dates beyond 2038-01-19. Since RHEL 9, the "bigtime" ++ feature (indicated by bigtime=1 in xfs_info) has been introduced to resolve ++ this issue. If an XFS filesystem lacks this feature, a report will be ++ created to just raise the awareness about the potential problem to the ++ user, but the upgrade will not be blocked. This will probably be resolved ++ automatically during the RHEL 10 lifetime, it is still 10+ years in future ++ until this could have any real impact. ++ ++ """ ++ ++ name = 'check_old_xfs' ++ consumes = (XFSInfoFacts,) ++ produces = (Report,) ++ tags = (ChecksPhaseTag, IPUWorkflowTag,) ++ ++ def process(self): ++ checkoldxfs.process() +diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py +new file mode 100644 +index 00000000..0069ae7f +--- /dev/null ++++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py +@@ -0,0 +1,141 @@ ++from leapp import reporting ++from leapp.exceptions import StopActorExecutionError ++from leapp.libraries.stdlib import api ++from leapp.models import XFSInfoFacts ++ ++RHEL_9_TO_10_BACKUP_RESTORE_LINK = 'https://red.ht/rhel_9_to_10_backup_restore_xfs' ++ ++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() ++ ++ invalid_bigtime = [] ++ invalid_crc = [] ++ for xfs_info in xfs_info_facts.mountpoints: ++ if not _has_valid_bigtime(xfs_info): ++ api.current_logger().debug( ++ 'Mountpoint {} has invalid bigtime'.format(xfs_info.mountpoint) ++ ) ++ invalid_bigtime.append(xfs_info.mountpoint) ++ ++ if not _has_valid_crc(xfs_info): ++ api.current_logger().debug( ++ 'Mountpoint {} has invalid crc'.format(xfs_info.mountpoint) ++ ) ++ invalid_crc.append(xfs_info.mountpoint) ++ ++ if invalid_bigtime or invalid_crc: ++ _inhibit_upgrade(invalid_bigtime, invalid_crc) ++ ++ return ++ ++ api.current_logger().debug('All XFS system detected are valid.') ++ ++ ++def _get_xfs_info_facts(): ++ msgs = api.consume(XFSInfoFacts) ++ ++ xfs_info_facts = next(msgs, None) ++ if xfs_info_facts is None: ++ raise StopActorExecutionError('Could not retrieve XFSInfoFacts!') ++ ++ if next(msgs, None): ++ api.current_logger().warning( ++ 'Unexpectedly received more than one XFSInfoFacts message.') ++ ++ return xfs_info_facts ++ ++ ++def _has_valid_bigtime(xfs_info): ++ return xfs_info.meta_data.bigtime == '1' ++ ++ ++def _has_valid_crc(xfs_info): ++ return xfs_info.meta_data.crc == '1' ++ ++ ++def _inhibit_upgrade(invalid_bigtime, invalid_crc): ++ if invalid_bigtime: ++ _report_bigtime(invalid_bigtime) ++ ++ if invalid_crc: ++ _inhibit_crc(invalid_crc) ++ ++ ++def _report_bigtime(invalid_bigtime): ++ title = 'Detected XFS filesystems without bigtime feature.' ++ summary = ( ++ 'The XFS v5 filesystem format introduced the "bigtime" feature in RHEL 9,' ++ ' to support timestamps beyond the year 2038. XFS filesystems that' ++ ' do not have the "bigtime" feature enabled remain vulnerable to timestamp' ++ ' overflow issues. It is recommended to enable this feature on all' ++ ' XFS filesystems to ensure long-term compatibility and prevent potential' ++ ' failures.' ++ ' Following XFS file systems have not enabled the "bigtime" feature:{}' ++ .format(''.join(_formatted_list_output(invalid_bigtime))) ++ ) ++ ++ # NOTE(pstodulk): This will affect any system which upgraded from RHEL 8 so ++ # it is clear that such FS will have to be modified offline e.g. from ++ # initramfs - and that we speak about significant number of systems. So ++ # this should be improved yet. E.g. to update the initramfs having ++ # xfs_admin inside and working: ++ # # dracut -I "/usr/sbin/xfs_admin /usr/bin/expr" -f ++ # Note that it seems that it could be done without xfs_admin, using xfs_db ++ # only - which is present already. ++ remediation_hint = ( ++ 'Enable the "bigtime" feature on XFS v5 filesystems using the command:' ++ '\n\txfs_admin -O bigtime=1 \n\n' ++ 'Note that for older XFS v5 filesystems this step can only be done' ++ ' offline right now (i.e. without the filesystem mounted).' ++ ) ++ ++ reporting.create_report([ ++ reporting.Title(title), ++ reporting.Summary(summary), ++ reporting.Remediation(hint=remediation_hint), ++ reporting.ExternalLink( ++ title='XFS supports bigtime feature', ++ url='https://red.ht/rhel-9-xfs-bigtime', ++ ), ++ reporting.Severity(reporting.Severity.LOW), ++ ]) ++ ++ ++def _inhibit_crc(invalid_crc): ++ title = 'Detected XFS filesystems incompatible with target kernel.' ++ summary = ( ++ 'XFS v4 format has been deprecated and it has been removed from' ++ ' 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))) ++ ) ++ remediation_hint = ( ++ 'Migrate XFS v4 filesystems to new XFS v5 format.' ++ ' For filesystems hosting data, perform a back up, reformat, and restore procedure.' ++ ' Refer to official documentation for details.' ++ ' For filesystems hosting the system a clean installation is recommended instead.' ++ ) ++ ++ reporting.create_report([ ++ reporting.Title(title), ++ reporting.Summary(summary), ++ reporting.Remediation(hint=remediation_hint), ++ reporting.ExternalLink( ++ title='Backing up an XFS file system', ++ url='https://red.ht/rhel-9-xfs-backup', ++ ), ++ reporting.ExternalLink( ++ title='Restoring an XFS file system from backup', ++ url='https://red.ht/rhel-9-xfs-restore-from-backup', ++ ), ++ reporting.Severity(reporting.Severity.HIGH), ++ reporting.Groups([reporting.Groups.INHIBITOR]), ++ ]) +diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py +new file mode 100644 +index 00000000..2dd6eaa7 +--- /dev/null ++++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py +@@ -0,0 +1,214 @@ ++import pytest ++ ++from leapp import reporting ++from leapp.exceptions import StopActorExecutionError ++from leapp.libraries.actor import checkoldxfs ++from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked ++from leapp.libraries.stdlib import api ++from leapp.models import ( ++ XFSInfo, ++ XFSInfoData, ++ XFSInfoFacts, ++ XFSInfoLog, ++ XFSInfoMetaData, ++ XFSInfoNaming, ++ XFSInfoRealtime ++) ++from leapp.utils.report import is_inhibitor ++ ++ ++def test_has_valid_bigtime_passes(): ++ """ ++ Test _has_valid_bigtime passes for correct attributes. ++ """ ++ ++ xfs_info = XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=XFSInfoMetaData(bigtime='1', crc=None, device='/dev/vda'), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ) ++ ++ assert checkoldxfs._has_valid_bigtime(xfs_info) ++ ++ ++@pytest.mark.parametrize("bigtime", ['0', '', '', None]) ++def test_has_valid_bigtime_fail(bigtime): ++ """ ++ Test _has_valid_bigtime fails for incorrect attributes. ++ """ ++ ++ xfs_info = XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=( ++ XFSInfoMetaData(bigtime=bigtime, crc=None, device='/dev/vda') ++ if bigtime ++ else XFSInfoMetaData(device='/dev/vda') ++ ), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ) ++ ++ assert not checkoldxfs._has_valid_bigtime(xfs_info) ++ ++ ++def test_has_valid_crc_passes(): ++ """ ++ Test _has_valid_crc passes for correct attributes. ++ """ ++ ++ xfs_info = XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=XFSInfoMetaData(crc='1', bigtime=None, device='/dev/vda'), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ) ++ ++ assert checkoldxfs._has_valid_crc(xfs_info) ++ ++ ++@pytest.mark.parametrize("crc", ['0', '', '', None]) ++def test_has_valid_crc_fail(crc): ++ """ ++ Test _has_valid_crc fails for incorrect attributes. ++ """ ++ ++ xfs_info = XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=( ++ XFSInfoMetaData(crc=crc, bigtime=None, device='/dev/vda') ++ if crc ++ else XFSInfoMetaData(device='/dev/vda') ++ ), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ) ++ ++ assert not checkoldxfs._has_valid_crc(xfs_info) ++ ++ ++def test_get_xfs_info_facts_info_single_entry(monkeypatch): ++ xfs_info_facts = XFSInfoFacts(mountpoints=[]) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[xfs_info_facts])) ++ ++ result = checkoldxfs._get_xfs_info_facts() ++ assert result == xfs_info_facts ++ ++ ++def test_get_workaround_efi_info_multiple_entries(monkeypatch): ++ logger = logger_mocked() ++ xfs_info_facts = XFSInfoFacts(mountpoints=[]) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked( ++ msgs=[xfs_info_facts, xfs_info_facts])) ++ monkeypatch.setattr(api, 'current_logger', logger) ++ ++ result = checkoldxfs._get_xfs_info_facts() ++ assert result == xfs_info_facts ++ assert 'Unexpectedly received more than one XFSInfoFacts message.' in logger.warnmsg ++ ++ ++def test_get_workaround_efi_info_no_entry(monkeypatch): ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[])) ++ ++ with pytest.raises(StopActorExecutionError, match='Could not retrieve XFSInfoFacts!'): ++ checkoldxfs._get_xfs_info_facts() ++ ++ ++def test_valid_xfs_passes(monkeypatch): ++ """ ++ Test no report is generated for valid XFS mountpoint ++ """ ++ ++ logger = logger_mocked() ++ monkeypatch.setattr(reporting, "create_report", create_report_mocked()) ++ monkeypatch.setattr(api, 'current_logger', logger) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[ ++ XFSInfoFacts( ++ mountpoints=[ ++ XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=XFSInfoMetaData(crc='1', bigtime='1', device='/dev/vda'), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ), ++ ] ++ ) ++ ])) ++ ++ checkoldxfs.process() ++ ++ assert 'All XFS system detected are valid.' in logger.dbgmsg[0] ++ assert not reporting.create_report.called ++ ++ ++@pytest.mark.parametrize( ++ 'valid_crc,valid_bigtime', ++ [ ++ (False, True), ++ (True, False), ++ (False, False), ++ ] ++) ++def test_unsupported_xfs(monkeypatch, valid_crc, valid_bigtime): ++ """ ++ Test report is generated for unsupported XFS mountpoint ++ """ ++ ++ logger = logger_mocked() ++ monkeypatch.setattr(reporting, "create_report", create_report_mocked()) ++ monkeypatch.setattr(api, 'current_logger', logger) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[ ++ XFSInfoFacts( ++ mountpoints=[ ++ XFSInfo( ++ mountpoint='/MOUNTPOINT', ++ meta_data=XFSInfoMetaData( ++ crc='1' if valid_crc else '0', ++ bigtime='1' if valid_bigtime else '0', ++ device='/dev/vda', ++ ), ++ data=XFSInfoData(blocks='524288', bsize='4096'), ++ naming=XFSInfoNaming(), ++ log=XFSInfoLog(blocks='2560', bsize='4096'), ++ realtime=XFSInfoRealtime(), ++ ), ++ ] ++ ) ++ ])) ++ ++ checkoldxfs.process() ++ ++ assert reporting.create_report.called == (int(not valid_crc) + int(not valid_bigtime)) ++ ++ if not valid_crc: ++ reports = [ ++ report ++ for report in reporting.create_report.reports ++ if report.get('title') == 'Detected XFS filesystems incompatible with target kernel.' ++ ] ++ assert reports ++ report = reports[-1] ++ assert 'XFS v4 format has been deprecated' in report.get('summary') ++ assert report['severity'] == reporting.Severity.HIGH ++ assert is_inhibitor(report) ++ ++ if not valid_bigtime: ++ reports = [ ++ report ++ for report in reporting.create_report.reports ++ if report.get('title') == 'Detected XFS filesystems without bigtime feature.' ++ ] ++ assert reports ++ report = reports[-1] ++ assert 'XFS v5 filesystem format introduced the "bigtime" feature' in report.get('summary') ++ assert report['severity'] == reporting.Severity.LOW +-- +2.48.1 + diff --git a/leapp-repository.spec b/leapp-repository.spec index 6f83036..db412f1 100644 --- a/leapp-repository.spec +++ b/leapp-repository.spec @@ -52,7 +52,7 @@ py2_byte_compile "%1" "%2"} Name: leapp-repository Version: 0.21.0 -Release: 5%{?dist} +Release: 6%{?dist} Summary: Repositories for leapp License: ASL 2.0 @@ -118,6 +118,16 @@ Patch0050: 0050-redhatsignedrpmcheck-Add-remediation-hint-and-URL.patch Patch0051: 0051-Update-postgresqlcheck.py.patch Patch0052: 0052-Update-repos-system_upgrade-el8toel9-actors-postgres.patch Patch0053: 0053-Fix-remediation-message-in-the-networkdeprecations-a.patch +Patch0054: 0054-linter-Fix-line-too-long-in-postgresqlcheck.patch +Patch0055: 0055-Fix-typos-in-comments-to-make-spellchecker-happy.patch +Patch0056: 0056-fix-add_upgrade_boot_entry-convert-arg-list-into-a-t.patch +Patch0057: 0057-fix-arm-bootloader-efi-patch-grub.cfg-used-for-upgra.patch +Patch0058: 0058-feat-arm-bootloader-efi-use-separate-BLS-directory-f.patch +Patch0059: 0059-fix-models-move-arm-bootloader-workaround-model-into.patch +Patch0060: 0060-cleanup-8to9-efi-do-not-use-symlinks-or-copy-grub-fi.patch +Patch0061: 0061-Introduce-deprecated-IPUPaths-msg-temporary-solution.patch +Patch0062: 0062-Verify-supported-target-OS-version-in-actors.patch +Patch0063: 0063-Add-inhibitor-for-unsupported-XFS.patch %description @@ -339,6 +349,16 @@ Requires: libdb-utils %patch -P 0051 -p1 %patch -P 0052 -p1 %patch -P 0053 -p1 +%patch -P 0054 -p1 +%patch -P 0055 -p1 +%patch -P 0056 -p1 +%patch -P 0057 -p1 +%patch -P 0058 -p1 +%patch -P 0059 -p1 +%patch -P 0060 -p1 +%patch -P 0061 -p1 +%patch -P 0062 -p1 +%patch -P 0063 -p1 %build @@ -420,6 +440,12 @@ done; # no files here %changelog +* Wed Jan 29 2025 Petr Stodulka - 0.21.0-6 +- Detect XFS file systems with problematic parameters +- Raise an inhibitor if unsupported target version supplied instead of error +- Prevent a possible crash with LiveMode when adding the upgrade boot entry on systems with LVM +- Resolves: RHEL-57043, RHEL-52309, RHEL-60034 + * Fri Jan 17 2025 Petr Stodulka - 0.21.0-5 - Obsolete RHEL9 GPG key signed with SHA1 - Activate LVM VGs with `--sysinit` option to correct the use in the upgrade initramfs