leapp-repository/SOURCES/0009-Add-actors-for-checkin...

384 lines
15 KiB
Diff

From d2d7999744e97776eda664592ac0cc7ec5747b99 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Thu, 8 Sep 2022 16:27:10 +0200
Subject: [PATCH 09/32] Add actors for checking and setting systemd services
states
Introduces a new `set_systemd_services_state` actor, which
enables/disables systemd services according to received
`SystemdServicesTasks` messages and a `check_systemd_services_tasks`
actor which checks tasks in the `TargetTransactionCheckPhase` and
inhibits upgrade if there are conflicts.
Actors are in a new directory `systemd`.
---
.../systemd/checksystemdservicetasks/actor.py | 30 +++++++
.../libraries/checksystemdservicetasks.py | 36 ++++++++
.../tests/test_checksystemdservicestasks.py | 88 +++++++++++++++++++
.../systemd/setsystemdservicesstates/actor.py | 18 ++++
.../libraries/setsystemdservicesstate.py | 31 +++++++
.../tests/test_setsystemdservicesstate.py | 83 +++++++++++++++++
.../common/models/systemdservices.py | 22 +++++
7 files changed, 308 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/actor.py
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/tests/test_checksystemdservicestasks.py
create mode 100644 repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/actor.py
create mode 100644 repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/libraries/setsystemdservicesstate.py
create mode 100644 repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/tests/test_setsystemdservicesstate.py
create mode 100644 repos/system_upgrade/common/models/systemdservices.py
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/actor.py b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/actor.py
new file mode 100644
index 00000000..2df995ee
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/actor.py
@@ -0,0 +1,30 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import checksystemdservicetasks
+from leapp.models import SystemdServicesTasks
+from leapp.reporting import Report
+from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag
+
+
+class CheckSystemdServicesTasks(Actor):
+ """
+ Inhibits upgrade if SystemdServicesTasks tasks are in conflict
+
+ There is possibility, that SystemdServicesTasks messages with conflicting
+ requested service states could be produced. For example a service is
+ requested to be both enabled and disabled. This actor inhibits upgrade in
+ such cases.
+
+ Note: We expect that SystemdServicesTasks could be produced even after the
+ TargetTransactionChecksPhase (e.g. during the ApplicationPhase). The
+ purpose of this actor is to report collisions in case we can already detect
+ them. In case of conflicts caused by produced messages later we just log
+ the collisions and the services will end up disabled.
+ """
+
+ name = 'check_systemd_services_tasks'
+ consumes = (SystemdServicesTasks,)
+ produces = (Report,)
+ tags = (TargetTransactionChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ checksystemdservicetasks.check_conflicts()
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
new file mode 100644
index 00000000..75833e4f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
@@ -0,0 +1,36 @@
+from leapp import reporting
+from leapp.libraries.stdlib import api
+from leapp.models import SystemdServicesTasks
+
+FMT_LIST_SEPARATOR = '\n - '
+
+
+def _printable_conflicts(conflicts):
+ return FMT_LIST_SEPARATOR + FMT_LIST_SEPARATOR.join(sorted(conflicts))
+
+
+def _inhibit_upgrade_with_conflicts(conflicts):
+ summary = (
+ 'The requested states for systemd services on the target system are in conflict.'
+ ' The following systemd services were requested to be both enabled and disabled on the target system: {}'
+ )
+ report = [
+ reporting.Title('Conflicting requirements of systemd service states'),
+ reporting.Summary(summary.format(_printable_conflicts(conflicts))),
+ reporting.Severity(reporting.Severity.HIGH),
+ reporting.Groups([reporting.Groups.SANITY]),
+ reporting.Groups([reporting.Groups.INHIBITOR]),
+ ]
+ reporting.create_report(report)
+
+
+def check_conflicts():
+ services_to_enable = set()
+ services_to_disable = set()
+ for task in api.consume(SystemdServicesTasks):
+ services_to_enable.update(task.to_enable)
+ services_to_disable.update(task.to_disable)
+
+ conflicts = services_to_enable.intersection(services_to_disable)
+ if conflicts:
+ _inhibit_upgrade_with_conflicts(conflicts)
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/tests/test_checksystemdservicestasks.py b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/tests/test_checksystemdservicestasks.py
new file mode 100644
index 00000000..36ded92f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/tests/test_checksystemdservicestasks.py
@@ -0,0 +1,88 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor import checksystemdservicetasks
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import SystemdServicesTasks
+
+
+@pytest.mark.parametrize(
+ ('tasks', 'should_inhibit'),
+ [
+ (
+ [SystemdServicesTasks(to_enable=['hello.service'], to_disable=['hello.service'])],
+ True
+ ),
+ (
+ [SystemdServicesTasks(to_enable=['hello.service', 'world.service'],
+ to_disable=['hello.service'])],
+ True
+ ),
+ (
+ [
+ SystemdServicesTasks(to_enable=['hello.service']),
+ SystemdServicesTasks(to_disable=['hello.service'])
+ ],
+ True
+ ),
+ (
+ [SystemdServicesTasks(to_enable=['hello.service'], to_disable=['world.service'])],
+ False
+ ),
+ (
+ [
+ SystemdServicesTasks(to_enable=['hello.service']),
+ SystemdServicesTasks(to_disable=['world.service'])
+ ],
+ False
+ ),
+ (
+ [
+ SystemdServicesTasks(to_enable=['hello.service', 'world.service']),
+ SystemdServicesTasks(to_disable=['world.service', 'httpd.service'])
+ ],
+ True
+ ),
+ ]
+)
+def test_conflicts_detected(monkeypatch, tasks, should_inhibit):
+
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=tasks))
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ checksystemdservicetasks.check_conflicts()
+
+ assert bool(created_reports.called) == should_inhibit
+
+
+@pytest.mark.parametrize(
+ ('tasks', 'expected_reported'),
+ [
+ (
+ [SystemdServicesTasks(to_enable=['world.service', 'httpd.service', 'hello.service'],
+ to_disable=['hello.service', 'world.service', 'test.service'])],
+ ['world.service', 'hello.service']
+ ),
+ (
+ [
+ SystemdServicesTasks(to_enable=['hello.service', 'httpd.service'],
+ to_disable=['world.service']),
+ SystemdServicesTasks(to_enable=['world.service', 'httpd.service'],
+ to_disable=['hello.service', 'test.service'])
+ ],
+ ['world.service', 'hello.service']
+ ),
+ ]
+)
+def test_coflict_reported(monkeypatch, tasks, expected_reported):
+
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=tasks))
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ checksystemdservicetasks.check_conflicts()
+
+ report_summary = reporting.create_report.report_fields['summary']
+ assert all(service in report_summary for service in expected_reported)
diff --git a/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/actor.py b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/actor.py
new file mode 100644
index 00000000..1709091e
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/actor.py
@@ -0,0 +1,18 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import setsystemdservicesstate
+from leapp.models import SystemdServicesTasks
+from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag
+
+
+class SetSystemdServicesState(Actor):
+ """
+ According to input messages sets systemd services states on the target system
+ """
+
+ name = 'set_systemd_services_state'
+ consumes = (SystemdServicesTasks,)
+ produces = ()
+ tags = (FinalizationPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ setsystemdservicesstate.process()
diff --git a/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/libraries/setsystemdservicesstate.py b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/libraries/setsystemdservicesstate.py
new file mode 100644
index 00000000..01272438
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/libraries/setsystemdservicesstate.py
@@ -0,0 +1,31 @@
+from leapp.libraries.stdlib import api, CalledProcessError, run
+from leapp.models import SystemdServicesTasks
+
+
+def _try_set_service_state(command, service):
+ try:
+ # it is possible to call this on multiple units at once,
+ # but failing to enable one service would cause others to not enable as well
+ run(['systemctl', command, service])
+ except CalledProcessError as err:
+ api.current_logger().error('Failed to {} systemd unit "{}". Message: {}'.format(command, service, str(err)))
+ # TODO(mmatuska) produce post-upgrade report
+
+
+def process():
+ services_to_enable = set()
+ services_to_disable = set()
+ for task in api.consume(SystemdServicesTasks):
+ services_to_enable.update(task.to_enable)
+ services_to_disable.update(task.to_disable)
+
+ intersection = services_to_enable.intersection(services_to_disable)
+ for service in intersection:
+ msg = 'Attempted to both enable and disable systemd service "{}", service will be disabled.'.format(service)
+ api.current_logger().error(msg)
+
+ for service in services_to_enable:
+ _try_set_service_state('enable', service)
+
+ for service in services_to_disable:
+ _try_set_service_state('disable', service)
diff --git a/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/tests/test_setsystemdservicesstate.py b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/tests/test_setsystemdservicesstate.py
new file mode 100644
index 00000000..dd153329
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/tests/test_setsystemdservicesstate.py
@@ -0,0 +1,83 @@
+import pytest
+
+from leapp.libraries import stdlib
+from leapp.libraries.actor import setsystemdservicesstate
+from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
+from leapp.libraries.stdlib import api, CalledProcessError
+from leapp.models import SystemdServicesTasks
+
+
+class MockedRun(object):
+ def __init__(self):
+ self.commands = []
+
+ def __call__(self, cmd, *args, **kwargs):
+ self.commands.append(cmd)
+ return {}
+
+
+@pytest.mark.parametrize(
+ ('msgs', 'expected_calls'),
+ [
+ (
+ [SystemdServicesTasks(to_enable=['hello.service'],
+ to_disable=['getty.service'])],
+ [['systemctl', 'enable', 'hello.service'], ['systemctl', 'disable', 'getty.service']]
+ ),
+ (
+ [SystemdServicesTasks(to_disable=['getty.service'])],
+ [['systemctl', 'disable', 'getty.service']]
+ ),
+ (
+ [SystemdServicesTasks(to_enable=['hello.service'])],
+ [['systemctl', 'enable', 'hello.service']]
+ ),
+ (
+ [SystemdServicesTasks()],
+ []
+ ),
+ ]
+)
+def test_process(monkeypatch, msgs, expected_calls):
+ mocked_run = MockedRun()
+ monkeypatch.setattr(setsystemdservicesstate, 'run', mocked_run)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+
+ setsystemdservicesstate.process()
+
+ assert mocked_run.commands == expected_calls
+
+
+def test_process_invalid(monkeypatch):
+
+ def mocked_run(cmd, *args, **kwargs):
+ if cmd == ['systemctl', 'enable', 'invalid.service']:
+ message = 'Command {0} failed with exit code {1}.'.format(str(cmd), 1)
+ raise CalledProcessError(message, cmd, 1)
+
+ msgs = [SystemdServicesTasks(to_enable=['invalid.service'])]
+
+ monkeypatch.setattr(setsystemdservicesstate, 'run', mocked_run)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ setsystemdservicesstate.process()
+
+ expect_msg = ("Failed to enable systemd unit \"invalid.service\". Message:"
+ " Command ['systemctl', 'enable', 'invalid.service'] failed with exit code 1.")
+ assert expect_msg in api.current_logger.errmsg
+
+
+def test_enable_disable_conflict_logged(monkeypatch):
+ msgs = [SystemdServicesTasks(to_enable=['hello.service'],
+ to_disable=['hello.service'])]
+ mocked_run = MockedRun()
+ monkeypatch.setattr(setsystemdservicesstate, 'run', mocked_run)
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
+
+ setsystemdservicesstate.process()
+
+ expect_msg = ('Attempted to both enable and disable systemd service "hello.service",'
+ ' service will be disabled.')
+ assert expect_msg in api.current_logger.errmsg
diff --git a/repos/system_upgrade/common/models/systemdservices.py b/repos/system_upgrade/common/models/systemdservices.py
new file mode 100644
index 00000000..6c7d4a1d
--- /dev/null
+++ b/repos/system_upgrade/common/models/systemdservices.py
@@ -0,0 +1,22 @@
+from leapp.models import fields, Model
+from leapp.topics import SystemInfoTopic
+
+
+class SystemdServicesTasks(Model):
+ topic = SystemInfoTopic
+
+ to_enable = fields.List(fields.String(), default=[])
+ """
+ List of systemd services to enable on the target system
+
+ Masked services will not be enabled. Attempting to enable a masked service
+ will be evaluated by systemctl as usually. The error will be logged and the
+ upgrade process will continue.
+ """
+ to_disable = fields.List(fields.String(), default=[])
+ """
+ List of systemd services to disable on the target system
+ """
+
+ # Note: possible extension in case of requirement (currently not implemented):
+ # to_unmask = fields.List(fields.String(), default=[])
--
2.38.1