forked from rpms/leapp-repository
75c9028095
- Enable new upgrade path for RHEL 8.10 -> RHEL 9.4 (including RHEL with SAP HANA) - Introduce generic transition of systemd services states during the IPU - Introduce possibility to upgrade with local repositories - Improve possibilities of upgrade when a proxy is configured in DNF configutation file - Fix handling of symlinks under /etc/pki when managing certificates - Fix the upgrade with custom https repositories - Default to the NO_RHSM mode when subscription-manager is not installed - Detect customized configuration of dynamic linker - Drop the invalid `tuv` target channel for the --channel option - Fix the issue of going out of bounds in the isccfg parser - Fix traceback when saving the rhsm facts results and the /etc/rhsm/facts directory doesn’t exist yet - Load all rpm repository substitutions that dnf knows about, not just "releasever" only - Simplify handling of upgrades on systems using RHUI, reducing the maintenance burden for cloud providers - Detect possible unexpected RPM GPG keys has been installed during RPM transaction - Resolves: RHEL-16729
532 lines
19 KiB
Diff
532 lines
19 KiB
Diff
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
|
|
|