From 7a61c281946ffa0436da8f8837074f17e2103361 Mon Sep 17 00:00:00 2001 From: Matej Matuska Date: Wed, 16 Nov 2022 14:11:39 +0100 Subject: [PATCH 27/32] Fix broken or incorrect systemd symlinks Introduce repairsystemdsymlinks actor. During the in-place upgrade process, it usually happens that some symlinks become incorrect - symlinks are broken, or they are defined in a wrong directory (e.g. when they are supposed to be defined in a different systemd target). This has various reasons, but usually it's caused by missing rpm scriptlets in particular rpms. This change corrects only systemd symlinks are (newly) broken during the in-place upgrade. Symlinks that have been already broken before the in-place upgrade are ignored. Symlinks are handled in the following fashion, if the symlink points to: - a removed unit, such a symlink is deleted - a unit whose installation has been changed (e.g. changed WantedBy), such symlinks are fixed (re-enabled using systemctl) JIRA: OAMG-5342 OAMG-5344 OAMG-6519 (possibly related) OAMG-7755 Bugzillas: https://bugzilla.redhat.com/show_bug.cgi?id=1988457 https://bugzilla.redhat.com/show_bug.cgi?id=1988449 https://bugzilla.redhat.com/show_bug.cgi?id=2055117 (possibly fixed) --- .../systemd/repairsystemdsymlinks/actor.py | 25 +++++ .../libraries/repairsystemdsymlinks.py | 76 ++++++++++++++++ .../tests/test_repairsystemdsymlinks.py | 91 +++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/actor.py create mode 100644 repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/libraries/repairsystemdsymlinks.py create mode 100644 repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/tests/test_repairsystemdsymlinks.py diff --git a/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/actor.py b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/actor.py new file mode 100644 index 00000000..29134373 --- /dev/null +++ b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/actor.py @@ -0,0 +1,25 @@ +from leapp.actors import Actor +from leapp.libraries.actor import repairsystemdsymlinks +from leapp.models import SystemdBrokenSymlinksSource, SystemdBrokenSymlinksTarget, SystemdServicesInfoSource +from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag + + +class RepairSystemdSymlinks(Actor): + """ + Fix broken or incorrect systemd symlinks + + Symlinks are handled in the following fashion, if the symlink points to: + - a removed unit, such a symlink is deleted + - a unit whose installation has been changed (e.g. changed WantedBy), + such symlinks are fixed (re-enabled using systemctl) + + Symlinks that have been already broken before the in-place upgrade are ignored. + """ + + name = 'repair_systemd_symlinks' + consumes = (SystemdBrokenSymlinksSource, SystemdBrokenSymlinksTarget, SystemdServicesInfoSource) + produces = () + tags = (ApplicationsPhaseTag, IPUWorkflowTag) + + def process(self): + repairsystemdsymlinks.process() diff --git a/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/libraries/repairsystemdsymlinks.py b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/libraries/repairsystemdsymlinks.py new file mode 100644 index 00000000..884b001e --- /dev/null +++ b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/libraries/repairsystemdsymlinks.py @@ -0,0 +1,76 @@ +import os + +from leapp.exceptions import StopActorExecutionError +from leapp.libraries.common import systemd +from leapp.libraries.common.config.version import get_target_major_version +from leapp.libraries.stdlib import api, CalledProcessError, run +from leapp.models import SystemdBrokenSymlinksSource, SystemdBrokenSymlinksTarget, SystemdServicesInfoSource + +_INSTALLATION_CHANGED_EL8 = ['rngd.service', 'sysstat.service'] +_INSTALLATION_CHANGED_EL9 = [] + + +def _get_installation_changed_units(): + version = get_target_major_version() + if version == '8': + return _INSTALLATION_CHANGED_EL8 + if version == '9': + return _INSTALLATION_CHANGED_EL9 + + return [] + + +def _service_enabled_source(service_info, name): + service_file = next((s for s in service_info.service_files if s.name == name), None) + return service_file and service_file.state == 'enabled' + + +def _is_unit_enabled(unit): + try: + ret = run(['systemctl', 'is-enabled', unit], split=True)['stdout'] + return ret and ret[0] == 'enabled' + except (OSError, CalledProcessError): + return False + + +def _handle_newly_broken_symlinks(symlinks, service_info): + for symlink in symlinks: + unit = os.path.basename(symlink) + try: + if not _is_unit_enabled(unit): + # removes the broken symlink + systemd.disable_unit(unit) + elif _service_enabled_source(service_info, unit) and _is_unit_enabled(unit): + # removes the old symlinks and creates the new ones + systemd.reenable_unit(unit) + except CalledProcessError: + # TODO(mmatuska): Produce post-upgrade report: failed to handle broken symlink (and suggest a fix?) + pass + + +def _handle_bad_symlinks(service_files): + install_changed_units = _get_installation_changed_units() + potentially_bad = [s for s in service_files if s.name in install_changed_units] + + for unit_file in potentially_bad: + if unit_file.state == 'enabled' and _is_unit_enabled(unit_file.name): + systemd.reenable_unit(unit_file.name) + + +def process(): + service_info_source = next(api.consume(SystemdServicesInfoSource), None) + if not service_info_source: + raise StopActorExecutionError("Expected SystemdServicesInfoSource message, but got None") + + source_info = next(api.consume(SystemdBrokenSymlinksSource), None) + target_info = next(api.consume(SystemdBrokenSymlinksTarget), None) + + if source_info and target_info: + newly_broken = [] + newly_broken = [s for s in target_info.broken_symlinks if s not in source_info.broken_symlinks] + if not newly_broken: + return + + _handle_newly_broken_symlinks(newly_broken, service_info_source) + + _handle_bad_symlinks(service_info_source.service_files) diff --git a/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/tests/test_repairsystemdsymlinks.py b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/tests/test_repairsystemdsymlinks.py new file mode 100644 index 00000000..2394df5e --- /dev/null +++ b/repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/tests/test_repairsystemdsymlinks.py @@ -0,0 +1,91 @@ +from leapp.libraries.actor import repairsystemdsymlinks +from leapp.libraries.common import systemd +from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api, CalledProcessError, run +from leapp.models import ( + SystemdBrokenSymlinksSource, + SystemdBrokenSymlinksTarget, + SystemdServiceFile, + SystemdServicesInfoSource +) + + +class MockedSystemdCmd(object): + def __init__(self): + self.units = [] + + def __call__(self, unit, *args, **kwargs): + self.units.append(unit) + return {} + + +def test_bad_symslinks(monkeypatch): + service_files = [ + SystemdServiceFile(name='rngd.service', state='enabled'), + SystemdServiceFile(name='sysstat.service', state='disabled'), + SystemdServiceFile(name='hello.service', state='enabled'), + SystemdServiceFile(name='world.service', state='disabled'), + ] + + def is_unit_enabled_mocked(unit): + return True + + monkeypatch.setattr(repairsystemdsymlinks, '_is_unit_enabled', is_unit_enabled_mocked) + + reenable_mocked = MockedSystemdCmd() + monkeypatch.setattr(systemd, 'reenable_unit', reenable_mocked) + + service_info = SystemdServicesInfoSource(service_files=service_files) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[service_info])) + + repairsystemdsymlinks._handle_bad_symlinks(service_info.service_files) + + assert reenable_mocked.units == ['rngd.service'] + + +def test_handle_newly_broken_symlink(monkeypatch): + + symlinks = [ + '/etc/systemd/system/default.target.wants/systemd-readahead-replay.service', + '/etc/systemd/system/multi-user.target.wants/vdo.service', + '/etc/systemd/system/multi-user.target.wants/hello.service', + '/etc/systemd/system/multi-user.target.wants/world.service', + '/etc/systemd/system/multi-user.target.wants/foo.service', + '/etc/systemd/system/multi-user.target.wants/bar.service', + ] + + def is_unit_enabled_mocked(unit): + return unit in ('hello.service', 'foo.service') + + expect_disabled = [ + 'systemd-readahead-replay.service', + 'vdo.service', + 'world.service', + 'bar.service', + ] + + expect_reenabled = [ + 'hello.service', + ] + + monkeypatch.setattr(repairsystemdsymlinks, '_is_unit_enabled', is_unit_enabled_mocked) + + reenable_mocked = MockedSystemdCmd() + monkeypatch.setattr(systemd, 'reenable_unit', reenable_mocked) + + disable_mocked = MockedSystemdCmd() + monkeypatch.setattr(systemd, 'disable_unit', disable_mocked) + + service_files = [ + SystemdServiceFile(name='systemd-readahead-replay.service', state='enabled'), + SystemdServiceFile(name='vdo.service', state='disabled'), + SystemdServiceFile(name='hello.service', state='enabled'), + SystemdServiceFile(name='world.service', state='disabled'), + SystemdServiceFile(name='foo.service', state='disabled'), + SystemdServiceFile(name='bar.service', state='enabled'), + ] + service_info = SystemdServicesInfoSource(service_files=service_files) + repairsystemdsymlinks._handle_newly_broken_symlinks(symlinks, service_info) + + assert reenable_mocked.units == expect_reenabled + assert disable_mocked.units == expect_disabled -- 2.38.1