From d2d7999744e97776eda664592ac0cc7ec5747b99 Mon Sep 17 00:00:00 2001 From: Matej Matuska 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