From 1ccd87afff41e5c175af15e3c88365b4e73fa96d Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Tue, 23 Jan 2024 08:36:54 +0100 Subject: [PATCH 1/3] Update SystemdService.exists to find all unit files --- testinfra/modules/service.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index 24e666cb..f2a2a896 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -176,12 +176,18 @@ def _has_systemd_suffix(self): @property def exists(self): - cmd = self.run_test('systemctl list-unit-files | grep -q "^%s"', self.name) - return cmd.rc == 0 + # systemctl return codes based on https://man7.org/linux/man-pages/man1/systemctl.1.html: + # 0: unit is active + # 1: unit not failed (used by is-failed) + # 2: unused + # 3: unit is not active + # 4: no such unit + cmd = self.run_expect([0, 1, 3, 4], "systemctl status %s", self.name) + return cmd.rc < 4 @property def is_running(self): - # based on https://man7.org/linux/man-pages/man1/systemctl.1.html + # systemctl return codes based on https://man7.org/linux/man-pages/man1/systemctl.1.html: # 0: program running # 1: program is dead and pid file exists # 3: not running and pid file does not exists From 2d02e2ebc623bfd708597392f2868b67dfa081bc Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Tue, 15 Apr 2025 21:50:10 +0200 Subject: [PATCH 2/3] Ignore non-relevant messages from "systemd-analyze verify" --- test/test_modules.py | 52 ++++++++++++++++++++++++++++++++++-- testinfra/modules/service.py | 23 +++++++++++++--- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/test/test_modules.py b/test/test_modules.py index 1057c646..7d19663a 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -127,8 +127,9 @@ def test_ssh_service(host, docker_image): assert ssh.is_enabled -def test_service_systemd_mask(host): - ssh = host.service("ssh") +def test_service_systemd_mask(host, docker_image): + name = "sshd" if docker_image == "rockylinux9" else "ssh" + ssh = host.service(name) assert not ssh.is_masked host.run("systemctl mask ssh") assert ssh.is_masked @@ -136,6 +137,53 @@ def test_service_systemd_mask(host): assert not ssh.is_masked +@all_images +def test_service_systemd_ssh(host, docker_image): + name = "sshd" if docker_image == "rockylinux9" else "ssh" + ssh = host.service(name) + assert ssh.exists + assert ssh.is_valid + assert ssh.is_enabled + assert ssh.is_running + + +def test_service_systemd_root_mount(host): + root = host.service("-.mount") # systemd unit for mounting / + assert root.exists + assert root.is_valid + assert root.is_running + + +# is_enabled does not work in Rocky Linux 9 +# $ systemctl status -- -.mount +# AssertionError: ● -.mount - Root Mount +# Loaded: loaded +# Active: active (mounted) since Wed 2025-04-16 21:03:04 UTC; 6s ago +# Until: Wed 2025-04-16 21:03:04 UTC; 6s ago +# Where: / +# What: overlay +# +# Notice: journal has been rotated since unit was started, output may be incomplete. +# +# $ systemctl is-enabled -- -.mount +# Failed to get unit file state for -.mount: No such file or directory +@pytest.mark.testinfra_hosts("docker://rockylinux9") +@pytest.mark.xfail( + reason='"systemctl is-enabled -- -.mount" fails even if "systemctl status" succeeds' +) +def test_service_systemd_root_mount_is_enabled(host): + root = host.service("-.mount") # systemd unit for mounting / + assert not root.is_enabled + + +def test_service_systemd_tmp_mount(host): + tmp = host.service("tmp.mount") + assert tmp.exists + assert tmp.is_valid + assert not tmp.is_enabled + assert not tmp.is_running + + def test_salt(host): ssh_version = host.salt("pkg.version", "openssh-server", local=True) assert ssh_version.startswith("1:9.2") diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index f2a2a896..8bf40cb5 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -219,11 +219,28 @@ def is_valid(self): name = self.name if self._has_systemd_suffix() else f"{self.name}.service" cmd = self.run("systemd-analyze verify %s", name) # A bad unit file still returns a rc of 0, so check the - # stdout for anything. Nothing means no warns/errors. + # stdout for anything. Nothing means no warns/errors. # Docs at https://www.freedesktop.org/software/systemd/man/systemd # -analyze.html#Examples%20for%20verify - assert (cmd.stdout, cmd.stderr) == ("", "") - return True + + # Ignore non-relevant messages from the output of "systemd-analyze + # verify": + # "Unit is bound to inactive unit" + # "ssh.service: Command 'man sshd(8)' failed with code" + # --man=no: suppress the man page existence check + # implemented in Systemd 235 (2017-10-06) + # "Suspicious symlink /etc/systemd/system/[...] treating as alias." + # probably a bug in systemd https://github.com/systemd/systemd/issues/30166 + stderr_lines = [ + i + for i in cmd.stderr.splitlines() + if "Unit is bound to inactive unit" not in i + and ": Command 'man" not in i + and "Suspicious symlink /" not in i + ] + + stderr = "".join(stderr_lines) + return (cmd.stdout, stderr) == ("", "") @property def is_masked(self): From 4a709d99d9a29587a9198789b575eccd8822157c Mon Sep 17 00:00:00 2001 From: Carsten Grohmann Date: Wed, 16 Apr 2025 20:53:05 +0200 Subject: [PATCH 3/3] Add "--" delimiter to support mount unit for / MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mount unit for the / filesystem is named -.mount. It is treated like an option because it begins with a “-”. It is fixed with the delimiter “--”, which is inserted before the unit. --- test/test_modules.py | 2 ++ testinfra/modules/service.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test/test_modules.py b/test/test_modules.py index 7d19663a..5fb8573e 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -147,6 +147,7 @@ def test_service_systemd_ssh(host, docker_image): assert ssh.is_running +@pytest.mark.testinfra_hosts("docker://rockylinux9") def test_service_systemd_root_mount(host): root = host.service("-.mount") # systemd unit for mounting / assert root.exists @@ -176,6 +177,7 @@ def test_service_systemd_root_mount_is_enabled(host): assert not root.is_enabled +@pytest.mark.testinfra_hosts("docker://rockylinux9") def test_service_systemd_tmp_mount(host): tmp = host.service("tmp.mount") assert tmp.exists diff --git a/testinfra/modules/service.py b/testinfra/modules/service.py index 8bf40cb5..651b66a8 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -169,7 +169,7 @@ class SystemdService(SysvService): def _has_systemd_suffix(self): """ - Check if service name has a known systemd unit suffix + Check if the service name has a known systemd unit suffix """ unit_suffix = self.name.split(".")[-1] return unit_suffix in self.suffix_list @@ -182,7 +182,7 @@ def exists(self): # 2: unused # 3: unit is not active # 4: no such unit - cmd = self.run_expect([0, 1, 3, 4], "systemctl status %s", self.name) + cmd = self.run_expect([0, 1, 3, 4], "systemctl status -- %s", self.name) return cmd.rc < 4 @property @@ -192,7 +192,7 @@ def is_running(self): # 1: program is dead and pid file exists # 3: not running and pid file does not exists # 4: Unable to determine status (no such unit) - out = self.run_expect([0, 1, 3, 4], "systemctl is-active %s", self.name) + out = self.run_expect([0, 1, 3, 4], "systemctl is-active -- %s", self.name) if out.rc == 1: # Failed to connect to bus: No such file or directory return super().is_running @@ -200,7 +200,7 @@ def is_running(self): @property def is_enabled(self): - cmd = self.run_test("systemctl is-enabled %s", self.name) + cmd = self.run_test("systemctl is-enabled -- %s", self.name) if cmd.rc == 0: return True if cmd.stdout.strip() == "disabled": @@ -217,7 +217,7 @@ def is_enabled(self): def is_valid(self): # systemd-analyze requires a full unit name. name = self.name if self._has_systemd_suffix() else f"{self.name}.service" - cmd = self.run("systemd-analyze verify %s", name) + cmd = self.run("systemd-analyze verify -- %s", name) # A bad unit file still returns a rc of 0, so check the # stdout for anything. Nothing means no warns/errors. # Docs at https://www.freedesktop.org/software/systemd/man/systemd @@ -244,12 +244,12 @@ def is_valid(self): @property def is_masked(self): - cmd = self.run_test("systemctl is-enabled %s", self.name) + cmd = self.run_test("systemctl is-enabled -- %s", self.name) return cmd.stdout.strip() == "masked" @functools.cached_property def systemd_properties(self): - out = self.check_output("systemctl show %s", self.name) + out = self.check_output("systemctl show -- %s", self.name) out_d = {} if out: # maxsplit is required because values can contain `=`