-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
6ae2d5a
commit b4f0345
Showing
3 changed files
with
483 additions
and
0 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
repos/system_upgrade/common/actors/systemd/transitionsystemdservicesstates/actor.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
211 changes: 211 additions & 0 deletions
211
...tors/systemd/transitionsystemdservicesstates/libraries/transitionsystemdservicesstates.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.