Skip to content

Commit

Permalink
Transition systemd service states during upgrade
Browse files Browse the repository at this point in the history
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
  • Loading branch information
matejmatuska committed Oct 6, 2023
1 parent 6ae2d5a commit b4f0345
Show file tree
Hide file tree
Showing 3 changed files with 483 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit b4f0345

Please sign in to comment.