384 lines
15 KiB
Diff
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
|
||
|
|