diff --git a/test/test_modules.py b/test/test_modules.py index 1057c646..5fb8573e 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,55 @@ 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 + + +@pytest.mark.testinfra_hosts("docker://rockylinux9") +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 + + +@pytest.mark.testinfra_hosts("docker://rockylinux9") +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 24e666cb..651b66a8 100644 --- a/testinfra/modules/service.py +++ b/testinfra/modules/service.py @@ -169,24 +169,30 @@ 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 @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 # 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 @@ -194,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": @@ -211,22 +217,39 @@ 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. + # 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): - 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 `=`