From 2f853fc90e02e370eb20a80e03733b80a35682cf Mon Sep 17 00:00:00 2001 From: Yuriy Kohut Date: Tue, 30 Sep 2025 10:32:05 +0300 Subject: [PATCH] Update Vendors patch against upstream 47fce173e75408d9a7a26225d389161caf72e244 (0.23.0-1) The package version 0.23.0-1.elevate.2 --- SOURCES/leapp-repository-0.23.0-elevate.patch | 937 ++++++++++++++++++ SPECS/leapp-repository.spec | 5 +- 2 files changed, 941 insertions(+), 1 deletion(-) diff --git a/SOURCES/leapp-repository-0.23.0-elevate.patch b/SOURCES/leapp-repository-0.23.0-elevate.patch index d80e3af..666d54e 100644 --- a/SOURCES/leapp-repository-0.23.0-elevate.patch +++ b/SOURCES/leapp-repository-0.23.0-elevate.patch @@ -3633,6 +3633,217 @@ index 56a94b5d..46c5d9b6 100755 mount -o "remount,$old_opts" "$NEWROOT" exit $result - +diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/module-setup.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/module-setup.sh +index d73060cb..45f98148 100755 +--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/module-setup.sh ++++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/module-setup.sh +@@ -102,7 +102,6 @@ install() { + inst_binary grep + + # script to actually run the upgrader binary +- inst_hook upgrade 49 "$_moddir/mount_usr.sh" + inst_hook upgrade 50 "$_moddir/do-upgrade.sh" + + #NOTE: some clean up?.. ideally, everything should be inside the leapp* +diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/mount_usr.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/mount_usr.sh +deleted file mode 100755 +index 9366ac13..00000000 +--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/85sys-upgrade-redhat/mount_usr.sh ++++ /dev/null +@@ -1,148 +0,0 @@ +-#!/bin/sh +-# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +-# ex: ts=8 sw=4 sts=4 et filetype=sh +- +-type info >/dev/null 2>&1 || . /lib/dracut-lib.sh +- +-export NEWROOT=${NEWROOT:-"/sysroot"} +- +-filtersubvol() { +- _oldifs="$IFS" +- IFS="," +- set "$@" +- IFS="$_oldifs" +- while [ $# -gt 0 ]; do +- case $1 in +- subvol=*) :;; +- *) printf '%s' "${1}," ;; +- esac +- shift +- done +-} +- +-mount_usr() +-{ +- # +- # mount_usr [true | false] +- # Expected a "true" value for the last attempt to mount /usr. On the last +- # attempt, in case of failure drop to shell. +- # +- # Return 0 when everything is all right +- # In case of failure and /usr has been detected: +- # return 2 when $1 is "true" (drop to shell invoked) +- # (note: possibly it's nonsense, but to be sure..) +- # return 1 otherwise +- # +- _last_attempt="$1" +- # check, if we have to mount the /usr filesystem +- while read -r _dev _mp _fs _opts _freq _passno; do +- [ "${_dev%%#*}" != "$_dev" ] && continue +- if [ "$_mp" = "/usr" ]; then +- case "$_dev" in +- LABEL=*) +- _dev="$(echo "$_dev" | sed 's,/,\\x2f,g')" +- _dev="/dev/disk/by-label/${_dev#LABEL=}" +- ;; +- UUID=*) +- _dev="${_dev#block:}" +- _dev="/dev/disk/by-uuid/${_dev#UUID=}" +- ;; +- esac +- +- # shellcheck disable=SC2154 # Variable root is assigned by dracut +- _root_dev=${root#block:} +- +- if strstr "$_opts" "subvol=" && \ +- [ "$(stat -c '%D:%i' "$_root_dev")" = "$(stat -c '%D:%i' "$_dev")" ] && \ +- [ -n "$rflags" ]; then +- # for btrfs subvolumes we have to mount /usr with the same rflags +- rflags=$(filtersubvol "$rflags") +- rflags=${rflags%%,} +- _opts="${_opts:+${_opts},}${rflags}" +- elif getargbool 0 ro; then +- # if "ro" is specified, we want /usr to be mounted read-only +- _opts="${_opts:+${_opts},}ro" +- elif getargbool 0 rw; then +- # if "rw" is specified, we want /usr to be mounted read-write +- _opts="${_opts:+${_opts},}rw" +- fi +- echo "$_dev ${NEWROOT}${_mp} $_fs ${_opts} $_freq $_passno" +- _usr_found="1" +- break +- fi +- done < "${NEWROOT}/etc/fstab" >> /etc/fstab +- +- if [ "$_usr_found" = "" ]; then +- # nothing to do +- return 0 +- fi +- +- info "Mounting /usr with -o $_opts" +- mount "${NEWROOT}/usr" 2>&1 | vinfo +- mount -o remount,rw "${NEWROOT}/usr" +- +- if ismounted "${NEWROOT}/usr"; then +- # success!! +- return 0 +- fi +- +- if [ "$_last_attempt" = "true" ]; then +- warn "Mounting /usr to ${NEWROOT}/usr failed" +- warn "*** Dropping you to a shell; the system will continue" +- warn "*** when you leave the shell." +- action_on_fail +- return 2 +- fi +- +- return 1 +-} +- +- +-try_to_mount_usr() { +- _last_attempt="$1" +- if [ ! -f "${NEWROOT}/etc/fstab" ]; then +- warn "File ${NEWROOT}/etc/fstab doesn't exist." +- return 1 +- fi +- +- # In case we have the LVM command available try make it activate all partitions +- if command -v lvm 2>/dev/null 1>/dev/null; then +- lvm vgchange --sysinit -a y || { +- warn "Detected problem when tried to activate LVM VG." +- if [ "$_last_attempt" != "true" ]; then +- # this is not last execution, retry +- return 1 +- fi +- # NOTE(pstodulk): +- # last execution, so call mount_usr anyway +- # I am not 100% about lvm vgchange exit codes and I am aware of +- # possible warnings, in this last run, let's keep it on mount_usr +- # anyway.. +- } +- fi +- +- mount_usr "$1" +-} +- +-_sleep_timeout=15 +-_last_attempt="false" +-for i in 0 1 2 3 4 5 6 7 8 9 10 11; do +- info "Storage initialisation: Attempt $i of 11. Wait $_sleep_timeout seconds." +- sleep $_sleep_timeout +- if [ $i -eq 11 ]; then +- _last_attempt="true" +- fi +- try_to_mount_usr "$_last_attempt" && break +- +- # something is wrong. In some cases, storage needs more time for the +- # initialisation - especially in case of SAN. +- +- if [ "$_last_attempt" = "true" ]; then +- warn "The last attempt to initialize storage has not been successful." +- warn "Unknown state of the storage. It is possible that upgrade will be stopped." +- break +- fi +- +- warn "Failed attempt to initialize the storage. Retry..." +-done +- +diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/initrd-cleanup-override.conf b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/initrd-cleanup-override.conf +new file mode 100644 +index 00000000..d24e0ef0 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/initrd-cleanup-override.conf +@@ -0,0 +1,3 @@ ++[Service] ++ExecStart= ++ExecStart=-/usr/bin/true +diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/module-setup.sh b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/module-setup.sh +index 06479fb5..30ae57b3 100755 +--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/module-setup.sh ++++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/module-setup.sh +@@ -54,6 +54,17 @@ install() { + ln -sf "../${s}.service" "$upgrade_wantsdir" + done + ++ # Setup modified initrd-cleanup.service in the upgrade initramfs to enable ++ # storage initialisation using systemd-fstab-generator. We want to run the ++ # initrd-parse-etc.service but this one triggers also the initrd-cleanup.service ++ # which triggers the switch-root and isolated actions that basically kills ++ # the original upgrade service when used. ++ # The initrd-parse-etc.service has different content across RHEL systems, ++ # so we override rather initrd-cleanup.service instead as we do not need ++ # that one for the upgrade process. ++ mkdir -p "${unitdir}/initrd-cleanup.service.d" ++ inst_simple "${_moddir}/initrd-cleanup-override.conf" "${unitdir}/initrd-cleanup.service.d/initrd-cleanup-override.conf" ++ + # just try : set another services into the wantsdir + # sysroot.mount \ + # dracut-mount \ +diff --git a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/upgrade.target b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/upgrade.target +index 366b5cab..d2bf7313 100644 +--- a/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/upgrade.target ++++ b/repos/system_upgrade/common/actors/commonleappdracutmodules/files/dracut/90sys-upgrade/upgrade.target +@@ -2,7 +2,7 @@ + Description=System Upgrade + Documentation=man:upgrade.target(7) + # ##sysinit.target sockets.target initrd-root-fs.target initrd-root-device.target initrd-fs.target +-Wants=initrd-root-fs.target initrd-root-device.target initrd-fs.target initrd-usr-fs.target ++Wants=initrd-root-fs.target initrd-root-device.target initrd-fs.target initrd-usr-fs.target initrd-parse-etc.service + Requires=basic.target sysroot.mount +-After=basic.target sysroot.mount ++After=basic.target sysroot.mount initrd-fs.target + AllowIsolate=yes diff --git a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py b/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py index 003f3fc5..9e7bbf4a 100644 --- a/repos/system_upgrade/common/actors/distributionsignedrpmscanner/actor.py @@ -3837,6 +4048,732 @@ index 582a5821..18f2c33f 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/enable_lvm_autoactivation/actor.py b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/actor.py +new file mode 100644 +index 00000000..aba60645 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/actor.py +@@ -0,0 +1,21 @@ ++from leapp.actors import Actor ++from leapp.libraries.actor import enable_lvm_autoactivation as enable_lvm_autoactivation_lib ++from leapp.models import DistributionSignedRPM, UpgradeInitramfsTasks ++from leapp.tags import FactsPhaseTag, IPUWorkflowTag ++ ++ ++class EnableLVMAutoactivation(Actor): ++ """ ++ Enable LVM autoactivation in upgrade initramfs. ++ ++ Produce instructions for upgrade initramfs generation that will result in LVM ++ autoactivation in the initramfs. ++ """ ++ ++ name = 'enable_lvm_autoactivation' ++ consumes = (DistributionSignedRPM,) ++ produces = (UpgradeInitramfsTasks, ) ++ tags = (FactsPhaseTag, IPUWorkflowTag) ++ ++ def process(self): ++ enable_lvm_autoactivation_lib.emit_lvm_autoactivation_instructions() +diff --git a/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/libraries/enable_lvm_autoactivation.py b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/libraries/enable_lvm_autoactivation.py +new file mode 100644 +index 00000000..e312277b +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/libraries/enable_lvm_autoactivation.py +@@ -0,0 +1,21 @@ ++from leapp.libraries.common.rpms import has_package ++from leapp.libraries.stdlib import api ++from leapp.models import DistributionSignedRPM, UpgradeInitramfsTasks ++ ++ ++def emit_lvm_autoactivation_instructions(): ++ if not has_package(DistributionSignedRPM, 'lvm2'): ++ api.current_logger().debug( ++ 'Upgrade initramfs will not autoenable LVM devices - `lvm2` RPM is not installed.' ++ ) ++ return ++ ++ # the 69-dm-lvm.rules trigger pvscan and vgchange when LVM device is detected ++ files_to_include = [ ++ '/usr/sbin/pvscan', ++ '/usr/sbin/vgchange', ++ '/usr/lib/udev/rules.d/69-dm-lvm.rules' ++ ] ++ lvm_autoactivation_instructions = UpgradeInitramfsTasks(include_files=files_to_include) ++ ++ api.produce(lvm_autoactivation_instructions) +diff --git a/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/tests/test_lvm_autoactivation_enablement.py b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/tests/test_lvm_autoactivation_enablement.py +new file mode 100644 +index 00000000..c5150aea +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/enable_lvm_autoactivation/tests/test_lvm_autoactivation_enablement.py +@@ -0,0 +1,50 @@ ++from leapp.libraries.actor import enable_lvm_autoactivation ++from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked ++from leapp.libraries.stdlib import api ++from leapp.models import DistributionSignedRPM, RPM, UpgradeInitramfsTasks ++ ++ ++def test_emit_lvm_autoactivation_instructions_produces_correct_message(monkeypatch): ++ """Test that emit_lvm_autoactivation_instructions produces UpgradeInitramfsTasks with correct files.""" ++ lvm_package = RPM( ++ name='lvm2', ++ version='2', ++ release='1', ++ epoch='1', ++ packager='', ++ arch='x86_64', ++ pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51' ++ ) ++ ++ msgs = [ ++ DistributionSignedRPM(items=[lvm_package]) ++ ] ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs)) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ enable_lvm_autoactivation.emit_lvm_autoactivation_instructions() ++ ++ assert api.produce.called == 1 ++ ++ produced_msg = api.produce.model_instances[0] ++ ++ assert isinstance(produced_msg, UpgradeInitramfsTasks) ++ ++ expected_files = [ ++ '/usr/sbin/pvscan', ++ '/usr/sbin/vgchange', ++ '/usr/lib/udev/rules.d/69-dm-lvm.rules' ++ ] ++ assert produced_msg.include_files == expected_files ++ ++ ++def test_no_action_if_lvm_rpm_missing(monkeypatch): ++ msgs = [ ++ DistributionSignedRPM(items=[]) ++ ] ++ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs)) ++ monkeypatch.setattr(api, 'produce', produce_mocked()) ++ ++ enable_lvm_autoactivation.emit_lvm_autoactivation_instructions() ++ ++ assert api.produce.called == 0 +diff --git a/repos/system_upgrade/common/actors/initramfs/mount_units_generator/actor.py b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/actor.py +new file mode 100644 +index 00000000..5fe25515 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/actor.py +@@ -0,0 +1,22 @@ ++from leapp.actors import Actor ++from leapp.libraries.actor import mount_unit_generator as mount_unit_generator_lib ++from leapp.models import TargetUserSpaceInfo, UpgradeInitramfsTasks ++from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag ++ ++ ++class MountUnitGenerator(Actor): ++ """ ++ Sets up storage initialization using systemd's mount units in the upgrade container. ++ """ ++ ++ name = 'mount_unit_generator' ++ consumes = ( ++ TargetUserSpaceInfo, ++ ) ++ produces = ( ++ UpgradeInitramfsTasks, ++ ) ++ tags = (IPUWorkflowTag, InterimPreparationPhaseTag) ++ ++ def process(self): ++ mount_unit_generator_lib.setup_storage_initialization() +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 +new file mode 100644 +index 00000000..e1060559 +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/libraries/mount_unit_generator.py +@@ -0,0 +1,307 @@ ++import os ++import shutil ++import tempfile ++ ++from leapp.exceptions import StopActorExecutionError ++from leapp.libraries.common import mounting ++from leapp.libraries.stdlib import api, CalledProcessError, run ++from leapp.models import TargetUserSpaceInfo, UpgradeInitramfsTasks ++ ++ ++def run_systemd_fstab_generator(output_directory): ++ api.current_logger().debug( ++ 'Generating mount units for the source system into {}'.format(output_directory) ++ ) ++ ++ try: ++ generator_cmd = [ ++ '/usr/lib/systemd/system-generators/systemd-fstab-generator', ++ output_directory, ++ output_directory, ++ output_directory ++ ] ++ run(generator_cmd) ++ except CalledProcessError as error: ++ api.current_logger().error( ++ 'Failed to generate mount units using systemd-fstab-generator. Error: {}'.format(error) ++ ) ++ details = {'details': str(error)} ++ raise StopActorExecutionError( ++ 'Failed to generate mount units using systemd-fstab-generator', ++ details ++ ) ++ ++ api.current_logger().debug( ++ 'Mount units successfully generated into {}'.format(output_directory) ++ ) ++ ++ ++def _read_unit_file_lines(unit_file_path): # Encapsulate IO for tests ++ with open(unit_file_path) as unit_file: ++ return unit_file.readlines() ++ ++ ++def _write_unit_file_lines(unit_file_path, lines): # Encapsulate IO for tests ++ with open(unit_file_path, 'w') as unit_file: ++ unit_file.write('\n'.join(lines) + '\n') ++ ++ ++def _delete_file(file_path): ++ os.unlink(file_path) ++ ++ ++def _prefix_mount_unit_with_sysroot(mount_unit_path, new_unit_destination): ++ """ ++ Prefix the mount target with /sysroot as expected in the upgrade initramfs. ++ ++ A new mount unit file is written to new_unit_destination. ++ """ ++ # NOTE(pstodulk): Note that right now we update just the 'Where' key, however ++ # what about RequiresMountsFor, .. there could be some hidden dragons. ++ # In case of issues, investigate these values in generated unit files. ++ api.current_logger().debug( ++ 'Prefixing {}\'s mount target with /sysroot. Output will be written to {}'.format( ++ mount_unit_path, ++ new_unit_destination ++ ) ++ ) ++ unit_lines = _read_unit_file_lines(mount_unit_path) ++ ++ output_lines = [] ++ for line in unit_lines: ++ line = line.strip() ++ if not line.startswith('Where='): ++ output_lines.append(line) ++ continue ++ ++ _, destination = line.split('=', 1) ++ new_destination = os.path.join('/sysroot', destination.lstrip('/')) ++ ++ output_lines.append('Where={}'.format(new_destination)) ++ ++ _write_unit_file_lines(new_unit_destination, output_lines) ++ ++ api.current_logger().debug( ++ 'Done. Modified mount unit successfully written to {}'.format(new_unit_destination) ++ ) ++ ++ ++def prefix_all_mount_units_with_sysroot(dir_containing_units): ++ for unit_file_path in os.listdir(dir_containing_units): ++ # systemd requires mount path to be in the unit name ++ modified_unit_destination = 'sysroot-{}'.format(unit_file_path) ++ modified_unit_destination = os.path.join(dir_containing_units, modified_unit_destination) ++ ++ unit_file_path = os.path.join(dir_containing_units, unit_file_path) ++ ++ if not unit_file_path.endswith('.mount'): ++ api.current_logger().debug( ++ 'Skipping {} when prefixing mount units with /sysroot - not a mount unit.'.format( ++ unit_file_path ++ ) ++ ) ++ continue ++ ++ _prefix_mount_unit_with_sysroot(unit_file_path, modified_unit_destination) ++ ++ _delete_file(unit_file_path) ++ api.current_logger().debug('Original mount unit {} removed.'.format(unit_file_path)) ++ ++ ++def _fix_symlinks_in_dir(dir_containing_mount_units, target_dir): ++ """ ++ Fix broken symlinks in given target_dir due to us modifying (renaming) the mount units. ++ ++ The target_dir contains symlinks to the (mount) units that are required ++ in order for the local-fs.target to be reached. However, we renamed these units to reflect ++ that we have changed their mount destinations by prefixing the mount destination with /sysroot. ++ Hence, we regenerate the symlinks. ++ """ ++ ++ target_dir_path = os.path.join(dir_containing_mount_units, target_dir) ++ if not os.path.exists(target_dir_path): ++ api.current_logger().debug( ++ 'The {} directory does not exist. Skipping' ++ .format(target_dir) ++ ) ++ return ++ ++ api.current_logger().debug( ++ 'Removing the old {} directory from {}.' ++ .format(target_dir, dir_containing_mount_units) ++ ) ++ ++ shutil.rmtree(target_dir_path) ++ os.mkdir(target_dir_path) ++ ++ api.current_logger().debug('Populating {} with new symlinks.'.format(target_dir)) ++ ++ for unit_file in os.listdir(dir_containing_mount_units): ++ if not unit_file.endswith('.mount'): ++ continue ++ ++ place_fastlink_at = os.path.join(target_dir_path, unit_file) ++ fastlink_points_to = os.path.join('../', unit_file) ++ try: ++ run(['ln', '-s', fastlink_points_to, place_fastlink_at]) ++ ++ api.current_logger().debug( ++ 'Dependency on {} created.'.format(unit_file) ++ ) ++ except CalledProcessError as err: ++ err_descr = ( ++ 'Failed to create required unit dependencies under {} for the upgrade initramfs.' ++ .format(target_dir) ++ ) ++ details = {'details': str(err)} ++ raise StopActorExecutionError(err_descr, details=details) ++ ++ ++def fix_symlinks_in_targets(dir_containing_mount_units): ++ """ ++ Fix broken symlinks in *.target.* directories caused by earlier modified mount units. ++ ++ Generated mount unit files are part of one of systemd targets (list below), ++ which means that a symlink from a systemd target to exists for each of ++ them. Based on this, systemd knows when (local or remote file systems?) ++ they must (".requires" suffix") or could (".wants" suffix) be mounted. ++ See the man 5 systemd.mount for more details how mount units are split into ++ these targets. ++ ++ The list of possible target directories where these mount units could end: ++ * local-fs.target.requires ++ * local-fs.target.wants ++ * local-fs-pre.target.requires ++ * local-fs-pre.target.wants ++ * remote-fs.target.requires ++ * remote-fs.target.wants ++ * remote-fs-pre.target.requires ++ * remote-fs-pre.target.wants ++ Most likely, unit files are not generated for "*pre*" targets, but to be ++ sure really. Longer list does not cause any issues in this code. ++ ++ In most cases, "local-fs.target.requires" is the only important directory ++ for us during the upgrade. But in some (sometimes common) cases we will ++ need some of the others as well. ++ ++ These directories do not have to necessarily exists if there are no mount ++ unit files that could be put there. But most likely "local-fs.target.requires" ++ will always exists. ++ """ ++ dir_list = [ ++ 'local-fs.target.requires', ++ 'local-fs.target.wants', ++ 'local-fs-pre.target.requires', ++ 'local-fs-pre.target.wants', ++ 'remote-fs.target.requires', ++ 'remote-fs.target.wants', ++ 'remote-fs-pre.target.requires', ++ 'remote-fs-pre.target.wants', ++ ] ++ for tdir in dir_list: ++ _fix_symlinks_in_dir(dir_containing_mount_units, tdir) ++ ++ ++def copy_units_into_system_location(upgrade_container_ctx, dir_with_our_mount_units): ++ """ ++ Copy units and their .wants/.requires directories into the target userspace container. ++ ++ :return: A list of files in the target userspace that were created by copying. ++ :rtype: list[str] ++ """ ++ dest_inside_container = '/usr/lib/systemd/system' ++ ++ api.current_logger().debug( ++ 'Copying generated mount units for upgrade from {} to {}'.format( ++ dir_with_our_mount_units, ++ upgrade_container_ctx.full_path(dest_inside_container) ++ ) ++ ) ++ ++ copied_files = [] ++ prefix_len_to_drop = len(upgrade_container_ctx.base_dir) ++ ++ # We cannot rely on mounting library when copying into container ++ # as we want to control what happens to symlinks and ++ # shutil.copytree in Python3.6 fails if dst directory exists already ++ # - which happens in some cases when copying these files. ++ for root, dummy_dirs, files in os.walk(dir_with_our_mount_units): ++ rel_path = os.path.relpath(root, dir_with_our_mount_units) ++ if rel_path == '.': ++ rel_path = '' ++ dst_dir = os.path.join(upgrade_container_ctx.full_path(dest_inside_container), rel_path) ++ os.makedirs(dst_dir, mode=0o755, exist_ok=True) ++ ++ for file in files: ++ src_file = os.path.join(root, file) ++ dst_file = os.path.join(dst_dir, file) ++ api.current_logger().debug( ++ 'Copying mount unit file {} to {}'.format(src_file, dst_file) ++ ) ++ if os.path.islink(dst_file): ++ # If the target file already exists and it is a symlink, it will ++ # fail and we want to overwrite this. ++ # NOTE(pstodulk): You could think that it cannot happen, but ++ # in future possibly it could happen, so let's rather be careful ++ # and handle it. If the dst file exists, we want to overwrite it ++ # for sure ++ _delete_file(dst_file) ++ shutil.copy2(src_file, dst_file, follow_symlinks=False) ++ copied_files.append(dst_file[prefix_len_to_drop:]) ++ ++ return copied_files ++ ++ ++def remove_units_for_targets_that_are_already_mounted_by_dracut(dir_with_our_mount_units): ++ """ ++ Remove mount units for mount targets that are already mounted by dracut. ++ ++ Namely, remove mount units: ++ '-.mount' (mounts /) ++ 'usr.mount' (mounts /usr) ++ """ ++ ++ # NOTE: remount-fs.service creates dependency cycles that are nondeterministically broken ++ # by systemd, causing unpredictable failures. The service is supposed to remount root ++ # and /usr, reapplying mount options from /etc/fstab. However, the fstab file present in ++ # the initramfs is not the fstab from the source system, and, therefore, it is pointless ++ # to require the service. It would make sense after we switched root during normal boot ++ # process. ++ already_mounted_units = [ ++ '-.mount', ++ 'usr.mount', ++ 'local-fs.target.wants/systemd-remount-fs.service' ++ ] ++ ++ for unit in already_mounted_units: ++ unit_location = os.path.join(dir_with_our_mount_units, unit) ++ ++ if not os.path.exists(unit_location): ++ api.current_logger().debug('The {} unit does not exists, no need to remove it.'.format(unit)) ++ continue ++ ++ _delete_file(unit_location) ++ ++ ++def request_units_inclusion_in_initramfs(files_to_include): ++ api.current_logger().debug('Including the following files into initramfs: {}'.format(files_to_include)) ++ ++ additional_files = [ ++ '/usr/sbin/swapon' # If the system has swap, we have also generated a swap unit to activate it ++ ] ++ ++ tasks = UpgradeInitramfsTasks(include_files=files_to_include + additional_files) ++ api.produce(tasks) ++ ++ ++def setup_storage_initialization(): ++ userspace_info = next(api.consume(TargetUserSpaceInfo), None) ++ ++ with mounting.NspawnActions(base_dir=userspace_info.path) as upgrade_container_ctx: ++ with tempfile.TemporaryDirectory(dir='/var/lib/leapp/', prefix='tmp_systemd_fstab_') as workspace_path: ++ run_systemd_fstab_generator(workspace_path) ++ remove_units_for_targets_that_are_already_mounted_by_dracut(workspace_path) ++ prefix_all_mount_units_with_sysroot(workspace_path) ++ fix_symlinks_in_targets(workspace_path) ++ mount_unit_files = copy_units_into_system_location(upgrade_container_ctx, workspace_path) ++ request_units_inclusion_in_initramfs(mount_unit_files) +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 +new file mode 100644 +index 00000000..b814f6ce +--- /dev/null ++++ b/repos/system_upgrade/common/actors/initramfs/mount_units_generator/tests/test_mount_unit_generation.py +@@ -0,0 +1,269 @@ ++import os ++import shutil ++ ++import pytest ++ ++from leapp.exceptions import StopActorExecutionError ++from leapp.libraries.actor import mount_unit_generator ++from leapp.libraries.common.testutils import logger_mocked ++from leapp.libraries.stdlib import api, CalledProcessError ++from leapp.models import TargetUserSpaceInfo, UpgradeInitramfsTasks ++ ++ ++def test_run_systemd_fstab_generator_successful_generation(monkeypatch): ++ """Test successful mount unit generation.""" ++ ++ output_dir = '/tmp/test_output' ++ expected_cmd = [ ++ '/usr/lib/systemd/system-generators/systemd-fstab-generator', ++ output_dir, ++ output_dir, ++ output_dir ++ ] ++ ++ def mock_run(command): ++ assert command == expected_cmd ++ ++ return { ++ "stdout": "", ++ "stderr": "", ++ "exit_code": 0, ++ } ++ ++ monkeypatch.setattr(mount_unit_generator, 'run', mock_run) ++ mount_unit_generator.run_systemd_fstab_generator(output_dir) ++ ++ ++def test_run_systemd_fstab_generator_failure(monkeypatch): ++ """Test handling of systemd-fstab-generator failure.""" ++ output_dir = '/tmp/test_output' ++ expected_cmd = [ ++ '/usr/lib/systemd/system-generators/systemd-fstab-generator', ++ output_dir, ++ output_dir, ++ output_dir ++ ] ++ ++ def mock_run(command): ++ assert command == expected_cmd ++ raise CalledProcessError(message='Generator failed', command=['test'], result={'exit_code': 1}) ++ ++ monkeypatch.setattr(mount_unit_generator, 'run', mock_run) ++ monkeypatch.setattr(api, 'current_logger', logger_mocked()) ++ ++ with pytest.raises(StopActorExecutionError): ++ mount_unit_generator.run_systemd_fstab_generator(output_dir) ++ ++ ++def test_prefix_mount_unit_with_sysroot(monkeypatch): ++ """Test prefixing a single mount unit with /sysroot.""" ++ monkeypatch.setattr(api, 'current_logger', logger_mocked()) ++ ++ input_content = [ ++ "[Unit]\n", ++ "Description=Test Mount\n", ++ "[Mount]\n", ++ "Where=/home\n", ++ "What=/dev/sda1\n" ++ ] ++ ++ expected_output_lines = [ ++ "[Unit]", ++ "Description=Test Mount", ++ "[Mount]", ++ "Where=/sysroot/home", ++ "What=/dev/sda1" ++ ] ++ ++ def mock_read_unit_file_lines(unit_file_path): ++ return input_content ++ ++ def mock_write_unit_file_lines(unit_file_path, lines): ++ assert unit_file_path == '/test/output.mount' ++ assert lines == expected_output_lines ++ ++ monkeypatch.setattr(mount_unit_generator, '_read_unit_file_lines', mock_read_unit_file_lines) ++ monkeypatch.setattr(mount_unit_generator, '_write_unit_file_lines', mock_write_unit_file_lines) ++ ++ mount_unit_generator._prefix_mount_unit_with_sysroot( ++ '/test/input.mount', ++ '/test/output.mount' ++ ) ++ ++ ++def test_prefix_all_mount_units_with_sysroot(monkeypatch): ++ """Test prefixing all mount units in a directory.""" ++ ++ expected_changes = { ++ '/test/dir/home.mount': { ++ 'new_unit_destination': '/test/dir/sysroot-home.mount', ++ 'should_be_deleted': True, ++ 'deleted': False, ++ }, ++ '/test/dir/var.mount': { ++ 'new_unit_destination': '/test/dir/sysroot-var.mount', ++ 'should_be_deleted': True, ++ 'deleted': False, ++ }, ++ '/test/dir/not-a-mount.service': { ++ 'new_unit_destination': None, ++ 'should_be_deleted': False, ++ 'deleted': False, ++ } ++ } ++ ++ def mock_listdir(dir_path): ++ return ['home.mount', 'var.mount', 'not-a-mount.service'] ++ ++ def mock_delete_file(file_path): ++ assert file_path in expected_changes ++ expected_changes[file_path]['deleted'] = True ++ ++ def mock_prefix(unit_file_path, new_unit_destination): ++ assert expected_changes[unit_file_path]['new_unit_destination'] == new_unit_destination ++ ++ monkeypatch.setattr('os.listdir', mock_listdir) ++ monkeypatch.setattr(mount_unit_generator, '_delete_file', mock_delete_file) ++ monkeypatch.setattr(mount_unit_generator, '_prefix_mount_unit_with_sysroot', mock_prefix) ++ ++ mount_unit_generator.prefix_all_mount_units_with_sysroot('/test/dir') ++ ++ for original_mount_unit_location in expected_changes: ++ should_be_deleted = expected_changes[original_mount_unit_location]['should_be_deleted'] ++ was_deleted = expected_changes[original_mount_unit_location]['deleted'] ++ assert should_be_deleted == was_deleted ++ ++ ++@pytest.mark.parametrize('dirname', ( ++ 'local-fs.target.requires', ++ 'local-fs.target.wants', ++ 'local-fs-pre.target.requires', ++ 'local-fs-pre.target.wants', ++ 'remote-fs.target.requires', ++ 'remote-fs.target.wants', ++ 'remote-fs-pre.target.requires', ++ 'remote-fs-pre.target.wants', ++)) ++def test_fix_symlinks_in_dir(monkeypatch, dirname): ++ """Test fixing local-fs.target.requires symlinks.""" ++ ++ 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-var.mount', 'not-a-mount.service'] ++ ++ def mock_os_path_exist(dir_path): ++ assert dir_path == DIR_PATH ++ return dir_path == DIR_PATH ++ ++ expected_calls = [ ++ ['ln', '-s', '../sysroot-home.mount', os.path.join(DIR_PATH, 'sysroot-home.mount')], ++ ['ln', '-s', '../sysroot-var.mount', os.path.join(DIR_PATH, 'sysroot-var.mount')] ++ ] ++ call_count = 0 ++ ++ def mock_run(command): ++ nonlocal call_count ++ assert command in expected_calls ++ call_count += 1 ++ 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) ++ ++ mount_unit_generator._fix_symlinks_in_dir('/test/dir', dirname) ++ ++ ++# Test the copy_units_into_system_location function ++def test_copy_units_mixed_content(monkeypatch): ++ """Test copying units with mixed files and directories.""" ++ ++ def mock_walk(dir_path): ++ tuples_to_yield = [ ++ ('/source/dir', ['local-fs.target.requires'], ['unit1.mount', 'unit2.mount']), ++ ('/source/dir/local-fs.target.requires', [], ['unit1.mount', 'unit2.mount']), ++ ] ++ for i in tuples_to_yield: ++ yield i ++ ++ def mock_isdir(path): ++ return 'local-fs.target.requires' in path ++ ++ def _make_couple(sub_path): ++ return ( ++ os.path.join('/source/dir/', sub_path), ++ os.path.join('/container/usr/lib/systemd/system/', sub_path) ++ ) ++ ++ def mock_copy2(src, dst, follow_symlinks=True): ++ valid_combinations = [ ++ _make_couple('unit1.mount'), ++ _make_couple('unit2.mount'), ++ _make_couple('local-fs.target.requires/unit1.mount'), ++ _make_couple('local-fs.target.requires/unit2.mount'), ++ ] ++ assert not follow_symlinks ++ assert (src, dst) in valid_combinations ++ ++ def mock_islink(file_path): ++ return file_path == '/container/usr/lib/systemd/system/local-fs.target.requires/unit2.mount' ++ ++ class MockedDeleteFile: ++ def __init__(self): ++ self.removal_called = False ++ ++ def __call__(self, file_path): ++ assert file_path == '/container/usr/lib/systemd/system/local-fs.target.requires/unit2.mount' ++ self.removal_called = True ++ ++ def mock_makedirs(dst_dir, mode=0o777, exist_ok=False): ++ assert exist_ok ++ assert mode == 0o755 ++ ++ allowed_paths = [ ++ '/container/usr/lib/systemd/system', ++ '/container/usr/lib/systemd/system/local-fs.target.requires' ++ ] ++ assert dst_dir.rstrip('/') in allowed_paths ++ ++ monkeypatch.setattr(os, 'walk', mock_walk) ++ monkeypatch.setattr(os, 'makedirs', mock_makedirs) ++ monkeypatch.setattr(os.path, 'isdir', mock_isdir) ++ monkeypatch.setattr(os.path, 'islink', mock_islink) ++ monkeypatch.setattr(mount_unit_generator, '_delete_file', MockedDeleteFile()) ++ monkeypatch.setattr(shutil, 'copy2', mock_copy2) ++ ++ class MockedContainerContext: ++ def __init__(self): ++ self.base_dir = '/container' ++ ++ def full_path(self, path): ++ return os.path.join('/container', path.lstrip('/')) ++ ++ mock_container = MockedContainerContext() ++ ++ files = mount_unit_generator.copy_units_into_system_location( ++ mock_container, '/source/dir' ++ ) ++ ++ expected_files = [ ++ '/usr/lib/systemd/system/unit1.mount', ++ '/usr/lib/systemd/system/unit2.mount', ++ '/usr/lib/systemd/system/local-fs.target.requires/unit1.mount', ++ '/usr/lib/systemd/system/local-fs.target.requires/unit2.mount', ++ ] ++ assert sorted(files) == sorted(expected_files) ++ assert mount_unit_generator._delete_file.removal_called 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 a00f0d3..058dd24 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.23.0 -Release: 1%{?dist}.elevate.1 +Release: 1%{?dist}.elevate.2 Summary: Repositories for leapp License: ASL 2.0 @@ -350,6 +350,9 @@ fi %changelog +* Tue Sep 30 2025 Yuriy Kohut - 0.23.0-1.elevate.2 +- ELevate vendors support for upstream 0.23.0-1 version (47fce173e75408d9a7a26225d389161caf72e244) + * Fri Sep 12 2025 Yuriy Kohut - 0.23.0-1.elevate.1 - ELevate vendors support for upstream 0.23.0-1 version (dcf53c28ea9c3fdd03277abcdeb1d124660f7f8e)