leapp-repository/SOURCES/0001-Add-upgrade-inhibitor-for-custom-DNF-pluginpath-conf.patch
2025-12-01 09:14:24 +00:00

334 lines
14 KiB
Diff

From dcf53c28ea9c3fdd03277abcdeb1d124660f7f8e Mon Sep 17 00:00:00 2001
From: karolinku <kkula@redhat.com>
Date: Tue, 19 Aug 2025 09:48:11 +0200
Subject: [PATCH 01/55] Add upgrade inhibitor for custom DNF pluginpath
configuration
Implements detection and inhibition of the upgrade when DNF
pluginpath is configured in /etc/dnf/dnf.conf:
- Add DnfPluginPathDetected model to communicate detection results
- Add ScanDnfPluginPath actor (FactsPhase) to scan DNF configuration
- Add CheckDnfPluginPath actor (ChecksPhase) to create inhibitor report
- Add related unit tests
Localisation of dnf plugins is not constant between system releases
which can cause issues with the upgrade, so the user should remove
this option or comment it out.
Jira: RHEL-69601
---
.../common/actors/checkdnfpluginpath/actor.py | 22 ++++++++
.../libraries/checkdnfpluginpath.py | 35 ++++++++++++
.../tests/test_checkdnfpluginpath.py | 34 ++++++++++++
.../common/actors/scandnfpluginpath/actor.py | 21 ++++++++
.../libraries/scandnfpluginpath.py | 30 +++++++++++
.../files/dnf_config_incorrect_pluginpath | 7 +++
.../tests/files/dnf_config_no_pluginpath | 6 +++
.../tests/files/dnf_config_with_pluginpath | 7 +++
.../tests/test_scandnfpluginpath.py | 53 +++++++++++++++++++
.../common/models/dnfpluginpathdetected.py | 14 +++++
10 files changed, 229 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/checkdnfpluginpath/actor.py
create mode 100644 repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
create mode 100644 repos/system_upgrade/common/actors/checkdnfpluginpath/tests/test_checkdnfpluginpath.py
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/actor.py
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/libraries/scandnfpluginpath.py
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_incorrect_pluginpath
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_no_pluginpath
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_with_pluginpath
create mode 100644 repos/system_upgrade/common/actors/scandnfpluginpath/tests/test_scandnfpluginpath.py
create mode 100644 repos/system_upgrade/common/models/dnfpluginpathdetected.py
diff --git a/repos/system_upgrade/common/actors/checkdnfpluginpath/actor.py b/repos/system_upgrade/common/actors/checkdnfpluginpath/actor.py
new file mode 100644
index 00000000..34055886
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkdnfpluginpath/actor.py
@@ -0,0 +1,22 @@
+from leapp.actors import Actor
+from leapp.libraries.actor.checkdnfpluginpath import perform_check
+from leapp.models import DnfPluginPathDetected
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckDnfPluginPath(Actor):
+ """
+ Inhibits the upgrade if a custom DNF plugin path is configured.
+
+ This actor checks whether the pluginpath option is configured in /etc/dnf/dnf.conf and produces a report if it is.
+ If the option is detected with any value, the upgrade is inhibited.
+ """
+
+ name = 'check_dnf_pluginpath'
+ consumes = (DnfPluginPathDetected,)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ perform_check()
diff --git a/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py b/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
new file mode 100644
index 00000000..ce705361
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkdnfpluginpath/libraries/checkdnfpluginpath.py
@@ -0,0 +1,35 @@
+from leapp import reporting
+from leapp.libraries.stdlib import api
+from leapp.models import DnfPluginPathDetected
+
+DNF_CONFIG_PATH = '/etc/dnf/dnf.conf'
+
+
+def check_dnf_pluginpath(dnf_pluginpath_detected):
+ """Create an inhibitor when pluginpath is detected in DNF configuration."""
+ if not dnf_pluginpath_detected.is_pluginpath_detected:
+ return
+ reporting.create_report([
+ reporting.Title('Detected specified pluginpath in DNF configuration.'),
+ reporting.Summary(
+ 'The "pluginpath" option is set in the {} file. The path to DNF plugins differs between '
+ 'system major releases due to different versions of Python. '
+ 'This breaks the in-place upgrades if defined explicitly as DNF plugins '
+ 'are stored on a different path on the new system.'
+ .format(DNF_CONFIG_PATH)
+ ),
+ reporting.Remediation(
+ hint='Remove or comment out the pluginpath option in the DNF '
+ 'configuration file to be able to upgrade the system',
+ commands=[['sed', '-i', '\'s/^pluginpath[[:space:]]*=/#pluginpath=/\'', DNF_CONFIG_PATH]],
+ ),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ reporting.RelatedResource('file', DNF_CONFIG_PATH),
+ ])
+
+
+def perform_check():
+ dnf_pluginpath_detected = next(api.consume(DnfPluginPathDetected), None)
+ if dnf_pluginpath_detected:
+ check_dnf_pluginpath(dnf_pluginpath_detected)
diff --git a/repos/system_upgrade/common/actors/checkdnfpluginpath/tests/test_checkdnfpluginpath.py b/repos/system_upgrade/common/actors/checkdnfpluginpath/tests/test_checkdnfpluginpath.py
new file mode 100644
index 00000000..7dd8bbf2
--- /dev/null
+++ b/repos/system_upgrade/common/actors/checkdnfpluginpath/tests/test_checkdnfpluginpath.py
@@ -0,0 +1,34 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor.checkdnfpluginpath import check_dnf_pluginpath, perform_check
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import DnfPluginPathDetected
+from leapp.utils.report import is_inhibitor
+
+
+@pytest.mark.parametrize('is_detected', [False, True])
+def test_check_dnf_pluginpath(monkeypatch, is_detected):
+ actor_reports = create_report_mocked()
+ msg = DnfPluginPathDetected(is_pluginpath_detected=is_detected)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[msg]))
+ monkeypatch.setattr(reporting, 'create_report', actor_reports)
+
+ perform_check()
+
+ assert bool(actor_reports.called) == is_detected
+
+ if is_detected:
+ assert is_inhibitor(actor_reports.report_fields)
+
+
+def test_perform_check_no_message_available(monkeypatch):
+ """Test perform_check when no DnfPluginPathDetected message is available."""
+ actor_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(reporting, 'create_report', actor_reports)
+
+ perform_check()
+
+ assert not actor_reports.called
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/actor.py b/repos/system_upgrade/common/actors/scandnfpluginpath/actor.py
new file mode 100644
index 00000000..e43a691e
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/actor.py
@@ -0,0 +1,21 @@
+from leapp.actors import Actor
+from leapp.libraries.actor.scandnfpluginpath import scan_dnf_pluginpath
+from leapp.models import DnfPluginPathDetected
+from leapp.tags import FactsPhaseTag, IPUWorkflowTag
+
+
+class ScanDnfPluginPath(Actor):
+ """
+ Scans DNF configuration for custom pluginpath option.
+
+ This actor collects information about whether the pluginpath option is configured in DNF configuration
+ and produces a DnfPluginPathDetected message, containing the information.
+ """
+
+ name = 'scan_dnf_pluginpath'
+ consumes = ()
+ produces = (DnfPluginPathDetected,)
+ tags = (FactsPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ scan_dnf_pluginpath()
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/libraries/scandnfpluginpath.py b/repos/system_upgrade/common/actors/scandnfpluginpath/libraries/scandnfpluginpath.py
new file mode 100644
index 00000000..818f7700
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/libraries/scandnfpluginpath.py
@@ -0,0 +1,30 @@
+import os
+
+from six.moves import configparser
+
+from leapp.libraries.stdlib import api
+from leapp.models import DnfPluginPathDetected
+
+DNF_CONFIG_PATH = '/etc/dnf/dnf.conf'
+
+
+def _is_pluginpath_set(config_path):
+ """Check if pluginpath option is set in DNF configuration file."""
+ if not os.path.isfile(config_path):
+ api.current_logger().warning('The %s file is missing.', config_path)
+ return False
+
+ parser = configparser.ConfigParser()
+
+ try:
+ parser.read(config_path)
+ return parser.has_option('main', 'pluginpath')
+ except (configparser.Error, IOError) as e:
+ api.current_logger().warning('The DNF config file %s couldn\'t be parsed: %s', config_path, e)
+ return False
+
+
+def scan_dnf_pluginpath():
+ """Scan DNF configuration and produce DnfPluginPathDetected message."""
+ is_detected = _is_pluginpath_set(DNF_CONFIG_PATH)
+ api.produce(DnfPluginPathDetected(is_pluginpath_detected=is_detected))
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_incorrect_pluginpath b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_incorrect_pluginpath
new file mode 100644
index 00000000..aa29db09
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_incorrect_pluginpath
@@ -0,0 +1,7 @@
+[main]
+gpgcheck=1
+installonly_limit=3
+clean_requirements_on_remove=True
+best=True
+skip_if_unavailable=False
+pluginpathincorrect=/usr/lib/python3.6/site-packages/dnf-plugins
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_no_pluginpath b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_no_pluginpath
new file mode 100644
index 00000000..3d08d075
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_no_pluginpath
@@ -0,0 +1,6 @@
+[main]
+gpgcheck=1
+installonly_limit=3
+clean_requirements_on_remove=True
+best=True
+skip_if_unavailable=False
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_with_pluginpath b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_with_pluginpath
new file mode 100644
index 00000000..09a81e64
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/files/dnf_config_with_pluginpath
@@ -0,0 +1,7 @@
+[main]
+gpgcheck=1
+installonly_limit=3
+clean_requirements_on_remove=True
+best=True
+skip_if_unavailable=False
+pluginpath=/usr/lib/python3.6/site-packages/dnf-plugins
diff --git a/repos/system_upgrade/common/actors/scandnfpluginpath/tests/test_scandnfpluginpath.py b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/test_scandnfpluginpath.py
new file mode 100644
index 00000000..fefb9d3f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/scandnfpluginpath/tests/test_scandnfpluginpath.py
@@ -0,0 +1,53 @@
+import os
+
+import pytest
+
+from leapp.libraries.actor import scandnfpluginpath
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import DnfPluginPathDetected
+
+
+@pytest.mark.parametrize('is_detected', [False, True])
+def test_scan_detects_pluginpath(monkeypatch, is_detected):
+ mocked_producer = produce_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+
+ monkeypatch.setattr(scandnfpluginpath, '_is_pluginpath_set',
+ lambda path: is_detected)
+
+ scandnfpluginpath.scan_dnf_pluginpath()
+
+ assert mocked_producer.called == 1
+ assert mocked_producer.model_instances[0].is_pluginpath_detected is is_detected
+
+
+@pytest.mark.parametrize(('config_file', 'result'), [
+ ('files/dnf_config_no_pluginpath', False),
+ ('files/dnf_config_with_pluginpath', True),
+ ('files/dnf_config_incorrect_pluginpath', False),
+ ('files/not_existing_file.conf', False)
+])
+def test_is_pluginpath_set(config_file, result):
+ CUR_DIR = os.path.dirname(os.path.abspath(__file__))
+
+ assert scandnfpluginpath._is_pluginpath_set(os.path.join(CUR_DIR, config_file)) == result
+
+
+def test_scan_no_config_file(monkeypatch):
+ mocked_producer = produce_mocked()
+ logger = logger_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
+ monkeypatch.setattr(api, 'produce', mocked_producer)
+ monkeypatch.setattr(api, 'current_logger', lambda: logger)
+
+ filename = 'files/not_existing_file.conf'
+ monkeypatch.setattr(scandnfpluginpath, 'DNF_CONFIG_PATH', filename)
+ scandnfpluginpath.scan_dnf_pluginpath()
+
+ assert mocked_producer.called == 1
+ assert mocked_producer.model_instances[0].is_pluginpath_detected is False
+
+ assert 'The %s file is missing.' in logger.warnmsg
+ assert filename in logger.warnmsg
diff --git a/repos/system_upgrade/common/models/dnfpluginpathdetected.py b/repos/system_upgrade/common/models/dnfpluginpathdetected.py
new file mode 100644
index 00000000..c5474857
--- /dev/null
+++ b/repos/system_upgrade/common/models/dnfpluginpathdetected.py
@@ -0,0 +1,14 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class DnfPluginPathDetected(Model):
+ """
+ This model contains information about whether DNF pluginpath option is configured in /etc/dnf/dnf.conf.
+ """
+ topic = SystemInfoTopic
+
+ is_pluginpath_detected = fields.Boolean()
+ """
+ True if pluginpath option is found in /etc/dnf/dnf.conf, False otherwise.
+ """
--
2.51.1