diff --git a/SOURCES/leapp-repository-0.24.0-elevate.patch b/SOURCES/leapp-repository-0.24.0-elevate.patch index 9437a11..9404092 100644 --- a/SOURCES/leapp-repository-0.24.0-elevate.patch +++ b/SOURCES/leapp-repository-0.24.0-elevate.patch @@ -906865,6 +906865,186 @@ index eeca8be0..949a731f 100644 checkmicroarchitecture.process() +diff --git a/repos/system_upgrade/common/actors/checkresumekernelarg/actor.py b/repos/system_upgrade/common/actors/checkresumekernelarg/actor.py +new file mode 100644 +index 00000000..0c047f19 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checkresumekernelarg/actor.py +@@ -0,0 +1,27 @@ ++from leapp.actors import Actor ++from leapp.libraries.actor import checkresumekernelarg ++from leapp.models import KernelCmdline, TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks ++from leapp.tags import ChecksPhaseTag, IPUWorkflowTag ++ ++ ++class CheckResumeKernelArg(Actor): ++ """ ++ Remove the resume argument from the upgrade boot entry. ++ ++ The resume argument points to a swap device used for hibernation. During the ++ upgrade the dracut resume module is excluded, so the resume device cannot be ++ resolved and the system can hang during reboot. Therefore, we remove the ++ resume kernel command line argument from the upgrade boot entry to prevent ++ a possible hang during the upgrade reboot. ++ ++ The argument is restored on the target kernel so hibernation keeps working ++ after the upgrade. ++ """ ++ ++ name = 'check_resume_kernel_arg' ++ consumes = (KernelCmdline,) ++ produces = (TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks) ++ tags = (ChecksPhaseTag, IPUWorkflowTag) ++ ++ def process(self): ++ checkresumekernelarg.process() +diff --git a/repos/system_upgrade/common/actors/checkresumekernelarg/libraries/checkresumekernelarg.py b/repos/system_upgrade/common/actors/checkresumekernelarg/libraries/checkresumekernelarg.py +new file mode 100644 +index 00000000..b881db6b +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checkresumekernelarg/libraries/checkresumekernelarg.py +@@ -0,0 +1,35 @@ ++from leapp.libraries.stdlib import api ++from leapp.models import KernelCmdline, TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks ++ ++ ++def process(): ++ """ ++ Remove resume from the upgrade boot entry and restore it on the target. ++ ++ The upgrade initramfs omits the dracut resume module, so the resume device ++ cannot be resolved during the upgrade boot. This can cause the system to ++ hang waiting for the device. Stripping resume argument from the upgrade kernel ++ command line avoids the hang. The value is added back to the target kernel ++ entry via TargetKernelCmdlineArgTasks so hibernation continues working ++ after the upgrade. ++ """ ++ cmdline = next(api.consume(KernelCmdline), None) ++ if not cmdline: ++ api.current_logger().debug('No KernelCmdline message received, nothing to do.') ++ return ++ ++ resume_args = [arg for arg in cmdline.parameters if arg.key == 'resume'] ++ if not resume_args: ++ api.current_logger().debug('No resume argument found on the kernel command line.') ++ return ++ ++ api.current_logger().info( ++ 'Requesting removal of resume argument from the upgrade kernel command line: %s', ++ ['{}={}'.format(a.key, a.value or '') for a in resume_args] ++ ) ++ ++ api.produce(UpgradeKernelCmdlineArgTasks(to_remove=resume_args)) ++ # When installing the target kernel RPM, the cmdline is copied from the booted system. ++ # As we remove the resume= args, we would accidentally remove them also from the ++ # target entry. Therefore, we add them back in here. ++ api.produce(TargetKernelCmdlineArgTasks(to_add=resume_args)) +diff --git a/repos/system_upgrade/common/actors/checkresumekernelarg/tests/test_checkresumekernelarg.py b/repos/system_upgrade/common/actors/checkresumekernelarg/tests/test_checkresumekernelarg.py +new file mode 100644 +index 00000000..c5f032f2 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/checkresumekernelarg/tests/test_checkresumekernelarg.py +@@ -0,0 +1,100 @@ ++import pytest ++ ++from leapp.libraries.actor import checkresumekernelarg ++from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked ++from leapp.libraries.stdlib import api ++from leapp.models import KernelCmdline, KernelCmdlineArg, TargetKernelCmdlineArgTasks, UpgradeKernelCmdlineArgTasks ++ ++_COMMON_PARAMS = [ ++ KernelCmdlineArg(key='ro', value=None), ++ KernelCmdlineArg(key='root', value='/dev/mapper/rhel-root'), ++] ++ ++ ++@pytest.mark.parametrize('resume_value', [ ++ 'UUID=010b9c5c-5aca-469b-a852-fdf2fefcf817', ++ '/dev/mapper/rhel-swap', ++ '/dev/md/swap', ++ '/dev/md127', ++ '/dev/dm-1', ++]) ++def test_resume_removed_from_upgrade_and_restored_on_target(monkeypatch, resume_value): ++ cmdline = KernelCmdline(parameters=_COMMON_PARAMS + [ ++ KernelCmdlineArg(key='resume', value=resume_value), ++ ]) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline])) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ checkresumekernelarg.process() ++ ++ upgrade_msgs = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)] ++ assert len(upgrade_msgs) == 1 ++ assert len(upgrade_msgs[0].to_remove) == 1 ++ assert upgrade_msgs[0].to_remove[0].key == 'resume' ++ assert upgrade_msgs[0].to_remove[0].value == resume_value ++ ++ target_msgs = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)] ++ assert len(target_msgs) == 1 ++ assert len(target_msgs[0].to_add) == 1 ++ assert target_msgs[0].to_add[0].key == 'resume' ++ assert target_msgs[0].to_add[0].value == resume_value ++ ++ ++def test_multiple_resume_args_all_handled(monkeypatch): ++ cmdline = KernelCmdline(parameters=_COMMON_PARAMS + [ ++ KernelCmdlineArg(key='resume', value='UUID=aaa-bbb'), ++ KernelCmdlineArg(key='resume', value='/dev/md127'), ++ ]) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline])) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ checkresumekernelarg.process() ++ ++ upgrade_msgs = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)] ++ assert len(upgrade_msgs) == 1 ++ removed = {(a.key, a.value) for a in upgrade_msgs[0].to_remove} ++ assert removed == {('resume', 'UUID=aaa-bbb'), ('resume', '/dev/md127')} ++ ++ target_msgs = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)] ++ assert len(target_msgs) == 1 ++ added = {(a.key, a.value) for a in target_msgs[0].to_add} ++ assert added == {('resume', 'UUID=aaa-bbb'), ('resume', '/dev/md127')} ++ ++ ++def test_resume_bare_key_without_value(monkeypatch): ++ cmdline = KernelCmdline(parameters=_COMMON_PARAMS + [ ++ KernelCmdlineArg(key='resume', value=None), ++ ]) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline])) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ checkresumekernelarg.process() ++ ++ upgrade_msgs = [m for m in api.produce.model_instances if isinstance(m, UpgradeKernelCmdlineArgTasks)] ++ assert len(upgrade_msgs) == 1 ++ assert upgrade_msgs[0].to_remove[0].key == 'resume' ++ assert upgrade_msgs[0].to_remove[0].value is None ++ ++ target_msgs = [m for m in api.produce.model_instances if isinstance(m, TargetKernelCmdlineArgTasks)] ++ assert len(target_msgs) == 1 ++ assert target_msgs[0].to_add[0].key == 'resume' ++ assert target_msgs[0].to_add[0].value is None ++ ++ ++def test_no_resume_produces_nothing(monkeypatch): ++ cmdline = KernelCmdline(parameters=_COMMON_PARAMS) ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[cmdline])) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ checkresumekernelarg.process() ++ ++ assert not api.produce.model_instances ++ ++ ++def test_no_kernel_cmdline_message_produces_nothing(monkeypatch): ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[])) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ checkresumekernelarg.process() ++ ++ assert not api.produce.model_instances diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh index 7d470459..af67cab5 100755 --- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/do-upgrade.sh @@ -907231,6 +907411,229 @@ index aa735da3..fdb4c5ac 100644 + to_reinstall=list(to_reinstall), modules_to_reset=list(modules_to_reset.values()), modules_to_enable=list(modules_to_enable.values()))) +diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py +index 900132f2..60355c40 100644 +--- a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py ++++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py +@@ -9,6 +9,28 @@ from leapp.models import LiveModeConfig, StorageInfo, TargetUserSpaceInfo, Upgra + + BIND_MOUNT_SYSROOT_BOOT_UNIT = 'boot.mount' + ++# Virtual/pseudo filesystem types that should be treated as nofail during the upgrade initramfs phase. ++# Their mount units are kept but placed only in .wants (not .requires) directories, so systemd ++# will attempt to mount them but continue booting if the mount fails (e.g. mount point does not ++# exist under /sysroot). ++PSEUDO_FS_TYPES = frozenset([ ++ 'hugetlbfs', ++ 'tmpfs', ++ 'devtmpfs', ++ 'devpts', ++ 'sysfs', ++ 'proc', ++ 'cgroup', ++ 'cgroup2', ++ 'securityfs', ++ 'selinuxfs', ++ 'debugfs', ++ 'mqueue', ++ 'pstore', ++ 'efivarfs', ++ 'bpf', ++]) ++ + + def run_systemd_fstab_generator(output_directory): + api.current_logger().debug( +@@ -110,25 +132,33 @@ def prefix_all_mount_units_with_sysroot(dir_containing_units): + api.current_logger().debug('Original mount unit {} removed.'.format(unit_file_path)) + + +-def is_unit_marked_with_nofail(unit_path: str) -> bool: ++def parse_unit(unit_path: str) -> dict: + try: + with open(unit_path) as in_file: + lines = [line.strip() for line in in_file.readlines()] + except OSError: + api.current_logger().debug( +- 'Could not read the unit {} to determine whether it is nofail.'.format(unit_path) ++ 'Could not read the unit {} to parse it contents.'.format(unit_path) + ) +- return False # This way, the unit will end up in target's '.requires', so it is a safe choice ++ return {} ++ ++ unit_properties = {} + + for line in lines: ++ if not line.startswith(('Options', 'Type')): ++ continue ++ line_fragments = line.split('=', 1) ++ if len(line_fragments) <= 1: ++ continue # Should not happen ++ ++ option_value = [opt.strip() for opt in line_fragments[1].split(',')] ++ + if line.startswith('Options'): +- line_fragments = line.split('=', 1) +- if len(line_fragments) <= 1: +- continue # Should not happen +- used_options = [opt.strip() for opt in line_fragments[1].split(',')] +- if 'nofail' in used_options: +- return True +- return False ++ unit_properties['Options'] = option_value ++ elif line.startswith('Type'): ++ unit_properties['Type'] = option_value[0] ++ ++ return unit_properties + + + def _fix_symlinks_in_dir(dir_containing_mount_units, target_dir): +@@ -164,8 +194,11 @@ def _fix_symlinks_in_dir(dir_containing_mount_units, target_dir): + if not unit_file.endswith('.mount'): + continue + +- is_nofail = is_unit_marked_with_nofail(os.path.join(dir_containing_mount_units, unit_file)) +- if is_nofail and will_units_be_required: ++ unit_full_path = os.path.join(dir_containing_mount_units, unit_file) ++ unit_properties = parse_unit(unit_full_path) ++ is_nofail = 'nofail' in unit_properties.get('Options', tuple()) ++ is_pseudo_fs = unit_properties.get('Type') in PSEUDO_FS_TYPES ++ if (is_nofail or is_pseudo_fs) and will_units_be_required: + continue + + place_fastlink_at = os.path.join(target_dir_path, unit_file) +@@ -226,6 +259,7 @@ def fix_symlinks_in_targets(dir_containing_mount_units): + 'remote-fs-pre.target.requires', + 'remote-fs-pre.target.wants', + ] ++ + for tdir in dir_list: + _fix_symlinks_in_dir(dir_containing_mount_units, tdir) + +diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py +index 99f26ad1..1d4cb80d 100644 +--- a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py ++++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py +@@ -134,6 +134,50 @@ def test_prefix_all_mount_units_with_sysroot(monkeypatch): + assert should_be_deleted == was_deleted + + ++def test_parse_unit_missing_file(monkeypatch, leapp_tmpdir): ++ """A non-existent unit file results in an empty dict without raising.""" ++ monkeypatch.setattr(api, 'current_logger', logger_mocked()) ++ ++ unit_path = os.path.join(leapp_tmpdir, 'does-not-exist.mount') ++ ++ assert mount_unit_generator.parse_unit(unit_path) == {} ++ assert api.current_logger.dbgmsg ++ ++ ++def test_parse_unit_parses_options_and_type(leapp_tmpdir): ++ """Options and Type keys are extracted, Options being split into a list.""" ++ unit_path = os.path.join(leapp_tmpdir, 'home.mount') ++ with open(unit_path, 'w') as unit_file: ++ unit_file.write( ++ '[Unit]\n' ++ 'Description=Mount unit for /home\n' ++ '\n' ++ '[Mount]\n' ++ 'What=/dev/sda1\n' ++ 'Where=/home\n' ++ 'Type=xfs\n' ++ 'Options=defaults,nofail,x-systemd.device-timeout=0\n' ++ ) ++ ++ unit_properties = mount_unit_generator.parse_unit(unit_path) ++ ++ assert unit_properties == { ++ 'Type': 'xfs', ++ 'Options': ['defaults', 'nofail', 'x-systemd.device-timeout=0'], ++ } ++ ++ ++def test_parse_unit_strips_whitespace_around_options(leapp_tmpdir): ++ """Whitespace surrounding comma-separated Options values is stripped.""" ++ unit_path = os.path.join(leapp_tmpdir, 'var.mount') ++ with open(unit_path, 'w') as unit_file: ++ unit_file.write('Options = nofail , x-systemd.device-timeout=0 \n') ++ ++ unit_properties = mount_unit_generator.parse_unit(unit_path) ++ ++ assert unit_properties == {'Options': ['nofail', 'x-systemd.device-timeout=0']} ++ ++ + @pytest.mark.parametrize('dirname', ( + 'local-fs.target.requires', + 'local-fs.target.wants', +@@ -328,18 +372,56 @@ def test_injection_of_sysroot_boot_bindmount_unit(monkeypatch, has_separate_boot + assert was_copyfile_for_sysroot_boot_called + + +-TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files') ++@pytest.mark.parametrize('dirname', ( ++ 'local-fs.target.requires', ++ 'local-fs.target.wants', ++)) ++def test_fix_symlinks_in_dir_skips_pseudo_fs_in_requires(monkeypatch, dirname): ++ """Pseudo FS mount units should be excluded from .requires directories.""" ++ ++ DIR_PATH = os.path.join('/test/dir/', dirname) ++ ++ def mock_rmtree(dir_path): ++ assert dir_path == DIR_PATH ++ ++ def mock_mkdir(dir_path): ++ assert dir_path == DIR_PATH ++ ++ def mock_listdir(dir_path): ++ return ['sysroot-home.mount', 'sysroot-hugepages.mount', 'not-a-mount.service', 'sysroot-nofail.mount'] ++ ++ def mock_os_path_exist(dir_path): ++ assert dir_path == DIR_PATH ++ return dir_path == DIR_PATH + ++ def mock_parse_unit(unit_path): ++ return { ++ 'Type': 'hugetlbfs' if 'hugepages' in unit_path else 'xfs', ++ 'Options': ['nofail'] if 'nofail' in unit_path else [''] ++ } + +-@pytest.mark.parametrize( +- ('unit_filename', 'is_nofail'), +- [ +- ('unit.mount', False), +- ('unit-nofail.mount', True), +- ('non-existing-unit.mount', False), # This file does not exist in the files/ ++ expected_calls = [ ++ ['ln', '-s', '../sysroot-home.mount', os.path.join(DIR_PATH, 'sysroot-home.mount')], + ] +-) +-def test_is_unit_marked_with_nofail(unit_filename, is_nofail): +- unit_path = os.path.join(TEST_DIR, unit_filename) +- determined_no_fail = mount_unit_generator.is_unit_marked_with_nofail(unit_path) +- assert determined_no_fail == is_nofail ++ if dirname.endswith('.wants'): ++ expected_calls.extend(( ++ ['ln', '-s', '../sysroot-hugepages.mount', os.path.join(DIR_PATH, 'sysroot-hugepages.mount')], ++ ['ln', '-s', '../sysroot-nofail.mount', os.path.join(DIR_PATH, 'sysroot-nofail.mount')] ++ )) ++ ++ def mock_run(command): ++ assert command in expected_calls ++ return { ++ "stdout": "", ++ "stderr": "", ++ "exit_code": 0, ++ } ++ ++ monkeypatch.setattr('shutil.rmtree', mock_rmtree) ++ monkeypatch.setattr('os.mkdir', mock_mkdir) ++ monkeypatch.setattr('os.listdir', mock_listdir) ++ monkeypatch.setattr('os.path.exists', mock_os_path_exist) ++ monkeypatch.setattr(mount_unit_generator, 'run', mock_run) ++ monkeypatch.setattr(mount_unit_generator, 'parse_unit', mock_parse_unit) ++ ++ mount_unit_generator._fix_symlinks_in_dir('/test/dir', dirname) diff --git a/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py b/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py index 32e4527b..1e595e9a 100644 --- a/repos/system_upgrade/common/actors/missinggpgkeysinhibitor/libraries/missinggpgkey.py diff --git a/SPECS/leapp-repository.spec b/SPECS/leapp-repository.spec index 8adad31..f9cdb9d 100644 --- a/SPECS/leapp-repository.spec +++ b/SPECS/leapp-repository.spec @@ -53,7 +53,7 @@ py2_byte_compile "%1" "%2"} Epoch: 1 Name: leapp-repository Version: 0.24.0 -Release: 3%{?dist}.elevate.1 +Release: 3%{?dist}.elevate.2 Summary: Repositories for leapp License: ASL 2.0 @@ -567,6 +567,9 @@ fi %changelog +* Fri Aug 07 2026 Yuriy Kohut - 0.24.0-3.elevate.2 +- ELevate vendors support for upstream 0.24.0-3 version (b04d9168519eb0b388abe788577df566899d2eab) + * Thu Jul 25 2026 Matej Matuska - 0.24.0-3 - Requires leapp-framework 6.6+ - Initial implementation of upgrades on systems with configured Software RAID