leapp-repository/SOURCES/0027-Fix-broken-or-incorrec...

254 lines
10 KiB
Diff

From 7a61c281946ffa0436da8f8837074f17e2103361 Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
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