Add tests for some device & address variables
[tinc] / test / integration / systemd.py
diff --git a/test/integration/systemd.py b/test/integration/systemd.py
new file mode 100755 (executable)
index 0000000..ccf2f23
--- /dev/null
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+
+"""Test systemd integration."""
+
+import os
+import socket
+import tempfile
+import time
+
+from testlib import check, path
+from testlib.log import log
+from testlib.feature import Feature
+from testlib.const import MAXSOCKETS
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+
+def tincd_start_socket(foo: Tinc, pass_pid: bool) -> int:
+    """Start tincd as systemd socket activation does it."""
+
+    pid = os.fork()
+    if not pid:
+        env = {**os.environ, "LISTEN_FDS": str(MAXSOCKETS + 1)}
+        if pass_pid:
+            env["LISTEN_PID"] = str(os.getpid())
+        args = [
+            path.TINCD_PATH,
+            "-c",
+            foo.work_dir,
+            "--pidfile",
+            foo.pid_file,
+            "--logfile",
+            foo.sub("log"),
+        ]
+        assert not os.execve(path.TINCD_PATH, args, env)
+
+    assert pid > 0
+
+    _, status = os.waitpid(pid, 0)
+    assert os.WIFEXITED(status)
+    return os.WEXITSTATUS(status)
+
+
+def test_listen_fds(foo: Tinc) -> None:
+    """Test systemd socket activation."""
+
+    foo_log = foo.sub("log")
+
+    log.info("foreground tincd fails with too high LISTEN_FDS")
+    status = tincd_start_socket(foo, pass_pid=True)
+    check.failure(status)
+    check.in_file(foo_log, "Too many listening sockets")
+
+    foo.add_script(Script.TINC_UP)
+    foo.add_script(Script.TINC_DOWN)
+    os.remove(foo_log)
+
+    log.info("LISTEN_FDS is ignored without LISTEN_PID")
+    status = tincd_start_socket(foo, pass_pid=False)
+    foo[Script.TINC_UP].wait()
+    foo.cmd("stop")
+    foo[Script.TINC_DOWN].wait()
+    check.success(status)
+    check.not_in_file(foo_log, "Too many listening sockets")
+
+
+def recv_until(sock: socket.socket, want: bytes) -> None:
+    """Receive from a datagram socket until a specific value is found."""
+
+    while True:
+        msg = sock.recv(65000)
+        log.info("received %s", msg)
+        if msg == want:
+            break
+
+
+def test_watchdog(foo: Tinc) -> None:
+    """Test systemd watchdog."""
+
+    address = tempfile.mktemp()
+    foo_log = foo.sub("log")
+
+    log.info("watchdog is disabled if no env vars are passed")
+    foo.cmd("start", "--logfile", foo_log)
+    foo.cmd("stop")
+    check.in_file(foo_log, "Watchdog is disabled")
+
+    log.info("watchdog is enabled by systemd env vars")
+    foo.add_script(Script.TINC_UP)
+    foo.add_script(Script.TINC_DOWN)
+
+    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
+        sock.bind(address)
+
+        watchdog = 0.5
+        env = {"NOTIFY_SOCKET": address, "WATCHDOG_USEC": str(int(watchdog * 1e6))}
+        proc = foo.tincd("-D", env=env)
+        recv_until(sock, b"READY=1")
+
+        for _ in range(6):
+            before = time.monotonic()
+            recv_until(sock, b"WATCHDOG=1")
+            spent = time.monotonic() - before
+            assert spent < watchdog
+
+        foo.cmd("stop")
+        recv_until(sock, b"STOPPING=1")
+
+        check.success(proc.wait())
+
+
+with Test("socket activation") as context:
+    test_listen_fds(context.node(init=True))
+
+with Test("watchdog") as context:
+    node = context.node(init=True)
+    if Feature.WATCHDOG in node.features:
+        test_watchdog(node)