leapp-repository/0018-Transition-systemd-service-states-during-upgrade.patch

532 lines
19 KiB
Diff
Raw Normal View History

From f50de2d3f541ca64934b4488dd1a403c8783a5da Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Tue, 14 Mar 2023 23:26:30 +0100
Subject: [PATCH 18/38] Transition systemd service states during upgrade
Sometimes after the upgrade some services end up disabled even if they
have been enabled on the source system.
There are already two separate actors that fix this for
`device_cio_free.service` and `rsyncd.service`.
A new actor `transition-systemd-services-states` handles this generally
for all services. A "desired" state is determined depending on state and
vendor preset of both source and target system and a
SystemdServicesTasks message is produced with each service that isn't
already in the "desired" state.
Jira ref.: OAMG-1745
---
.../transitionsystemdservicesstates/actor.py | 53 +++++
.../transitionsystemdservicesstates.py | 211 +++++++++++++++++
.../test_transitionsystemdservicesstates.py | 219 ++++++++++++++++++
3 files changed, 483 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/actor.py
create mode 100644 repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
create mode 100644 repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/tests/test_transitionsystemdservicesstates.py
diff --git a/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/actor.py b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/actor.py
new file mode 100644
index 00000000..139f9f6b
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/actor.py
@@ -0,0 +1,53 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import transitionsystemdservicesstates
+from leapp.models import (
+ SystemdServicesInfoSource,
+ SystemdServicesInfoTarget,
+ SystemdServicesPresetInfoSource,
+ SystemdServicesPresetInfoTarget,
+ SystemdServicesTasks
+)
+from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag
+
+
+class TransitionSystemdServicesStates(Actor):
+ """
+ Transition states of systemd services between source and target systems
+
+ Services on the target system might end up in incorrect/unexpected state
+ after an upgrade. This actor puts such services into correct/expected
+ state.
+
+ A SystemdServicesTasks message is produced containing all tasks that need
+ to be executed to put all services into the correct states.
+
+ The correct states are determined according to following rules:
+ - All enabled services remain enabled
+ - All masked services remain masked
+ - Disabled services will be enabled if they are disabled by default on
+ the source system (by preset files), but enabled by default on target
+ system, otherwise they will remain disabled
+ - Runtime enabled service (state == runtime-enabled) are treated
+ the same as disabled services
+ - Services in other states are not handled as they can't be
+ enabled/disabled
+
+ Two reports are generated:
+ - Report with services that were corrected from disabled to enabled on
+ the upgraded system
+ - Report with services that were newly enabled on the upgraded system
+ by a preset
+ """
+
+ name = 'transition_systemd_services_states'
+ consumes = (
+ SystemdServicesInfoSource,
+ SystemdServicesInfoTarget,
+ SystemdServicesPresetInfoSource,
+ SystemdServicesPresetInfoTarget
+ )
+ produces = (SystemdServicesTasks,)
+ tags = (ApplicationsPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ transitionsystemdservicesstates.process()
diff --git a/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
new file mode 100644
index 00000000..494271ae
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
@@ -0,0 +1,211 @@
+from leapp import reporting
+from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.stdlib import api
+from leapp.models import (
+ SystemdServicesInfoSource,
+ SystemdServicesInfoTarget,
+ SystemdServicesPresetInfoSource,
+ SystemdServicesPresetInfoTarget,
+ SystemdServicesTasks
+)
+
+FMT_LIST_SEPARATOR = "\n - "
+
+
+def _get_desired_service_state(state_source, preset_source, preset_target):
+ """
+ Get the desired service state on the target system
+
+ :param state_source: State on the source system
+ :param preset_source: Preset on the source system
+ :param preset_target: Preset on the target system
+ :return: The desired state on the target system
+ """
+
+ if state_source in ("disabled", "enabled-runtime"):
+ if preset_source == "disable":
+ return preset_target + "d" # use the default from target
+
+ return state_source
+
+
+def _get_desired_states(
+ services_source, presets_source, services_target, presets_target
+):
+ "Get the states that services should be in on the target system"
+ desired_states = {}
+
+ for service in services_target:
+ state_source = services_source.get(service.name)
+ preset_target = _get_service_preset(service.name, presets_target)
+ preset_source = _get_service_preset(service.name, presets_source)
+
+ desired_state = _get_desired_service_state(
+ state_source, preset_source, preset_target
+ )
+ desired_states[service.name] = desired_state
+
+ return desired_states
+
+
+def _get_service_task(service_name, desired_state, state_target, tasks):
+ """
+ Get the task to set the desired state of the service on the target system
+
+ :param service_name: Then name of the service
+ :param desired_state: The state the service should set to
+ :param state_target: State on the target system
+ :param tasks: The tasks to append the task to
+ """
+ if desired_state == state_target:
+ return
+
+ if desired_state == "enabled":
+ tasks.to_enable.append(service_name)
+ if desired_state == "disabled":
+ tasks.to_disable.append(service_name)
+
+
+def _get_service_preset(service_name, presets):
+ preset = presets.get(service_name)
+ if not preset:
+ # shouldn't really happen as there is usually a `disable *` glob as
+ # the last statement in the presets
+ api.current_logger().debug(
+ 'No presets found for service "{}", assuming "disable"'.format(service_name)
+ )
+ return "disable"
+ return preset
+
+
+def _filter_services(services_source, services_target):
+ """
+ Filter out irrelevant services
+ """
+ filtered = []
+ for service in services_target:
+ if service.state not in ("enabled", "disabled", "enabled-runtime"):
+ # Enabling/disabling of services is only relevant to these states
+ continue
+
+ state_source = services_source.get(service.name)
+ if not state_source:
+ # The service doesn't exist on the source system
+ continue
+
+ if state_source == "masked-runtime":
+ # TODO(mmatuska): It's not possible to get the persistent
+ # (non-runtime) state of a service with `systemctl`. One solution
+ # might be to check symlinks
+ api.current_logger().debug(
+ 'Skipping service in "masked-runtime" state: {}'.format(service.name)
+ )
+ continue
+
+ filtered.append(service)
+
+ return filtered
+
+
+def _get_required_tasks(services_target, desired_states):
+ """
+ Get the required tasks to set the services on the target system to their desired state
+
+ :return: The tasks required to be executed
+ :rtype: SystemdServicesTasks
+ """
+ tasks = SystemdServicesTasks()
+
+ for service in services_target:
+ desired_state = desired_states[service.name]
+ _get_service_task(service.name, desired_state, service.state, tasks)
+
+ return tasks
+
+
+def _report_kept_enabled(tasks):
+ summary = (
+ "Systemd services which were enabled on the system before the upgrade"
+ " were kept enabled after the upgrade. "
+ )
+ if tasks:
+ summary += (
+ "The following services were originally disabled on the upgraded system"
+ " and Leapp attempted to enable them:{}{}"
+ ).format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(tasks.to_enable)))
+ # TODO(mmatuska): When post-upgrade reports are implemented in
+ # `setsystemdservicesstates actor, add a note here to check the reports
+ # if the enabling failed
+
+ reporting.create_report(
+ [
+ reporting.Title("Previously enabled systemd services were kept enabled"),
+ reporting.Summary(summary),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.POST]),
+ ]
+ )
+
+
+def _get_newly_enabled(services_source, desired_states):
+ newly_enabled = []
+ for service, state in desired_states.items():
+ state_source = services_source[service]
+ if state_source == "disabled" and state == "enabled":
+ newly_enabled.append(service)
+
+ return newly_enabled
+
+
+def _report_newly_enabled(newly_enabled):
+ summary = (
+ "The following services were disabled before the upgrade and were set"
+ "to enabled by a systemd preset after the upgrade:{}{}.".format(
+ FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(newly_enabled))
+ )
+ )
+
+ reporting.create_report(
+ [
+ reporting.Title("Some systemd services were newly enabled"),
+ reporting.Summary(summary),
+ reporting.Severity(reporting.Severity.INFO),
+ reporting.Groups([reporting.Groups.POST]),
+ ]
+ )
+
+
+def _expect_message(model):
+ """
+ Get the expected message or throw an error
+ """
+ message = next(api.consume(model), None)
+ if not message:
+ raise StopActorExecutionError(
+ "Expected {} message, but didn't get any".format(model.__name__)
+ )
+ return message
+
+
+def process():
+ services_source = _expect_message(SystemdServicesInfoSource).service_files
+ services_target = _expect_message(SystemdServicesInfoTarget).service_files
+ presets_source = _expect_message(SystemdServicesPresetInfoSource).presets
+ presets_target = _expect_message(SystemdServicesPresetInfoTarget).presets
+
+ services_source = dict((p.name, p.state) for p in services_source)
+ presets_source = dict((p.service, p.state) for p in presets_source)
+ presets_target = dict((p.service, p.state) for p in presets_target)
+
+ services_target = _filter_services(services_source, services_target)
+
+ desired_states = _get_desired_states(
+ services_source, presets_source, services_target, presets_target
+ )
+ tasks = _get_required_tasks(services_target, desired_states)
+
+ api.produce(tasks)
+ _report_kept_enabled(tasks)
+
+ newly_enabled = _get_newly_enabled(services_source, desired_states)
+ _report_newly_enabled(newly_enabled)
diff --git a/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/tests/test_transitionsystemdservicesstates.py b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/tests/test_transitionsystemdservicesstates.py
new file mode 100644
index 00000000..a19afc7f
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/tests/test_transitionsystemdservicesstates.py
@@ -0,0 +1,219 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor import transitionsystemdservicesstates
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
+from leapp.libraries.stdlib import api
+from leapp.models import (
+ SystemdServiceFile,
+ SystemdServicePreset,
+ SystemdServicesInfoSource,
+ SystemdServicesInfoTarget,
+ SystemdServicesPresetInfoSource,
+ SystemdServicesPresetInfoTarget,
+ SystemdServicesTasks
+)
+
+
+@pytest.mark.parametrize(
+ "state_source, preset_source, preset_target, expected",
+ (
+ ["enabled", "disable", "enable", "enabled"],
+ ["enabled", "disable", "disable", "enabled"],
+ ["disabled", "disable", "disable", "disabled"],
+ ["disabled", "disable", "enable", "enabled"],
+ ["masked", "disable", "enable", "masked"],
+ ["masked", "disable", "disable", "masked"],
+ ["enabled", "enable", "enable", "enabled"],
+ ["enabled", "enable", "disable", "enabled"],
+ ["masked", "enable", "enable", "masked"],
+ ["masked", "enable", "disable", "masked"],
+ ["disabled", "enable", "enable", "disabled"],
+ ["disabled", "enable", "disable", "disabled"],
+ ),
+)
+def test_get_desired_service_state(
+ state_source, preset_source, preset_target, expected
+):
+ target_state = transitionsystemdservicesstates._get_desired_service_state(
+ state_source, preset_source, preset_target
+ )
+
+ assert target_state == expected
+
+
+@pytest.mark.parametrize(
+ "desired_state, state_target, expected",
+ (
+ ("enabled", "enabled", SystemdServicesTasks()),
+ ("enabled", "disabled", SystemdServicesTasks(to_enable=["test.service"])),
+ ("disabled", "enabled", SystemdServicesTasks(to_disable=["test.service"])),
+ ("disabled", "disabled", SystemdServicesTasks()),
+ ),
+)
+def test_get_service_task(monkeypatch, desired_state, state_target, expected):
+ def _get_desired_service_state_mocked(*args):
+ return desired_state
+
+ monkeypatch.setattr(
+ transitionsystemdservicesstates,
+ "_get_desired_service_state",
+ _get_desired_service_state_mocked,
+ )
+
+ tasks = SystemdServicesTasks()
+ transitionsystemdservicesstates._get_service_task(
+ "test.service", desired_state, state_target, tasks
+ )
+ assert tasks == expected
+
+
+def test_filter_services_services_filtered():
+ services_source = {
+ "test2.service": "static",
+ "test3.service": "masked",
+ "test4.service": "indirect",
+ "test5.service": "indirect",
+ "test6.service": "indirect",
+ }
+ services_target = [
+ SystemdServiceFile(name="test1.service", state="enabled"),
+ SystemdServiceFile(name="test2.service", state="masked"),
+ SystemdServiceFile(name="test3.service", state="indirect"),
+ SystemdServiceFile(name="test4.service", state="static"),
+ SystemdServiceFile(name="test5.service", state="generated"),
+ SystemdServiceFile(name="test6.service", state="masked-runtime"),
+ ]
+
+ filtered = transitionsystemdservicesstates._filter_services(
+ services_source, services_target
+ )
+
+ assert not filtered
+
+
+def test_filter_services_services_not_filtered():
+ services_source = {
+ "test1.service": "enabled",
+ "test2.service": "disabled",
+ "test3.service": "static",
+ "test4.service": "indirect",
+ }
+ services_target = [
+ SystemdServiceFile(name="test1.service", state="enabled"),
+ SystemdServiceFile(name="test2.service", state="disabled"),
+ SystemdServiceFile(name="test3.service", state="enabled-runtime"),
+ SystemdServiceFile(name="test4.service", state="enabled"),
+ ]
+
+ filtered = transitionsystemdservicesstates._filter_services(
+ services_source, services_target
+ )
+
+ assert len(filtered) == len(services_target)
+
+
+@pytest.mark.parametrize(
+ "presets",
+ [
+ dict(),
+ {"other.service": "enable"},
+ ],
+)
+def test_service_preset_missing_presets(presets):
+ preset = transitionsystemdservicesstates._get_service_preset(
+ "test.service", presets
+ )
+ assert preset == "disable"
+
+
+def test_tasks_produced_reports_created(monkeypatch):
+ services_source = [
+ SystemdServiceFile(name="rsyncd.service", state="enabled"),
+ SystemdServiceFile(name="test.service", state="enabled"),
+ ]
+ service_info_source = SystemdServicesInfoSource(service_files=services_source)
+
+ presets_source = [
+ SystemdServicePreset(service="rsyncd.service", state="enable"),
+ SystemdServicePreset(service="test.service", state="enable"),
+ ]
+ preset_info_source = SystemdServicesPresetInfoSource(presets=presets_source)
+
+ services_target = [
+ SystemdServiceFile(name="rsyncd.service", state="disabled"),
+ SystemdServiceFile(name="test.service", state="enabled"),
+ ]
+ service_info_target = SystemdServicesInfoTarget(service_files=services_target)
+
+ presets_target = [
+ SystemdServicePreset(service="rsyncd.service", state="enable"),
+ SystemdServicePreset(service="test.service", state="enable"),
+ ]
+ preset_info_target = SystemdServicesPresetInfoTarget(presets=presets_target)
+
+ monkeypatch.setattr(
+ api,
+ "current_actor",
+ CurrentActorMocked(
+ msgs=[
+ service_info_source,
+ service_info_target,
+ preset_info_source,
+ preset_info_target,
+ ]
+ ),
+ )
+ monkeypatch.setattr(api, "produce", produce_mocked())
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(reporting, "create_report", created_reports)
+
+ expected_tasks = SystemdServicesTasks(to_enable=["rsyncd.service"], to_disable=[])
+ transitionsystemdservicesstates.process()
+
+ assert created_reports.called == 2
+ assert api.produce.called
+ assert api.produce.model_instances[0].to_enable == expected_tasks.to_enable
+ assert api.produce.model_instances[0].to_disable == expected_tasks.to_disable
+
+
+def test_report_kept_enabled(monkeypatch):
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(reporting, "create_report", created_reports)
+
+ tasks = SystemdServicesTasks(
+ to_enable=["test.service", "other.service"], to_disable=["another.service"]
+ )
+ transitionsystemdservicesstates._report_kept_enabled(tasks)
+
+ assert created_reports.called
+ assert all([s in created_reports.report_fields["summary"] for s in tasks.to_enable])
+
+
+def test_get_newly_enabled():
+ services_source = {
+ "test.service": "disabled",
+ "other.service": "enabled",
+ "another.service": "enabled",
+ }
+ desired_states = {
+ "test.service": "enabled",
+ "other.service": "enabled",
+ "another.service": "disabled",
+ }
+
+ newly_enabled = transitionsystemdservicesstates._get_newly_enabled(
+ services_source, desired_states
+ )
+ assert newly_enabled == ['test.service']
+
+
+def test_report_newly_enabled(monkeypatch):
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(reporting, "create_report", created_reports)
+
+ newly_enabled = ["test.service", "other.service"]
+ transitionsystemdservicesstates._report_newly_enabled(newly_enabled)
+
+ assert created_reports.called
+ assert all([s in created_reports.report_fields["summary"] for s in newly_enabled])
--
2.41.0