From 25959610c2cd3ddf719c4376f24b193f48171fb6 Mon Sep 17 00:00:00 2001 From: Matej Matuska Date: Tue, 14 Nov 2023 09:51:41 +0100 Subject: [PATCH] Copy dnf.conf to target userspace and allow a custom one This change allows working around the fact that source and target `dnf.conf` files might be incompatible. For example some of the proxy configuration between RHEL7 and RHEL8. Target system compatible configuration can be specified in /etc/leapp/files/dnf.conf. If this file is present it is copied into the target userspace and also applied to the target system. If it doesn't exist, the `/etc/dnf/dnf.conf` from the source system will be copied instead. Errors that could be caused by incompatible/incorrect proxy configuration now contain a hint with a remediation with the steps above mentioned. Jira: OAMG-6544 --- .../actors/dnfconftargetusecustom/actor.py | 19 ++++++++++++++ .../libraries/dnfconftargetusecustom.py | 15 +++++++++++ .../tests/test_dnfconftargetusecustom.py | 23 ++++++++++++++++ .../actors/dnfconfuserspacecopy/actor.py | 24 +++++++++++++++++ .../libraries/dnfconfuserspacecopy.py | 19 ++++++++++++++ .../tests/test_dnfconfuserspacecopy.py | 26 +++++++++++++++++++ .../libraries/userspacegen.py | 18 ++++++++++--- .../common/libraries/dnfplugin.py | 22 +++++++++++++++- 8 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 repos/system_upgrade/common/actors/dnfconftargetusecustom/actor.py create mode 100644 repos/system_upgrade/common/actors/dnfconftargetusecustom/libraries/dnfconftargetusecustom.py create mode 100644 repos/system_upgrade/common/actors/dnfconftargetusecustom/tests/test_dnfconftargetusecustom.py create mode 100644 repos/system_upgrade/common/actors/dnfconfuserspacecopy/actor.py create mode 100644 repos/system_upgrade/common/actors/dnfconfuserspacecopy/libraries/dnfconfuserspacecopy.py create mode 100644 repos/system_upgrade/common/actors/dnfconfuserspacecopy/tests/test_dnfconfuserspacecopy.py diff --git a/repos/system_upgrade/common/actors/dnfconftargetusecustom/actor.py b/repos/system_upgrade/common/actors/dnfconftargetusecustom/actor.py new file mode 100644 index 0000000000..019a21e78c --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconftargetusecustom/actor.py @@ -0,0 +1,19 @@ +from leapp.actors import Actor +from leapp.libraries.actor import dnfconftargetusecustom +from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag + + +class DNFConfTargetUseCustom(Actor): + """ + Move /etc/leapp/files/dnf.conf to /etc/dnf/dnf.conf if it exists + + An actor in FactsPhase copies this file to the target userspace if present. + In such case we also want to use the file on the target system. + """ + name = "dnf_conf_target_use_custom" + consumes = () + produces = () + tags = (ApplicationsPhaseTag, IPUWorkflowTag) + + def process(self): + dnfconftargetusecustom.process() diff --git a/repos/system_upgrade/common/actors/dnfconftargetusecustom/libraries/dnfconftargetusecustom.py b/repos/system_upgrade/common/actors/dnfconftargetusecustom/libraries/dnfconftargetusecustom.py new file mode 100644 index 0000000000..2eabd6782c --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconftargetusecustom/libraries/dnfconftargetusecustom.py @@ -0,0 +1,15 @@ +import os + +from leapp.libraries.stdlib import api, CalledProcessError, run + +CUSTOM_DNF_CONF_PATH = "/etc/leapp/files/dnf.conf" + + +def process(): + if os.path.exists(CUSTOM_DNF_CONF_PATH): + try: + run(["mv", CUSTOM_DNF_CONF_PATH, "/etc/dnf/dnf.conf"]) + except (CalledProcessError, OSError) as e: + api.current_logger().debug( + "Failed to move /etc/leapp/files/dnf.conf to /etc/dnf/dnf.conf: {}".format(e) + ) diff --git a/repos/system_upgrade/common/actors/dnfconftargetusecustom/tests/test_dnfconftargetusecustom.py b/repos/system_upgrade/common/actors/dnfconftargetusecustom/tests/test_dnfconftargetusecustom.py new file mode 100644 index 0000000000..ab84b7f0f1 --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconftargetusecustom/tests/test_dnfconftargetusecustom.py @@ -0,0 +1,23 @@ +import os + +import pytest + +from leapp.libraries.actor import dnfconftargetusecustom + + +@pytest.mark.parametrize( + "exists,should_move", + [(False, False), (True, True)], +) +def test_copy_correct_dnf_conf(monkeypatch, exists, should_move): + monkeypatch.setattr(os.path, "exists", lambda _: exists) + + run_called = [False] + + def mocked_run(_): + run_called[0] = True + + monkeypatch.setattr(dnfconftargetusecustom, 'run', mocked_run) + + dnfconftargetusecustom.process() + assert run_called[0] == should_move diff --git a/repos/system_upgrade/common/actors/dnfconfuserspacecopy/actor.py b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/actor.py new file mode 100644 index 0000000000..2142e1f102 --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/actor.py @@ -0,0 +1,24 @@ +from leapp.actors import Actor +from leapp.libraries.actor import dnfconfuserspacecopy +from leapp.models import TargetUserSpacePreupgradeTasks +from leapp.tags import FactsPhaseTag, IPUWorkflowTag + + +class DNFConfUserspaceCopy(Actor): + """ + Copy dnf.conf to target userspace + + Copies /etc/leapp/files/dnf.conf to target userspace. If it isn't available + /etc/dnf/dnf.conf is copied instead. This allows specifying a different + config for the target userspace, which might be required if the source + system configuration file isn't compatible with the target one. One such + example is incompatible proxy configuration between RHEL7 and RHEL8 DNF + versions. + """ + name = "dnf_conf_userspace_copy" + consumes = () + produces = (TargetUserSpacePreupgradeTasks,) + tags = (FactsPhaseTag, IPUWorkflowTag) + + def process(self): + dnfconfuserspacecopy.process() diff --git a/repos/system_upgrade/common/actors/dnfconfuserspacecopy/libraries/dnfconfuserspacecopy.py b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/libraries/dnfconfuserspacecopy.py new file mode 100644 index 0000000000..4e74acdb1b --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/libraries/dnfconfuserspacecopy.py @@ -0,0 +1,19 @@ +import os + +from leapp.libraries.stdlib import api +from leapp.models import CopyFile, TargetUserSpacePreupgradeTasks + + +def process(): + src = "/etc/dnf/dnf.conf" + if os.path.exists("/etc/leapp/files/dnf.conf"): + src = "/etc/leapp/files/dnf.conf" + + api.current_logger().debug( + "Copying dnf.conf at {} to the target userspace".format(src) + ) + api.produce( + TargetUserSpacePreupgradeTasks( + copy_files=[CopyFile(src=src, dst="/etc/dnf/dnf.conf")] + ) + ) diff --git a/repos/system_upgrade/common/actors/dnfconfuserspacecopy/tests/test_dnfconfuserspacecopy.py b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/tests/test_dnfconfuserspacecopy.py new file mode 100644 index 0000000000..abe004cd5c --- /dev/null +++ b/repos/system_upgrade/common/actors/dnfconfuserspacecopy/tests/test_dnfconfuserspacecopy.py @@ -0,0 +1,26 @@ +import os + +import pytest + +from leapp.libraries.actor import dnfconfuserspacecopy +from leapp.libraries.common.testutils import logger_mocked, produce_mocked + + +@pytest.mark.parametrize( + "userspace_conf_exists,expected", + [(False, "/etc/dnf/dnf.conf"), (True, "/etc/leapp/files/dnf.conf")], +) +def test_copy_correct_dnf_conf(monkeypatch, userspace_conf_exists, expected): + monkeypatch.setattr(os.path, "exists", lambda _: userspace_conf_exists) + + mocked_produce = produce_mocked() + monkeypatch.setattr(dnfconfuserspacecopy.api, 'produce', mocked_produce) + monkeypatch.setattr(dnfconfuserspacecopy.api, 'current_logger', logger_mocked()) + + dnfconfuserspacecopy.process() + + assert mocked_produce.called == 1 + assert len(mocked_produce.model_instances) == 1 + assert len(mocked_produce.model_instances[0].copy_files) == 1 + assert mocked_produce.model_instances[0].copy_files[0].src == expected + assert mocked_produce.model_instances[0].copy_files[0].dst == "/etc/dnf/dnf.conf" diff --git a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py index 039b99a5be..8e28116ba6 100644 --- a/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py +++ b/repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py @@ -269,15 +269,25 @@ def prepare_target_userspace(context, userspace_dir, enabled_repos, packages): # failed since leapp does not support updates behind proxy yet. for manager_info in api.consume(PkgManagerInfo): if manager_info.configured_proxies: - details['details'] = ("DNF failed to install userspace packages, likely due to the proxy " - "configuration detected in the YUM/DNF configuration file.") + details['details'] = ( + "DNF failed to install userspace packages, likely due to the proxy " + "configuration detected in the YUM/DNF configuration file. " + "Make sure the proxy is properly configured in /etc/dnf/dnf.conf. " + "It's also possible the proxy settings in the DNF configuration file are " + "incompatible with the target system. A compatible configuration can be " + "placed in /etc/leapp/files/dnf.conf which, if present, will be used during " + "the upgrade instead of /etc/dnf/dnf.conf. " + "In such case the configuration will also be applied to the target system." + ) # Similarly if a proxy was set specifically for one of the repositories. for repo_facts in api.consume(RepositoriesFacts): for repo_file in repo_facts.repositories: if any(repo_data.proxy and repo_data.enabled for repo_data in repo_file.data): - details['details'] = ("DNF failed to install userspace packages, likely due to the proxy " - "configuration detected in a repository configuration file.") + details['details'] = ( + "DNF failed to install userspace packages, likely due to the proxy " + "configuration detected in a repository configuration file." + ) raise StopActorExecutionError(message=message, details=details) diff --git a/repos/system_upgrade/common/libraries/dnfplugin.py b/repos/system_upgrade/common/libraries/dnfplugin.py index ffde211fd9..d05283c8b3 100644 --- a/repos/system_upgrade/common/libraries/dnfplugin.py +++ b/repos/system_upgrade/common/libraries/dnfplugin.py @@ -178,8 +178,28 @@ def _handle_transaction_err_msg(stage, xfs_info, err, is_container=False): return # not needed actually as the above function raises error, but for visibility NO_SPACE_STR = 'more space needed on the' message = 'DNF execution failed with non zero exit code.' - details = {'STDOUT': err.stdout, 'STDERR': err.stderr} if NO_SPACE_STR not in err.stderr: + # if there was a problem reaching repos and proxy is configured in DNF/YUM configs, the + # proxy is likely the problem. + # NOTE(mmatuska): We can't consistently detect there was a problem reaching some repos, + # because it isn't clear what are all the possible DNF error messages we can encounter, + # such as: "Failed to synchronize cache for repo ..." or "Errors during downloading + # metadata for # repository" or "No more mirrors to try - All mirrors were already tried + # without success" + # NOTE(mmatuska): We could check PkgManagerInfo to detect if proxy is indeed configured, + # however it would be pretty ugly to pass it all the way down here + proxy_hint = ( + "If there was a problem reaching repositories (see stderr output) and proxy is " + "configured in the YUM/DNF configuration file, the proxy configuration is likely " + "causing this error. " + "Make sure the proxy is properly configured in /etc/dnf/dnf.conf. " + "It's also possible the proxy settings in the DNF configuration file are " + "incompatible with the target system. A compatible configuration can be " + "placed in /etc/leapp/files/dnf.conf which, if present, will be used during " + "the upgrade instead of /etc/dnf/dnf.conf. " + "In such case the configuration will also be applied to the target system." + ) + details = {'STDOUT': err.stdout, 'STDERR': err.stderr, 'hint': proxy_hint} raise StopActorExecutionError(message=message, details=details) # Disk Requirements: