1248 lines
49 KiB
Diff
1248 lines
49 KiB
Diff
|
From fac07e2aeb59092871fbd9807e718c6f6da1193d Mon Sep 17 00:00:00 2001
|
||
|
From: Matej Matuska <mmatuska@redhat.com>
|
||
|
Date: Fri, 26 Aug 2022 15:33:44 +0200
|
||
|
Subject: [PATCH 25/32] Provide common information about systemd
|
||
|
|
||
|
Introduced new actors providing information about systemd services,
|
||
|
preset files, and broken symlinks. Both for the source and the
|
||
|
target system. All related actors are under
|
||
|
repos/system_upgrade/common/actors/systemd/
|
||
|
directory. Also it's introduced the systemd shared library
|
||
|
providing basic functionality for the gathering of systemd data.
|
||
|
Currently all functions from the library that are not called by
|
||
|
actors directly are marked as private. Note that provided functions
|
||
|
could raise exceptions. The handling of exceptions is expected to be
|
||
|
done inside actors. See docstrings for the set of possible exceptions.
|
||
|
|
||
|
In case of the source system, all data is valid during the upgrade
|
||
|
and in case the required data cannot be obtained, the upgrade
|
||
|
is interrupted.
|
||
|
|
||
|
However in case of data about the target system we speak about
|
||
|
a snapshot, how the system looks like in the moment after the upgrade
|
||
|
rpm transaction (data is collected during the Application phase).
|
||
|
The data can be used for the basic overview of the system configuration
|
||
|
after the upgrade transaction and also does not have to be necessary
|
||
|
produced! In case of an error a particular *Target* msg is not
|
||
|
produced, to make clear we could not collect correctly the required
|
||
|
data. But the upgrade is not interrupted in this phase and the errors
|
||
|
are logged only.
|
||
|
|
||
|
Systemd symlinks (under /etc/systemd/system/) prior the upgrade are
|
||
|
reported during the preupgrade, so administrator could fix them
|
||
|
prior the upgrade.
|
||
|
|
||
|
It's expected to create post-upgrade reports in future, but currently
|
||
|
skipping this topic until the post-upgrade reports are defined by
|
||
|
the leapp framework.
|
||
|
|
||
|
Introduced models (messages):
|
||
|
* SystemdBrokenSymlinksSource
|
||
|
* SystemdServicesInfoSource
|
||
|
* SystemdServicesPresetInfoSource
|
||
|
|
||
|
* SystemdBrokenSymlinksTarget
|
||
|
* SystemdServicesInfoTarget
|
||
|
* SystemdServicesPresetInfoTarget
|
||
|
---
|
||
|
.../libraries/checksystemdservicetasks.py | 10 +-
|
||
|
.../actors/systemd/scansystemdsource/actor.py | 25 ++
|
||
|
.../libraries/scansystemdsource.py | 45 +++
|
||
|
.../tests/test_scansystemdsource.py | 100 +++++++
|
||
|
.../actors/systemd/scansystemdtarget/actor.py | 28 ++
|
||
|
.../libraries/scansystemdtarget.py | 37 +++
|
||
|
.../tests/test_scansystemdtarget.py | 110 ++++++++
|
||
|
.../common/libraries/systemd.py | 216 ++++++++++++++
|
||
|
.../common/libraries/tests/00-test.preset | 10 +
|
||
|
.../common/libraries/tests/01-test.preset | 4 +
|
||
|
.../common/libraries/tests/05-invalid.preset | 8 +
|
||
|
.../common/libraries/tests/test_systemd.py | 263 ++++++++++++++++++
|
||
|
.../tests/test_systemd_files/abc.service | 0
|
||
|
.../tests/test_systemd_files/example.service | 0
|
||
|
.../tests/test_systemd_files/example.socket | 0
|
||
|
.../tests/test_systemd_files/extra.service | 0
|
||
|
.../test_systemd_files/globbed-one.service | 0
|
||
|
.../test_systemd_files/globbed-two.service | 0
|
||
|
.../test_systemd_files/template2@.service | 0
|
||
|
.../test_systemd_files/template@.service | 0
|
||
|
repos/system_upgrade/common/models/systemd.py | 155 +++++++++++
|
||
|
.../common/models/systemdservices.py | 22 --
|
||
|
22 files changed, 1005 insertions(+), 28 deletions(-)
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdsource/actor.py
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdsource/libraries/scansystemdsource.py
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdsource/tests/test_scansystemdsource.py
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdtarget/actor.py
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdtarget/libraries/scansystemdtarget.py
|
||
|
create mode 100644 repos/system_upgrade/common/actors/systemd/scansystemdtarget/tests/test_scansystemdtarget.py
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/systemd.py
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/00-test.preset
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/01-test.preset
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/05-invalid.preset
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd.py
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/abc.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/example.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/example.socket
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/extra.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-one.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-two.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/template2@.service
|
||
|
create mode 100644 repos/system_upgrade/common/libraries/tests/test_systemd_files/template@.service
|
||
|
create mode 100644 repos/system_upgrade/common/models/systemd.py
|
||
|
delete mode 100644 repos/system_upgrade/common/models/systemdservices.py
|
||
|
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
|
||
|
index 75833e4f..4d1bcda7 100644
|
||
|
--- a/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/libraries/checksystemdservicetasks.py
|
||
|
@@ -5,18 +5,16 @@ from leapp.models import SystemdServicesTasks
|
||
|
FMT_LIST_SEPARATOR = '\n - '
|
||
|
|
||
|
|
||
|
-def _printable_conflicts(conflicts):
|
||
|
- return FMT_LIST_SEPARATOR + FMT_LIST_SEPARATOR.join(sorted(conflicts))
|
||
|
-
|
||
|
-
|
||
|
def _inhibit_upgrade_with_conflicts(conflicts):
|
||
|
summary = (
|
||
|
'The requested states for systemd services on the target system are in conflict.'
|
||
|
- ' The following systemd services were requested to be both enabled and disabled on the target system: {}'
|
||
|
+ ' The following systemd services were requested to be both enabled and'
|
||
|
+ ' disabled on the target system:{}{}'
|
||
|
+ .format(FMT_LIST_SEPARATOR, FMT_LIST_SEPARATOR.join(sorted(conflicts)))
|
||
|
)
|
||
|
report = [
|
||
|
reporting.Title('Conflicting requirements of systemd service states'),
|
||
|
- reporting.Summary(summary.format(_printable_conflicts(conflicts))),
|
||
|
+ reporting.Summary(summary),
|
||
|
reporting.Severity(reporting.Severity.HIGH),
|
||
|
reporting.Groups([reporting.Groups.SANITY]),
|
||
|
reporting.Groups([reporting.Groups.INHIBITOR]),
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdsource/actor.py b/repos/system_upgrade/common/actors/systemd/scansystemdsource/actor.py
|
||
|
new file mode 100644
|
||
|
index 00000000..04a504b9
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdsource/actor.py
|
||
|
@@ -0,0 +1,25 @@
|
||
|
+from leapp.actors import Actor
|
||
|
+from leapp.libraries.actor import scansystemdsource
|
||
|
+from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource, SystemdServicesPresetInfoSource
|
||
|
+from leapp.tags import FactsPhaseTag, IPUWorkflowTag
|
||
|
+
|
||
|
+
|
||
|
+class ScanSystemdSource(Actor):
|
||
|
+ """
|
||
|
+ Provides info about systemd on the source system
|
||
|
+
|
||
|
+ The provided info includes information about:
|
||
|
+ - vendor presets of services
|
||
|
+ - systemd service files, including their state
|
||
|
+ - broken systemd symlinks
|
||
|
+
|
||
|
+ There is an analogous actor :class:`ScanSystemdTarget` for target system.
|
||
|
+ """
|
||
|
+
|
||
|
+ name = 'scan_systemd_source'
|
||
|
+ consumes = ()
|
||
|
+ produces = (SystemdBrokenSymlinksSource, SystemdServicesInfoSource, SystemdServicesPresetInfoSource)
|
||
|
+ tags = (IPUWorkflowTag, FactsPhaseTag)
|
||
|
+
|
||
|
+ def process(self):
|
||
|
+ scansystemdsource.scan()
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdsource/libraries/scansystemdsource.py b/repos/system_upgrade/common/actors/systemd/scansystemdsource/libraries/scansystemdsource.py
|
||
|
new file mode 100644
|
||
|
index 00000000..f6d9599c
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdsource/libraries/scansystemdsource.py
|
||
|
@@ -0,0 +1,45 @@
|
||
|
+from leapp.exceptions import StopActorExecutionError
|
||
|
+from leapp.libraries.common import systemd
|
||
|
+from leapp.libraries.stdlib import api, CalledProcessError
|
||
|
+from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource, SystemdServicesPresetInfoSource
|
||
|
+
|
||
|
+
|
||
|
+def scan():
|
||
|
+ try:
|
||
|
+ broken_symlinks = systemd.get_broken_symlinks()
|
||
|
+ except (OSError, CalledProcessError) as err:
|
||
|
+ details = {'details': str(err)}
|
||
|
+ if isinstance(err, CalledProcessError):
|
||
|
+ details['stderr'] = err.stderr
|
||
|
+ raise StopActorExecutionError(
|
||
|
+ message='Cannot scan the system to list possible broken systemd symlinks.',
|
||
|
+ details=details
|
||
|
+ )
|
||
|
+
|
||
|
+ try:
|
||
|
+ services_files = systemd.get_service_files()
|
||
|
+ except CalledProcessError as err:
|
||
|
+ raise StopActorExecutionError(
|
||
|
+ message='Cannot obtain the list of systemd service unit files.',
|
||
|
+ details={'details': str(err), 'stderr': err.stderr}
|
||
|
+ )
|
||
|
+
|
||
|
+ try:
|
||
|
+ presets = systemd.get_system_service_preset_files(services_files, ignore_invalid_entries=False)
|
||
|
+ except (OSError, CalledProcessError) as err:
|
||
|
+ details = {'details': str(err)}
|
||
|
+ if isinstance(err, CalledProcessError):
|
||
|
+ details['stderr'] = err.stderr
|
||
|
+ raise StopActorExecutionError(
|
||
|
+ message='Cannot obtain the list of systemd preset files.',
|
||
|
+ details=details
|
||
|
+ )
|
||
|
+ except ValueError as err:
|
||
|
+ raise StopActorExecutionError(
|
||
|
+ message='Discovered an invalid systemd preset file.',
|
||
|
+ details={'details': str(err)}
|
||
|
+ )
|
||
|
+
|
||
|
+ api.produce(SystemdBrokenSymlinksSource(broken_symlinks=broken_symlinks))
|
||
|
+ api.produce(SystemdServicesInfoSource(service_files=services_files))
|
||
|
+ api.produce(SystemdServicesPresetInfoSource(presets=presets))
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdsource/tests/test_scansystemdsource.py b/repos/system_upgrade/common/actors/systemd/scansystemdsource/tests/test_scansystemdsource.py
|
||
|
new file mode 100644
|
||
|
index 00000000..7b95a2df
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdsource/tests/test_scansystemdsource.py
|
||
|
@@ -0,0 +1,100 @@
|
||
|
+import pytest
|
||
|
+
|
||
|
+from leapp.exceptions import StopActorExecutionError
|
||
|
+from leapp.libraries.actor import scansystemdsource
|
||
|
+from leapp.libraries.common import systemd
|
||
|
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
|
||
|
+from leapp.libraries.stdlib import api, CalledProcessError
|
||
|
+from leapp.models import (
|
||
|
+ SystemdServiceFile,
|
||
|
+ SystemdServicePreset,
|
||
|
+ SystemdServicesInfoSource,
|
||
|
+ SystemdServicesPresetInfoSource
|
||
|
+)
|
||
|
+
|
||
|
+_BROKEN_SYMLINKS = [
|
||
|
+ "/etc/systemd/system/multi-user.target.wants/vdo.service",
|
||
|
+ "/etc/systemd/system/multi-user.target.wants/rngd.service"
|
||
|
+]
|
||
|
+
|
||
|
+_SERVICE_FILES = [
|
||
|
+ SystemdServiceFile(name='getty@.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='vdo.service', state='disabled')
|
||
|
+]
|
||
|
+
|
||
|
+_PRESETS = [
|
||
|
+ SystemdServicePreset(service='getty@.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='vdo.service', state='disable'),
|
||
|
+]
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize(
|
||
|
+ ('broken_symlinks', 'files', 'presets'),
|
||
|
+ (
|
||
|
+ (_BROKEN_SYMLINKS, _SERVICE_FILES, _PRESETS),
|
||
|
+ ([], [], [])
|
||
|
+ )
|
||
|
+)
|
||
|
+def test_message_produced(monkeypatch, broken_symlinks, files, presets):
|
||
|
+
|
||
|
+ def get_broken_symlinks_mocked():
|
||
|
+ return broken_symlinks
|
||
|
+
|
||
|
+ def get_service_files_mocked():
|
||
|
+ return files
|
||
|
+
|
||
|
+ def get_system_service_preset_files_mocked(service_files, ignore_invalid_entries):
|
||
|
+ return presets
|
||
|
+
|
||
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
|
||
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
||
|
+ monkeypatch.setattr(systemd, 'get_broken_symlinks', get_broken_symlinks_mocked)
|
||
|
+ monkeypatch.setattr(systemd, 'get_service_files', get_service_files_mocked)
|
||
|
+ monkeypatch.setattr(systemd, 'get_system_service_preset_files', get_system_service_preset_files_mocked)
|
||
|
+
|
||
|
+ scansystemdsource.scan()
|
||
|
+
|
||
|
+ assert api.produce.called
|
||
|
+ assert api.produce.model_instances[0].broken_symlinks == broken_symlinks
|
||
|
+ assert api.produce.model_instances[1].service_files == files
|
||
|
+ assert api.produce.model_instances[2].presets == presets
|
||
|
+
|
||
|
+
|
||
|
+_CALL_PROC_ERR = CalledProcessError(
|
||
|
+ message='BooCalled',
|
||
|
+ command=['find'],
|
||
|
+ result={
|
||
|
+ 'stdout': 'stdout',
|
||
|
+ 'stderr': 'stderr',
|
||
|
+ 'exit_code': 1,
|
||
|
+ 'signal': 1,
|
||
|
+ 'pid': 1,
|
||
|
+ }
|
||
|
+)
|
||
|
+
|
||
|
+
|
||
|
+class GetOrRaise(object):
|
||
|
+ def __init__(self, value):
|
||
|
+ self.value = value
|
||
|
+
|
||
|
+ def __call__(self, *dummyArgs, **dummy):
|
||
|
+ if isinstance(self.value, list):
|
||
|
+ return self.value
|
||
|
+ raise self.value
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize('symlinks', [OSError('Boo'), _CALL_PROC_ERR, []])
|
||
|
+@pytest.mark.parametrize('files', [_CALL_PROC_ERR, []])
|
||
|
+@pytest.mark.parametrize('presets', [OSError('Boo'), _CALL_PROC_ERR, ValueError('Hamster'), []])
|
||
|
+def test_exception_handling(monkeypatch, symlinks, files, presets):
|
||
|
+ if symlinks == files == presets == []:
|
||
|
+ # covered by test above
|
||
|
+ return
|
||
|
+
|
||
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
|
||
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
||
|
+ monkeypatch.setattr(systemd, 'get_broken_symlinks', GetOrRaise(symlinks))
|
||
|
+ monkeypatch.setattr(systemd, 'get_service_files', GetOrRaise(files))
|
||
|
+ monkeypatch.setattr(systemd, 'get_system_service_preset_files', GetOrRaise(presets))
|
||
|
+ with pytest.raises(StopActorExecutionError):
|
||
|
+ scansystemdsource.scan()
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdtarget/actor.py b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/actor.py
|
||
|
new file mode 100644
|
||
|
index 00000000..185b30ac
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/actor.py
|
||
|
@@ -0,0 +1,28 @@
|
||
|
+from leapp.actors import Actor
|
||
|
+from leapp.libraries.actor import scansystemdtarget
|
||
|
+from leapp.models import SystemdBrokenSymlinksTarget, SystemdServicesInfoTarget, SystemdServicesPresetInfoTarget
|
||
|
+from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag
|
||
|
+
|
||
|
+
|
||
|
+class ScanSystemdTarget(Actor):
|
||
|
+ """
|
||
|
+ Provides info about systemd on the source system
|
||
|
+
|
||
|
+ The provided info includes information about:
|
||
|
+ - vendor presets of services
|
||
|
+ - systemd service files, including their state
|
||
|
+ - broken systemd symlinks
|
||
|
+
|
||
|
+ There is an analogous actor :class:`ScanSystemdSource` for source system
|
||
|
+
|
||
|
+ The actor ignore errors (errors are logged, but do not stop the upgrade).
|
||
|
+ If some data cannot be obtained, particular message is not produced.
|
||
|
+ Actors are expected to check whether the data is available.
|
||
|
+ """
|
||
|
+ name = 'scan_systemd_target'
|
||
|
+ consumes = ()
|
||
|
+ produces = (SystemdBrokenSymlinksTarget, SystemdServicesInfoTarget, SystemdServicesPresetInfoTarget)
|
||
|
+ tags = (IPUWorkflowTag, ApplicationsPhaseTag)
|
||
|
+
|
||
|
+ def process(self):
|
||
|
+ scansystemdtarget.scan()
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdtarget/libraries/scansystemdtarget.py b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/libraries/scansystemdtarget.py
|
||
|
new file mode 100644
|
||
|
index 00000000..9c922c93
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/libraries/scansystemdtarget.py
|
||
|
@@ -0,0 +1,37 @@
|
||
|
+from leapp.libraries.common import systemd
|
||
|
+from leapp.libraries.stdlib import api, CalledProcessError
|
||
|
+from leapp.models import SystemdBrokenSymlinksTarget, SystemdServicesInfoTarget, SystemdServicesPresetInfoTarget
|
||
|
+
|
||
|
+
|
||
|
+def scan_broken_symlinks():
|
||
|
+ try:
|
||
|
+ broken_symlinks = systemd.get_broken_symlinks()
|
||
|
+ except (OSError, CalledProcessError):
|
||
|
+ return
|
||
|
+ api.produce(SystemdBrokenSymlinksTarget(broken_symlinks=broken_symlinks))
|
||
|
+
|
||
|
+
|
||
|
+def scan_service_files():
|
||
|
+ try:
|
||
|
+ services_files = systemd.get_service_files()
|
||
|
+ except CalledProcessError:
|
||
|
+ return None
|
||
|
+ api.produce(SystemdServicesInfoTarget(service_files=services_files))
|
||
|
+ return services_files
|
||
|
+
|
||
|
+
|
||
|
+def scan_preset_files(services_files):
|
||
|
+ if services_files is None:
|
||
|
+ return
|
||
|
+ try:
|
||
|
+ presets = systemd.get_system_service_preset_files(services_files, ignore_invalid_entries=True)
|
||
|
+ except (OSError, CalledProcessError):
|
||
|
+ return
|
||
|
+ api.produce(SystemdServicesPresetInfoTarget(presets=presets))
|
||
|
+
|
||
|
+
|
||
|
+def scan():
|
||
|
+ # Errors are logged inside the systemd library, no need to log them here again.
|
||
|
+ scan_broken_symlinks()
|
||
|
+ services_files = scan_service_files()
|
||
|
+ scan_preset_files(services_files)
|
||
|
diff --git a/repos/system_upgrade/common/actors/systemd/scansystemdtarget/tests/test_scansystemdtarget.py b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/tests/test_scansystemdtarget.py
|
||
|
new file mode 100644
|
||
|
index 00000000..227ba61a
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/actors/systemd/scansystemdtarget/tests/test_scansystemdtarget.py
|
||
|
@@ -0,0 +1,110 @@
|
||
|
+import pytest
|
||
|
+
|
||
|
+from leapp.libraries.actor import scansystemdtarget
|
||
|
+from leapp.libraries.common import systemd
|
||
|
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
|
||
|
+from leapp.libraries.stdlib import api, CalledProcessError
|
||
|
+from leapp.models import (
|
||
|
+ SystemdBrokenSymlinksTarget,
|
||
|
+ SystemdServiceFile,
|
||
|
+ SystemdServicePreset,
|
||
|
+ SystemdServicesInfoTarget,
|
||
|
+ SystemdServicesPresetInfoTarget
|
||
|
+)
|
||
|
+
|
||
|
+_BROKEN_SYMLINKS = [
|
||
|
+ "/etc/systemd/system/multi-user.target.wants/vdo.service",
|
||
|
+ "/etc/systemd/system/multi-user.target.wants/rngd.service"
|
||
|
+]
|
||
|
+
|
||
|
+_SERVICE_FILES = [
|
||
|
+ SystemdServiceFile(name='getty@.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='vdo.service', state='disabled')
|
||
|
+]
|
||
|
+
|
||
|
+_PRESETS = [
|
||
|
+ SystemdServicePreset(service='getty@.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='vdo.service', state='disable'),
|
||
|
+]
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize(
|
||
|
+ ('broken_symlinks', 'files', 'presets'),
|
||
|
+ (
|
||
|
+ (_BROKEN_SYMLINKS, _SERVICE_FILES, _PRESETS),
|
||
|
+ ([], [], [])
|
||
|
+ )
|
||
|
+)
|
||
|
+def test_message_produced(monkeypatch, broken_symlinks, files, presets):
|
||
|
+
|
||
|
+ def scan_broken_symlinks_mocked():
|
||
|
+ return broken_symlinks
|
||
|
+
|
||
|
+ def get_service_files_mocked():
|
||
|
+ return files
|
||
|
+
|
||
|
+ def get_system_service_preset_files_mocked(service_files, ignore_invalid_entries):
|
||
|
+ return presets
|
||
|
+
|
||
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
|
||
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
||
|
+ monkeypatch.setattr(systemd, 'get_broken_symlinks', scan_broken_symlinks_mocked)
|
||
|
+ monkeypatch.setattr(systemd, 'get_service_files', get_service_files_mocked)
|
||
|
+ monkeypatch.setattr(systemd, 'get_system_service_preset_files', get_system_service_preset_files_mocked)
|
||
|
+
|
||
|
+ scansystemdtarget.scan()
|
||
|
+
|
||
|
+ assert api.produce.called
|
||
|
+ assert api.produce.model_instances[0].broken_symlinks == broken_symlinks
|
||
|
+ assert api.produce.model_instances[1].service_files == files
|
||
|
+ assert api.produce.model_instances[2].presets == presets
|
||
|
+
|
||
|
+
|
||
|
+_CALL_PROC_ERR = CalledProcessError(
|
||
|
+ message='BooCalled',
|
||
|
+ command=['find'],
|
||
|
+ result={
|
||
|
+ 'stdout': 'stdout',
|
||
|
+ 'stderr': 'stderr',
|
||
|
+ 'exit_code': 1,
|
||
|
+ 'signal': 1,
|
||
|
+ 'pid': 1,
|
||
|
+ }
|
||
|
+)
|
||
|
+
|
||
|
+
|
||
|
+class GetOrRaise(object):
|
||
|
+ def __init__(self, value):
|
||
|
+ self.value = value
|
||
|
+
|
||
|
+ def __call__(self, *dummyArgs, **dummy):
|
||
|
+ if isinstance(self.value, list):
|
||
|
+ return self.value
|
||
|
+ raise self.value
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize('symlinks', [OSError('Boo'), _CALL_PROC_ERR, []])
|
||
|
+@pytest.mark.parametrize('files', [_CALL_PROC_ERR, []])
|
||
|
+@pytest.mark.parametrize('presets', [OSError('Boo'), _CALL_PROC_ERR, []])
|
||
|
+def test_exception_handling(monkeypatch, symlinks, files, presets):
|
||
|
+
|
||
|
+ def check_msg(input_data, msg_type, msgs, is_msg_expected):
|
||
|
+ for msg in msgs.model_instances:
|
||
|
+ if isinstance(msg, msg_type):
|
||
|
+ return is_msg_expected
|
||
|
+ return not is_msg_expected
|
||
|
+
|
||
|
+ if symlinks == files == presets == []:
|
||
|
+ # covered by test above
|
||
|
+ return
|
||
|
+
|
||
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
|
||
|
+ monkeypatch.setattr(api, 'produce', produce_mocked())
|
||
|
+ monkeypatch.setattr(systemd, 'get_broken_symlinks', GetOrRaise(symlinks))
|
||
|
+ monkeypatch.setattr(systemd, 'get_service_files', GetOrRaise(files))
|
||
|
+ monkeypatch.setattr(systemd, 'get_system_service_preset_files', GetOrRaise(presets))
|
||
|
+ scansystemdtarget.scan()
|
||
|
+ assert check_msg(symlinks, SystemdBrokenSymlinksTarget, api.produce, isinstance(symlinks, list))
|
||
|
+ assert check_msg(files, SystemdServicesInfoTarget, api.produce, isinstance(files, list))
|
||
|
+ is_msg_expected = isinstance(files, list) and isinstance(presets, list)
|
||
|
+ assert check_msg(presets, SystemdServicesPresetInfoTarget, api.produce, is_msg_expected)
|
||
|
diff --git a/repos/system_upgrade/common/libraries/systemd.py b/repos/system_upgrade/common/libraries/systemd.py
|
||
|
new file mode 100644
|
||
|
index 00000000..bbf71af7
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/libraries/systemd.py
|
||
|
@@ -0,0 +1,216 @@
|
||
|
+import fnmatch
|
||
|
+import os
|
||
|
+
|
||
|
+from leapp.libraries.stdlib import api, CalledProcessError, run
|
||
|
+from leapp.models import SystemdServiceFile, SystemdServicePreset
|
||
|
+
|
||
|
+SYSTEMD_SYMLINKS_DIR = '/etc/systemd/system/'
|
||
|
+
|
||
|
+_SYSTEMCTL_CMD_OPTIONS = ['--type=service', '--all', '--plain', '--no-legend']
|
||
|
+_USR_PRESETS_PATH = '/usr/lib/systemd/system-preset/'
|
||
|
+_ETC_PRESETS_PATH = '/etc/systemd/system-preset/'
|
||
|
+
|
||
|
+SYSTEMD_SYSTEM_LOAD_PATH = [
|
||
|
+ '/etc/systemd/system',
|
||
|
+ '/usr/lib/systemd/system'
|
||
|
+]
|
||
|
+
|
||
|
+
|
||
|
+def get_broken_symlinks():
|
||
|
+ """
|
||
|
+ Get broken systemd symlinks on the system
|
||
|
+
|
||
|
+ :return: List of broken systemd symlinks
|
||
|
+ :rtype: list[str]
|
||
|
+ :raises: CalledProcessError: if the `find` command fails
|
||
|
+ :raises: OSError: if the find utility is not found
|
||
|
+ """
|
||
|
+ try:
|
||
|
+ return run(['find', SYSTEMD_SYMLINKS_DIR, '-xtype', 'l'], split=True)['stdout']
|
||
|
+ except (OSError, CalledProcessError):
|
||
|
+ api.current_logger().error('Cannot obtain the list of broken systemd symlinks.')
|
||
|
+ raise
|
||
|
+
|
||
|
+
|
||
|
+def get_service_files():
|
||
|
+ """
|
||
|
+ Get list of unit files of systemd services on the system
|
||
|
+
|
||
|
+ The list includes template units.
|
||
|
+
|
||
|
+ :return: List of service unit files with states
|
||
|
+ :rtype: list[SystemdServiceFile]
|
||
|
+ :raises: CalledProcessError: in case of failure of `systemctl` command
|
||
|
+ """
|
||
|
+ services_files = []
|
||
|
+ try:
|
||
|
+ cmd = ['systemctl', 'list-unit-files'] + _SYSTEMCTL_CMD_OPTIONS
|
||
|
+ service_units_data = run(cmd, split=True)['stdout']
|
||
|
+ except CalledProcessError as err:
|
||
|
+ api.current_logger().error('Cannot obtain the list of unit files:{}'.format(str(err)))
|
||
|
+ raise
|
||
|
+
|
||
|
+ for entry in service_units_data:
|
||
|
+ columns = entry.split()
|
||
|
+ services_files.append(SystemdServiceFile(name=columns[0], state=columns[1]))
|
||
|
+ return services_files
|
||
|
+
|
||
|
+
|
||
|
+def _join_presets_resolving_overrides(etc_files, usr_files):
|
||
|
+ """
|
||
|
+ Join presets and resolve preset file overrides
|
||
|
+
|
||
|
+ Preset files in /etc/ override those with the same name in /usr/.
|
||
|
+ If such a file is a symlink to /dev/null, it disables the one in /usr/ instead.
|
||
|
+
|
||
|
+ :param etc_files: Systemd preset files in /etc/
|
||
|
+ :param usr_files: Systemd preset files in /usr/
|
||
|
+ :return: List of preset files in /etc/ and /usr/ with overridden files removed
|
||
|
+ """
|
||
|
+ for etc_file in etc_files:
|
||
|
+ filename = os.path.basename(etc_file)
|
||
|
+ for usr_file in usr_files:
|
||
|
+ if filename == os.path.basename(usr_file):
|
||
|
+ usr_files.remove(usr_file)
|
||
|
+ if os.path.islink(etc_file) and os.readlink(etc_file) == '/dev/null':
|
||
|
+ etc_files.remove(etc_file)
|
||
|
+
|
||
|
+ return etc_files + usr_files
|
||
|
+
|
||
|
+
|
||
|
+def _search_preset_files(path):
|
||
|
+ """
|
||
|
+ Search preset files in the given path
|
||
|
+
|
||
|
+ Presets are search recursively in the given directory.
|
||
|
+ If path isn't an existing directory, return empty list.
|
||
|
+
|
||
|
+ :param path: The path to search preset files in
|
||
|
+ :return: List of found preset files
|
||
|
+ :rtype: list[str]
|
||
|
+ :raises: CalledProcessError: if the `find` command fails
|
||
|
+ :raises: OSError: if the find utility is not found
|
||
|
+ """
|
||
|
+ if os.path.isdir(path):
|
||
|
+ try:
|
||
|
+ return run(['find', path, '-name', '*.preset'], split=True)['stdout']
|
||
|
+ except (OSError, CalledProcessError) as err:
|
||
|
+ api.current_logger().error('Cannot obtain list of systemd preset files in {}:{}'.format(path, str(err)))
|
||
|
+ raise
|
||
|
+ else:
|
||
|
+ return []
|
||
|
+
|
||
|
+
|
||
|
+def _get_system_preset_files():
|
||
|
+ """
|
||
|
+ Get systemd system preset files and remove overriding entries. Entries in /run/systemd/system are ignored.
|
||
|
+
|
||
|
+ :return: List of system systemd preset files
|
||
|
+ :raises: CalledProcessError: if the `find` command fails
|
||
|
+ :raises: OSError: if the find utility is not found
|
||
|
+ """
|
||
|
+ etc_files = _search_preset_files(_ETC_PRESETS_PATH)
|
||
|
+ usr_files = _search_preset_files(_USR_PRESETS_PATH)
|
||
|
+
|
||
|
+ preset_files = _join_presets_resolving_overrides(etc_files, usr_files)
|
||
|
+ preset_files.sort()
|
||
|
+ return preset_files
|
||
|
+
|
||
|
+
|
||
|
+def _recursive_glob(pattern, root_dir):
|
||
|
+ for _, _, filenames in os.walk(root_dir):
|
||
|
+ for filename in filenames:
|
||
|
+ if fnmatch.fnmatch(filename, pattern):
|
||
|
+ yield filename
|
||
|
+
|
||
|
+
|
||
|
+def _parse_preset_entry(entry, presets, load_path):
|
||
|
+ """
|
||
|
+ Parse a single entry (line) in a preset file
|
||
|
+
|
||
|
+ Single entry might set presets on multiple units using globs.
|
||
|
+
|
||
|
+ :param entry: The entry to parse
|
||
|
+ :param presets: Dictionary to store the presets into
|
||
|
+ :param load_path: List of paths to look systemd unit files up in
|
||
|
+ """
|
||
|
+
|
||
|
+ columns = entry.split()
|
||
|
+ if len(columns) < 2 or columns[0] not in ('enable', 'disable'):
|
||
|
+ raise ValueError('Invalid preset file entry: "{}"'.format(entry))
|
||
|
+
|
||
|
+ for path in load_path:
|
||
|
+ # TODO(mmatuska): This currently also globs non unit files,
|
||
|
+ # so the results need to be filtered with something like endswith('.<unit_type>')
|
||
|
+ unit_files = _recursive_glob(columns[1], root_dir=path)
|
||
|
+
|
||
|
+ for unit_file in unit_files:
|
||
|
+ if '@' in columns[1] and len(columns) > 2:
|
||
|
+ # unit is a template,
|
||
|
+ # if the entry contains instance names after template unit name
|
||
|
+ # the entry only applies to the specified instances, not to the
|
||
|
+ # template itself
|
||
|
+ for instance in columns[2:]:
|
||
|
+ service_name = unit_file[:unit_file.index('@') + 1] + instance + '.service'
|
||
|
+ if service_name not in presets: # first occurrence has priority
|
||
|
+ presets[service_name] = columns[0]
|
||
|
+
|
||
|
+ elif unit_file not in presets: # first occurrence has priority
|
||
|
+ presets[unit_file] = columns[0]
|
||
|
+
|
||
|
+
|
||
|
+def _parse_preset_files(preset_files, load_path, ignore_invalid_entries):
|
||
|
+ """
|
||
|
+ Parse presets from preset files
|
||
|
+
|
||
|
+ :param load_path: List of paths to search units at
|
||
|
+ :param ignore_invalid_entries: Whether to ignore invalid entries in preset files or raise an error
|
||
|
+ :return: Dictionary mapping systemd units to their preset state
|
||
|
+ :rtype: dict[str, str]
|
||
|
+ :raises: ValueError: when a preset file has invalid content
|
||
|
+ """
|
||
|
+ presets = {}
|
||
|
+
|
||
|
+ for preset in preset_files:
|
||
|
+ with open(preset, 'r') as preset_file:
|
||
|
+ for line in preset_file:
|
||
|
+ stripped = line.strip()
|
||
|
+ if stripped and stripped[0] not in ('#', ';'): # ignore comments
|
||
|
+ try:
|
||
|
+ _parse_preset_entry(stripped, presets, load_path)
|
||
|
+ except ValueError as err:
|
||
|
+ new_msg = 'Invalid preset file {pfile}: {error}'.format(pfile=preset, error=str(err))
|
||
|
+ if ignore_invalid_entries:
|
||
|
+ api.current_logger().warning(new_msg)
|
||
|
+ continue
|
||
|
+ raise ValueError(new_msg)
|
||
|
+ return presets
|
||
|
+
|
||
|
+
|
||
|
+def get_system_service_preset_files(service_files, ignore_invalid_entries=False):
|
||
|
+ """
|
||
|
+ Get system preset files for services
|
||
|
+
|
||
|
+ Presets for static and transient services are filtered out.
|
||
|
+
|
||
|
+ :param services_files: List of service unit files
|
||
|
+ :param ignore_invalid_entries: Ignore invalid entries in preset files if True, raise ValueError otherwise
|
||
|
+ :return: List of system systemd services presets
|
||
|
+ :rtype: list[SystemdServicePreset]
|
||
|
+ :raises: CalledProcessError: In case of errors when discovering systemd preset files
|
||
|
+ :raises: OSError: When the `find` command is not available
|
||
|
+ :raises: ValueError: When a preset file has invalid content and ignore_invalid_entries is False
|
||
|
+ """
|
||
|
+ preset_files = _get_system_preset_files()
|
||
|
+ presets = _parse_preset_files(preset_files, SYSTEMD_SYSTEM_LOAD_PATH, ignore_invalid_entries)
|
||
|
+
|
||
|
+ preset_models = []
|
||
|
+ for unit, state in presets.items():
|
||
|
+ if unit.endswith('.service'):
|
||
|
+ service_file = next(iter([s for s in service_files if s.name == unit]), None)
|
||
|
+ # presets can also be set on instances of template services which don't have a unit file
|
||
|
+ if service_file and service_file.state in ('static', 'transient'):
|
||
|
+ continue
|
||
|
+ preset_models.append(SystemdServicePreset(service=unit, state=state))
|
||
|
+
|
||
|
+ return preset_models
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/00-test.preset b/repos/system_upgrade/common/libraries/tests/00-test.preset
|
||
|
new file mode 100644
|
||
|
index 00000000..85e4cb0b
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/libraries/tests/00-test.preset
|
||
|
@@ -0,0 +1,10 @@
|
||
|
+enable example.service
|
||
|
+# first line takes priority
|
||
|
+disable example.service
|
||
|
+
|
||
|
+# hello, world!
|
||
|
+disable abc.service
|
||
|
+
|
||
|
+; another comment format
|
||
|
+disable template@.service
|
||
|
+enable template@.service instance1 instance2
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/01-test.preset b/repos/system_upgrade/common/libraries/tests/01-test.preset
|
||
|
new file mode 100644
|
||
|
index 00000000..6ef393c4
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/libraries/tests/01-test.preset
|
||
|
@@ -0,0 +1,4 @@
|
||
|
+disable example.*
|
||
|
+enable globbed*.service
|
||
|
+
|
||
|
+disable *
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/05-invalid.preset b/repos/system_upgrade/common/libraries/tests/05-invalid.preset
|
||
|
new file mode 100644
|
||
|
index 00000000..9ec39de1
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/libraries/tests/05-invalid.preset
|
||
|
@@ -0,0 +1,8 @@
|
||
|
+# missing unit or glob
|
||
|
+enable
|
||
|
+; missing enable or disable
|
||
|
+hello.service
|
||
|
+# only enable and disable directives are allowed
|
||
|
+mask hello.service
|
||
|
+
|
||
|
+disable example.service
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd.py b/repos/system_upgrade/common/libraries/tests/test_systemd.py
|
||
|
new file mode 100644
|
||
|
index 00000000..a91fce11
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/libraries/tests/test_systemd.py
|
||
|
@@ -0,0 +1,263 @@
|
||
|
+import os
|
||
|
+from functools import partial
|
||
|
+
|
||
|
+import pytest
|
||
|
+
|
||
|
+from leapp.libraries.common import systemd
|
||
|
+from leapp.libraries.common.testutils import logger_mocked
|
||
|
+from leapp.libraries.stdlib import api
|
||
|
+from leapp.models import SystemdServiceFile, SystemdServicePreset
|
||
|
+
|
||
|
+CURR_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
+
|
||
|
+
|
||
|
+def test_get_service_files(monkeypatch):
|
||
|
+ def run_mocked(cmd, *args, **kwargs):
|
||
|
+ if cmd == ['systemctl', 'list-unit-files'] + systemd._SYSTEMCTL_CMD_OPTIONS:
|
||
|
+ return {'stdout': [
|
||
|
+ 'auditd.service enabled',
|
||
|
+ 'crond.service enabled ',
|
||
|
+ 'dbus.service static ',
|
||
|
+ 'dnf-makecache.service static ',
|
||
|
+ 'firewalld.service enabled ',
|
||
|
+ 'getty@.service enabled ',
|
||
|
+ 'gssproxy.service disabled',
|
||
|
+ 'kdump.service enabled ',
|
||
|
+ 'mdmon@.service static ',
|
||
|
+ 'nfs.service disabled',
|
||
|
+ 'polkit.service static ',
|
||
|
+ 'rescue.service static ',
|
||
|
+ 'rngd.service enabled ',
|
||
|
+ 'rsyncd.service disabled',
|
||
|
+ 'rsyncd@.service static ',
|
||
|
+ 'smartd.service enabled ',
|
||
|
+ 'sshd.service enabled ',
|
||
|
+ 'sshd@.service static ',
|
||
|
+ 'wpa_supplicant.service disabled'
|
||
|
+ ]}
|
||
|
+ raise ValueError('Attempted to call unexpected command: {}'.format(cmd))
|
||
|
+
|
||
|
+ monkeypatch.setattr(systemd, 'run', run_mocked)
|
||
|
+ service_files = systemd.get_service_files()
|
||
|
+
|
||
|
+ expected = [
|
||
|
+ SystemdServiceFile(name='auditd.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='crond.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='dbus.service', state='static'),
|
||
|
+ SystemdServiceFile(name='dnf-makecache.service', state='static'),
|
||
|
+ SystemdServiceFile(name='firewalld.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='getty@.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='gssproxy.service', state='disabled'),
|
||
|
+ SystemdServiceFile(name='kdump.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='mdmon@.service', state='static'),
|
||
|
+ SystemdServiceFile(name='nfs.service', state='disabled'),
|
||
|
+ SystemdServiceFile(name='polkit.service', state='static'),
|
||
|
+ SystemdServiceFile(name='rescue.service', state='static'),
|
||
|
+ SystemdServiceFile(name='rngd.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='rsyncd.service', state='disabled'),
|
||
|
+ SystemdServiceFile(name='rsyncd@.service', state='static'),
|
||
|
+ SystemdServiceFile(name='smartd.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='sshd.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='sshd@.service', state='static'),
|
||
|
+ SystemdServiceFile(name='wpa_supplicant.service', state='disabled')
|
||
|
+ ]
|
||
|
+
|
||
|
+ assert service_files == expected
|
||
|
+
|
||
|
+
|
||
|
+def test_preset_files_overrides():
|
||
|
+ etc_files = [
|
||
|
+ '/etc/systemd/system-preset/00-abc.preset',
|
||
|
+ '/etc/systemd/system-preset/preset_without_prio.preset'
|
||
|
+ ]
|
||
|
+ usr_files = [
|
||
|
+ '/usr/lib/systemd/system-preset/00-abc.preset',
|
||
|
+ '/usr/lib/systemd/system-preset/99-xyz.preset',
|
||
|
+ '/usr/lib/systemd/system-preset/preset_without_prio.preset'
|
||
|
+ ]
|
||
|
+
|
||
|
+ expected = [
|
||
|
+ '/usr/lib/systemd/system-preset/99-xyz.preset',
|
||
|
+ '/etc/systemd/system-preset/00-abc.preset',
|
||
|
+ '/etc/systemd/system-preset/preset_without_prio.preset'
|
||
|
+ ]
|
||
|
+
|
||
|
+ presets = systemd._join_presets_resolving_overrides(etc_files, usr_files)
|
||
|
+ assert sorted(presets) == sorted(expected)
|
||
|
+
|
||
|
+
|
||
|
+def test_preset_files_block_override(monkeypatch):
|
||
|
+ etc_files = [
|
||
|
+ '/etc/systemd/system-preset/00-abc.preset'
|
||
|
+ ]
|
||
|
+ usr_files = [
|
||
|
+ '/usr/lib/systemd/system-preset/00-abc.preset',
|
||
|
+ '/usr/lib/systemd/system-preset/99-xyz.preset'
|
||
|
+ ]
|
||
|
+
|
||
|
+ expected = [
|
||
|
+ '/usr/lib/systemd/system-preset/99-xyz.preset',
|
||
|
+ ]
|
||
|
+
|
||
|
+ def islink_mocked(path):
|
||
|
+ return path == '/etc/systemd/system-preset/00-abc.preset'
|
||
|
+
|
||
|
+ def readlink_mocked(path):
|
||
|
+ if path == '/etc/systemd/system-preset/00-abc.preset':
|
||
|
+ return '/dev/null'
|
||
|
+ raise OSError
|
||
|
+
|
||
|
+ monkeypatch.setattr(os.path, 'islink', islink_mocked)
|
||
|
+ monkeypatch.setattr(os, 'readlink', readlink_mocked)
|
||
|
+
|
||
|
+ presets = systemd._join_presets_resolving_overrides(etc_files, usr_files)
|
||
|
+ assert sorted(presets) == sorted(expected)
|
||
|
+
|
||
|
+
|
||
|
+TEST_SYSTEMD_LOAD_PATH = [os.path.join(CURR_DIR, 'test_systemd_files/')]
|
||
|
+
|
||
|
+TESTING_PRESET_FILES = [
|
||
|
+ os.path.join(CURR_DIR, '00-test.preset'),
|
||
|
+ os.path.join(CURR_DIR, '01-test.preset')
|
||
|
+]
|
||
|
+
|
||
|
+TESTING_PRESET_WITH_INVALID_ENTRIES = os.path.join(CURR_DIR, '05-invalid.preset')
|
||
|
+
|
||
|
+_PARSE_PRESET_ENTRIES_TEST_DEFINITION = (
|
||
|
+ ('enable example.service', {'example.service': 'enable'}),
|
||
|
+ ('disable abc.service', {'abc.service': 'disable'}),
|
||
|
+ ('enable template@.service', {'template@.service': 'enable'}),
|
||
|
+ ('disable template2@.service', {'template2@.service': 'disable'}),
|
||
|
+ ('disable template@.service instance1 instance2', {
|
||
|
+ 'template@instance1.service': 'disable',
|
||
|
+ 'template@instance2.service': 'disable'
|
||
|
+ }),
|
||
|
+ ('enable globbed*.service', {'globbed-one.service': 'enable', 'globbed-two.service': 'enable'}),
|
||
|
+ ('enable example.*', {'example.service': 'enable', 'example.socket': 'enable'}),
|
||
|
+ ('disable *', {
|
||
|
+ 'example.service': 'disable',
|
||
|
+ 'abc.service': 'disable',
|
||
|
+ 'template@.service': 'disable',
|
||
|
+ 'template2@.service': 'disable',
|
||
|
+ 'globbed-one.service': 'disable',
|
||
|
+ 'globbed-two.service': 'disable',
|
||
|
+ 'example.socket': 'disable',
|
||
|
+ 'extra.service': 'disable'
|
||
|
+ })
|
||
|
+)
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize('entry,expected', _PARSE_PRESET_ENTRIES_TEST_DEFINITION)
|
||
|
+def test_parse_preset_entry(monkeypatch, entry, expected):
|
||
|
+ presets = {}
|
||
|
+ systemd._parse_preset_entry(entry, presets, TEST_SYSTEMD_LOAD_PATH)
|
||
|
+ assert presets == expected
|
||
|
+
|
||
|
+
|
||
|
+@pytest.mark.parametrize(
|
||
|
+ 'entry',
|
||
|
+ [
|
||
|
+ ('hello.service'),
|
||
|
+ ('mask hello.service'),
|
||
|
+ ('enable'),
|
||
|
+ ]
|
||
|
+)
|
||
|
+def test_parse_preset_entry_invalid(monkeypatch, entry):
|
||
|
+ presets = {}
|
||
|
+ with pytest.raises(ValueError, match=r'^Invalid preset file entry: '):
|
||
|
+ systemd._parse_preset_entry(entry, presets, TEST_SYSTEMD_LOAD_PATH)
|
||
|
+
|
||
|
+
|
||
|
+def test_parse_preset_files(monkeypatch):
|
||
|
+
|
||
|
+ expected = {
|
||
|
+ 'example.service': 'enable',
|
||
|
+ 'example.socket': 'disable',
|
||
|
+ 'abc.service': 'disable',
|
||
|
+ 'template@.service': 'disable',
|
||
|
+ 'template@instance1.service': 'enable',
|
||
|
+ 'template@instance2.service': 'enable',
|
||
|
+ 'globbed-one.service': 'enable',
|
||
|
+ 'globbed-two.service': 'enable',
|
||
|
+ 'extra.service': 'disable',
|
||
|
+ 'template2@.service': 'disable'
|
||
|
+ }
|
||
|
+
|
||
|
+ presets = systemd._parse_preset_files(TESTING_PRESET_FILES, TEST_SYSTEMD_LOAD_PATH, False)
|
||
|
+ assert presets == expected
|
||
|
+
|
||
|
+
|
||
|
+def test_parse_preset_files_invalid():
|
||
|
+ with pytest.raises(ValueError):
|
||
|
+ systemd._parse_preset_files(
|
||
|
+ [TESTING_PRESET_WITH_INVALID_ENTRIES], TEST_SYSTEMD_LOAD_PATH, ignore_invalid_entries=False
|
||
|
+ )
|
||
|
+
|
||
|
+
|
||
|
+def test_parse_preset_files_ignore_invalid(monkeypatch):
|
||
|
+ monkeypatch.setattr(api, 'current_logger', logger_mocked())
|
||
|
+
|
||
|
+ invalid_preset_files = [TESTING_PRESET_WITH_INVALID_ENTRIES]
|
||
|
+ presets = systemd._parse_preset_files(
|
||
|
+ invalid_preset_files, TEST_SYSTEMD_LOAD_PATH, ignore_invalid_entries=True
|
||
|
+ )
|
||
|
+
|
||
|
+ for entry in ('enable', 'hello.service', 'mask hello.service'):
|
||
|
+ msg = 'Invalid preset file {}: Invalid preset file entry: "{}"'.format(invalid_preset_files[0], entry)
|
||
|
+ assert msg in api.current_logger.warnmsg
|
||
|
+
|
||
|
+ assert presets == {'example.service': 'disable'}
|
||
|
+
|
||
|
+
|
||
|
+def parse_preset_files_mocked():
|
||
|
+ mocked = partial(systemd._parse_preset_files, load_path=TEST_SYSTEMD_LOAD_PATH)
|
||
|
+
|
||
|
+ def impl(preset_files, load_path, ignore_invalid_entries):
|
||
|
+ return mocked(preset_files, ignore_invalid_entries=ignore_invalid_entries)
|
||
|
+ return impl
|
||
|
+
|
||
|
+
|
||
|
+def test_get_service_preset_files(monkeypatch):
|
||
|
+
|
||
|
+ def get_system_preset_files_mocked():
|
||
|
+ return TESTING_PRESET_FILES
|
||
|
+
|
||
|
+ monkeypatch.setattr(systemd, '_get_system_preset_files', get_system_preset_files_mocked)
|
||
|
+ monkeypatch.setattr(systemd, '_parse_preset_files', parse_preset_files_mocked())
|
||
|
+
|
||
|
+ service_files = [
|
||
|
+ SystemdServiceFile(name='abc.service', state='transient'),
|
||
|
+ SystemdServiceFile(name='example.service', state='static'),
|
||
|
+ SystemdServiceFile(name='example.socket', state='masked'),
|
||
|
+ SystemdServiceFile(name='extra.service', state='disabled'),
|
||
|
+ SystemdServiceFile(name='template2@.service', state='enabled'),
|
||
|
+ SystemdServiceFile(name='template@.service', state='enabled'),
|
||
|
+ ]
|
||
|
+
|
||
|
+ expected = [
|
||
|
+ # dont expect example.service since it's static
|
||
|
+ # dont expect abc.service since it's transient
|
||
|
+ SystemdServicePreset(service='template@.service', state='disable'),
|
||
|
+ SystemdServicePreset(service='template@instance1.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='template@instance2.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='globbed-one.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='globbed-two.service', state='enable'),
|
||
|
+ SystemdServicePreset(service='extra.service', state='disable'),
|
||
|
+ SystemdServicePreset(service='template2@.service', state='disable')
|
||
|
+ ]
|
||
|
+
|
||
|
+ presets = systemd.get_system_service_preset_files(service_files, False)
|
||
|
+ assert sorted(presets, key=lambda e: e.service) == sorted(expected, key=lambda e: e.service)
|
||
|
+
|
||
|
+
|
||
|
+def test_get_service_preset_files_invalid(monkeypatch):
|
||
|
+
|
||
|
+ def get_system_preset_files_mocked():
|
||
|
+ return [TESTING_PRESET_WITH_INVALID_ENTRIES]
|
||
|
+
|
||
|
+ monkeypatch.setattr(systemd, '_get_system_preset_files', get_system_preset_files_mocked)
|
||
|
+ monkeypatch.setattr(systemd, '_parse_preset_files', parse_preset_files_mocked())
|
||
|
+
|
||
|
+ with pytest.raises(ValueError):
|
||
|
+ # doesn't matter what service_files are
|
||
|
+ systemd.get_system_service_preset_files([], ignore_invalid_entries=False)
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/abc.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/abc.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/example.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/example.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/example.socket b/repos/system_upgrade/common/libraries/tests/test_systemd_files/example.socket
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/extra.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/extra.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-one.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-one.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-two.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/globbed-two.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/template2@.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/template2@.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/libraries/tests/test_systemd_files/template@.service b/repos/system_upgrade/common/libraries/tests/test_systemd_files/template@.service
|
||
|
new file mode 100644
|
||
|
index 00000000..e69de29b
|
||
|
diff --git a/repos/system_upgrade/common/models/systemd.py b/repos/system_upgrade/common/models/systemd.py
|
||
|
new file mode 100644
|
||
|
index 00000000..f66ae5dd
|
||
|
--- /dev/null
|
||
|
+++ b/repos/system_upgrade/common/models/systemd.py
|
||
|
@@ -0,0 +1,155 @@
|
||
|
+from leapp.models import fields, Model
|
||
|
+from leapp.topics import SystemInfoTopic
|
||
|
+
|
||
|
+
|
||
|
+class SystemdBrokenSymlinksSource(Model):
|
||
|
+ """
|
||
|
+ Information about broken systemd symlinks on the source system
|
||
|
+ """
|
||
|
+
|
||
|
+ topic = SystemInfoTopic
|
||
|
+ broken_symlinks = fields.List(fields.String(), default=[])
|
||
|
+ """
|
||
|
+ List of broken systemd symlinks on the source system
|
||
|
+
|
||
|
+ The values are absolute paths of the broken symlinks.
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdBrokenSymlinksTarget(SystemdBrokenSymlinksSource):
|
||
|
+ """
|
||
|
+ Analogy to :class:`SystemdBrokenSymlinksSource`, but for the target system
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicesTasks(Model):
|
||
|
+ """
|
||
|
+ Influence the systemd services of the target system
|
||
|
+
|
||
|
+ E.g. it could be specified explicitly whether some services should
|
||
|
+ be enabled or disabled after the in-place upgrade - follow descriptions
|
||
|
+ of particular tasks for details.
|
||
|
+
|
||
|
+ In case of conflicting tasks (e.g. the A service should be enabled and
|
||
|
+ disabled in the same time):
|
||
|
+ a) If conflicting tasks are detected during check phases,
|
||
|
+ the upgrade is inhibited with the proper report.
|
||
|
+ b) If conflicting tasks are detected during the final evaluation,
|
||
|
+ error logs are created and such services will be disabled.
|
||
|
+ """
|
||
|
+ topic = SystemInfoTopic
|
||
|
+
|
||
|
+ to_enable = fields.List(fields.String(), default=[])
|
||
|
+ """
|
||
|
+ List of systemd services to enable on the target system
|
||
|
+
|
||
|
+ Masked services will not be enabled. Attempting to enable a masked service
|
||
|
+ will be evaluated by systemctl as usually. The error will be logged and the
|
||
|
+ upgrade process will continue.
|
||
|
+ """
|
||
|
+
|
||
|
+ to_disable = fields.List(fields.String(), default=[])
|
||
|
+ """
|
||
|
+ List of systemd services to disable on the target system
|
||
|
+ """
|
||
|
+
|
||
|
+ # NOTE: possible extension in case of requirement (currently not implemented):
|
||
|
+ # to_unmask = fields.List(fields.String(), default=[])
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServiceFile(Model):
|
||
|
+ """
|
||
|
+ Information about single systemd service unit file
|
||
|
+
|
||
|
+ This model is not expected to be produced nor consumed by actors directly.
|
||
|
+ See the :class:`SystemdServicesInfoSource` and :class:`SystemdServicesPresetInfoTarget`
|
||
|
+ for more info.
|
||
|
+ """
|
||
|
+ topic = SystemInfoTopic
|
||
|
+
|
||
|
+ name = fields.String()
|
||
|
+ """
|
||
|
+ Name of the service unit file
|
||
|
+ """
|
||
|
+
|
||
|
+ state = fields.StringEnum([
|
||
|
+ 'alias',
|
||
|
+ 'bad',
|
||
|
+ 'disabled',
|
||
|
+ 'enabled',
|
||
|
+ 'enabled-runtime',
|
||
|
+ 'generated',
|
||
|
+ 'indirect',
|
||
|
+ 'linked',
|
||
|
+ 'linked-runtime',
|
||
|
+ 'masked',
|
||
|
+ 'masked-runtime',
|
||
|
+ 'static',
|
||
|
+ 'transient',
|
||
|
+ ])
|
||
|
+ """
|
||
|
+ The state of the service unit file
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicesInfoSource(Model):
|
||
|
+ """
|
||
|
+ Information about systemd services on the source system
|
||
|
+ """
|
||
|
+ topic = SystemInfoTopic
|
||
|
+
|
||
|
+ service_files = fields.List(fields.Model(SystemdServiceFile), default=[])
|
||
|
+ """
|
||
|
+ List of all installed systemd service unit files
|
||
|
+
|
||
|
+ Instances of service template unit files don't have a unit file
|
||
|
+ and therefore aren't included, but their template files are.
|
||
|
+ Generated service unit files are also included.
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicesInfoTarget(SystemdServicesInfoSource):
|
||
|
+ """
|
||
|
+ Analogy to :class:`SystemdServicesInfoSource`, but for the target system
|
||
|
+
|
||
|
+ This information is taken after the RPM Upgrade and might become
|
||
|
+ invalid if there are actors calling systemctl enable/disable directly later
|
||
|
+ in the upgrade process. Therefore it is recommended to use
|
||
|
+ :class:`SystemdServicesTasks` to alter the state of units in the
|
||
|
+ FinalizationPhase.
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicePreset(Model):
|
||
|
+ """
|
||
|
+ Information about a preset for systemd service
|
||
|
+ """
|
||
|
+
|
||
|
+ topic = SystemInfoTopic
|
||
|
+ service = fields.String()
|
||
|
+ """
|
||
|
+ Name of the service, with the .service suffix
|
||
|
+ """
|
||
|
+
|
||
|
+ state = fields.StringEnum(['disable', 'enable'])
|
||
|
+ """
|
||
|
+ The state set by a preset file
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicesPresetInfoSource(Model):
|
||
|
+ """
|
||
|
+ Information about presets for systemd services
|
||
|
+ """
|
||
|
+ topic = SystemInfoTopic
|
||
|
+
|
||
|
+ presets = fields.List(fields.Model(SystemdServicePreset), default=[])
|
||
|
+ """
|
||
|
+ List of all service presets
|
||
|
+ """
|
||
|
+
|
||
|
+
|
||
|
+class SystemdServicesPresetInfoTarget(SystemdServicesPresetInfoSource):
|
||
|
+ """
|
||
|
+ Analogy to :class:`SystemdServicesPresetInfoSource` but for the target system
|
||
|
+ """
|
||
|
diff --git a/repos/system_upgrade/common/models/systemdservices.py b/repos/system_upgrade/common/models/systemdservices.py
|
||
|
deleted file mode 100644
|
||
|
index 6c7d4a1d..00000000
|
||
|
--- a/repos/system_upgrade/common/models/systemdservices.py
|
||
|
+++ /dev/null
|
||
|
@@ -1,22 +0,0 @@
|
||
|
-from leapp.models import fields, Model
|
||
|
-from leapp.topics import SystemInfoTopic
|
||
|
-
|
||
|
-
|
||
|
-class SystemdServicesTasks(Model):
|
||
|
- topic = SystemInfoTopic
|
||
|
-
|
||
|
- to_enable = fields.List(fields.String(), default=[])
|
||
|
- """
|
||
|
- List of systemd services to enable on the target system
|
||
|
-
|
||
|
- Masked services will not be enabled. Attempting to enable a masked service
|
||
|
- will be evaluated by systemctl as usually. The error will be logged and the
|
||
|
- upgrade process will continue.
|
||
|
- """
|
||
|
- to_disable = fields.List(fields.String(), default=[])
|
||
|
- """
|
||
|
- List of systemd services to disable on the target system
|
||
|
- """
|
||
|
-
|
||
|
- # Note: possible extension in case of requirement (currently not implemented):
|
||
|
- # to_unmask = fields.List(fields.String(), default=[])
|
||
|
--
|
||
|
2.38.1
|
||
|
|