leapp-repository/SOURCES/0028-Add-check-for-systemd-symlinks-broken-before-the-upg.patch
2023-03-29 09:01:41 +00:00

272 lines
11 KiB
Diff

From 2713d60a99b60a352b89374dec89f6faa683861d Mon Sep 17 00:00:00 2001
From: Matej Matuska <mmatuska@redhat.com>
Date: Wed, 16 Nov 2022 14:19:36 +0100
Subject: [PATCH 28/32] Add check for systemd symlinks broken before the
upgrade
Broken systemd symlinks are not treated during the in-place upgrade
if they are broken prior the leapp execution. This could lead in
unwanted behaviour on the upgraded system, but it does not have to
- so we do not inhibit the upgrade when such symlinks are detected.
Also, such symlinks could have been created by previous in-place
upgrade, when an automatical fixing of broken symlinks have not been
implemented yet. By this actor we inform people about such issues,
so they can fix it prior the upgrade.
Co-authored-by: Petr Stodulka <pstodulk@redhat.com>
---
.../checksystemdbrokensymlinks/actor.py | 29 +++++
.../libraries/checksystemdbrokensymlinks.py | 106 ++++++++++++++++++
.../tests/test_checksystemdbrokensymlinks.py | 89 +++++++++++++++
3 files changed, 224 insertions(+)
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/actor.py
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
create mode 100644 repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/tests/test_checksystemdbrokensymlinks.py
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/actor.py b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/actor.py
new file mode 100644
index 00000000..257e8c33
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/actor.py
@@ -0,0 +1,29 @@
+from leapp.actors import Actor
+from leapp.libraries.actor import checksystemdbrokensymlinks
+from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource
+from leapp.reporting import Report
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
+
+
+class CheckSystemdBrokenSymlinks(Actor):
+ """
+ Check whether some systemd symlinks are broken
+
+ If some systemd symlinks are broken, report them but do not inhibit the
+ upgrade. The symlinks broken already before the upgrade will not be
+ handled by the upgrade process anyhow. Two different reports are created:
+ - symlinks which have the same filename as an existing enabled systemd
+ service (the symlink doesn't point to an existing unit file, but the
+ service is enabled)
+ - broken symlinks which names do not correspond with any existing systemd
+ unit file (typically when the service is removed but not disabled
+ correctly)
+ """
+
+ name = 'check_systemd_broken_symlinks'
+ consumes = (SystemdBrokenSymlinksSource, SystemdServicesInfoSource)
+ produces = (Report,)
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
+
+ def process(self):
+ checksystemdbrokensymlinks.process()
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
new file mode 100644
index 00000000..23addf72
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/libraries/checksystemdbrokensymlinks.py
@@ -0,0 +1,106 @@
+import os
+
+from leapp import reporting
+from leapp.exceptions import StopActorExecutionError
+from leapp.libraries.stdlib import api
+from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource
+
+FMT_LIST_SEPARATOR = '\n - '
+
+
+def _report_broken_symlinks(symlinks):
+ summary = (
+ 'Leapp detected broken systemd symlinks on the system that do not'
+ ' correspond to any installed systemd unit.'
+ ' This typically happens when the original systemd unit file has been'
+ ' removed (e.g. an rpm removal) or renamed and the system configration'
+ ' has not been properly modified.'
+ ' These symlinks will not be handled during the in-place upgrade'
+ ' as they are already broken.'
+ ' The list of detected broken systemd symlinks:{}{}'
+ .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(symlinks)))
+ )
+
+ command = ['/usr/bin/rm'] + symlinks
+
+ hint = (
+ 'Remove the invalid symlinks before the upgrade.'
+ )
+
+ reporting.create_report([
+ reporting.Title(
+ 'Detected broken systemd symlinks for non-existing services'
+ ),
+ reporting.Summary(summary),
+ reporting.Remediation(hint=hint, commands=[command]),
+ reporting.Severity(reporting.Severity.LOW),
+ reporting.Tags([reporting.Tags.FILESYSTEM]),
+ ])
+
+
+def _report_enabled_services_broken_symlinks(symlinks):
+ summary = (
+ 'Leapp detected broken systemd symlinks on the system that correspond'
+ ' to existing systemd units, but on different paths. This could lead'
+ ' in future to unexpected behaviour. Also, these symlinks will not be'
+ ' handled during the in-place upgrade as they are already broken.'
+ ' The list of detected broken symlinks:{}{}'
+ .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(symlinks)))
+ )
+
+ hint = (
+ 'Fix the broken symlinks before the upgrade or remove them. For this'
+ ' purpose, you can re-enable or disable the related systemd services'
+ ' using the systemctl tool.'
+ )
+
+ reporting.create_report([
+ reporting.Title(
+ 'Detected broken systemd symlinks for existing services'
+ ),
+ reporting.Summary(summary),
+ reporting.Remediation(hint=hint),
+ reporting.Severity(reporting.Severity.MEDIUM),
+ reporting.Tags([reporting.Tags.FILESYSTEM]),
+ ])
+
+
+def _is_enabled(unit, service_files):
+ # FIXME(pstodulk): currently our msgs contain only information about systemd
+ # services. If the unit (broken symlink) refers to timers, etc. They will
+ # be treated now as disabled (read: symlink is broken and there is not
+ # a corresponding unit-file on the system). Considering it for now as
+ # minor issue that will be resolved in future.
+ # NOTE: One of possible solution is to put the information about enabled broken
+ # symlinks to the msg, so it can be just consumed.
+ for service_file in service_files:
+ if service_file.name == unit:
+ return service_file.state == 'enabled'
+ return False
+
+
+def process():
+ broken_symlinks_info = next(api.consume(SystemdBrokenSymlinksSource), None)
+ if not broken_symlinks_info:
+ # nothing to do
+ return
+ services = next(api.consume(SystemdServicesInfoSource), None)
+ if not services:
+ # This is just a seatbelt. It's not expected this msg will be missing.
+ # Skipping tests.
+ raise StopActorExecutionError('Missing SystemdServicesInfoSource message.')
+
+ enabled_to_report = []
+ to_report = []
+ for broken_symlink in broken_symlinks_info.broken_symlinks:
+ unit = os.path.basename(broken_symlink)
+ if _is_enabled(unit, services.service_files):
+ enabled_to_report.append(broken_symlink)
+ else:
+ to_report.append(broken_symlink)
+
+ if enabled_to_report:
+ _report_enabled_services_broken_symlinks(enabled_to_report)
+
+ if to_report:
+ _report_broken_symlinks(to_report)
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/tests/test_checksystemdbrokensymlinks.py b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/tests/test_checksystemdbrokensymlinks.py
new file mode 100644
index 00000000..2364f7a5
--- /dev/null
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/tests/test_checksystemdbrokensymlinks.py
@@ -0,0 +1,89 @@
+import pytest
+
+from leapp import reporting
+from leapp.libraries.actor import checksystemdbrokensymlinks
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
+from leapp.libraries.stdlib import api
+from leapp.models import SystemdBrokenSymlinksSource, SystemdServiceFile, SystemdServicesInfoSource
+
+
+def test_report_broken_symlinks(monkeypatch):
+
+ symlinks = [
+ '/etc/systemd/system/multi-user.target.wants/hello.service',
+ '/etc/systemd/system/multi-user.target.wants/world.service',
+ ]
+
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ checksystemdbrokensymlinks._report_broken_symlinks(symlinks)
+
+ assert created_reports.called
+ assert all([s in created_reports.report_fields['summary'] for s in symlinks])
+
+
+def test_report_enabled_services_broken_symlinks(monkeypatch):
+ symlinks = [
+ '/etc/systemd/system/multi-user.target.wants/foo.service',
+ '/etc/systemd/system/multi-user.target.wants/bar.service',
+ ]
+
+ created_reports = create_report_mocked()
+ monkeypatch.setattr(reporting, 'create_report', created_reports)
+
+ checksystemdbrokensymlinks._report_enabled_services_broken_symlinks(symlinks)
+
+ assert created_reports.called
+ assert all([s in created_reports.report_fields['summary'] for s in symlinks])
+
+
+class ReportBrokenSymlinks(object):
+ def __init__(self):
+ self.symlinks = []
+
+ def __call__(self, unit, *args, **kwargs):
+ self.symlinks.append(unit)
+ return {}
+
+
+def test_broken_symlinks_reported(monkeypatch):
+ broken_symlinks = SystemdBrokenSymlinksSource(broken_symlinks=[
+ '/etc/systemd/system/multi-user.target.wants/foo.service',
+ '/etc/systemd/system/multi-user.target.wants/bar.service',
+ '/etc/systemd/system/multi-user.target.wants/hello.service',
+ '/etc/systemd/system/multi-user.target.wants/world.service',
+ ])
+ systemd_services = SystemdServicesInfoSource(service_files=[
+ SystemdServiceFile(name='foo.service', state='enabled'),
+ SystemdServiceFile(name='bar.service', state='enabled'),
+ SystemdServiceFile(name='hello.service', state='disabled'),
+ ])
+ broken = []
+ enabled_broken = []
+
+ def _report_broken_symlinks_mocked(symlinks):
+ broken.extend(symlinks)
+
+ def _report_enabled_services_broken_symlinks_mocked(symlinks):
+ enabled_broken.extend(symlinks)
+
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[broken_symlinks, systemd_services]))
+ monkeypatch.setattr(checksystemdbrokensymlinks, '_report_broken_symlinks', _report_broken_symlinks_mocked)
+ monkeypatch.setattr(
+ checksystemdbrokensymlinks,
+ '_report_enabled_services_broken_symlinks',
+ _report_enabled_services_broken_symlinks_mocked
+ )
+
+ checksystemdbrokensymlinks.process()
+
+ assert broken == [
+ '/etc/systemd/system/multi-user.target.wants/hello.service',
+ '/etc/systemd/system/multi-user.target.wants/world.service',
+ ]
+
+ assert enabled_broken == [
+ '/etc/systemd/system/multi-user.target.wants/foo.service',
+ '/etc/systemd/system/multi-user.target.wants/bar.service',
+ ]
--
2.38.1