feat(deploy): systemd units w/ capability-based hardening; updater restarts agent via systemctl

Add deploy/ unit files for every DECNET daemon (agent, updater, api, web,
swarmctl, listener, forwarder). All run as User=decnet with NoNewPrivileges,
ProtectSystem, PrivateTmp, LockPersonality; AmbientCapabilities=CAP_NET_ADMIN
CAP_NET_RAW only on the agent (MACVLAN/scapy). Existing api/web units migrated
to /opt/decnet layout and the same hardening stanza.

Make the updater's _spawn_agent systemd-aware: under systemd (detected via
INVOCATION_ID + systemctl on PATH), `systemctl restart decnet-agent.service`
replaces the Popen path so the new agent inherits the unit's ambient caps
instead of the updater's empty set. _stop_agent becomes a no-op in that mode
to avoid racing systemctl's own stop phase.

Tests cover the dispatcher branch selection, MainPID parsing, and the
systemd no-op stop.
This commit is contained in:
2026-04-19 00:44:06 -04:00
parent 40d3e86e55
commit f5a5fec607
9 changed files with 381 additions and 19 deletions

View File

@@ -306,6 +306,7 @@ def test_stop_agent_falls_back_to_proc_scan_when_no_pidfile(
killed.append((pid, sig))
raise ProcessLookupError # pretend it already died after SIGTERM
monkeypatch.setattr(ex, "_systemd_available", lambda: False)
monkeypatch.setattr(ex, "_discover_agent_pids", lambda: [4242, 4243])
monkeypatch.setattr(ex.os, "kill", fake_kill)
@@ -315,3 +316,76 @@ def test_stop_agent_falls_back_to_proc_scan_when_no_pidfile(
import signal as _signal
assert (4242, _signal.SIGTERM) in killed
assert (4243, _signal.SIGTERM) in killed
def test_systemd_available_requires_invocation_id_and_systemctl(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Both INVOCATION_ID and a resolvable systemctl are needed."""
monkeypatch.delenv("INVOCATION_ID", raising=False)
assert ex._systemd_available() is False
monkeypatch.setenv("INVOCATION_ID", "abc")
monkeypatch.setattr("shutil.which", lambda _: None)
assert ex._systemd_available() is False
monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/systemctl")
assert ex._systemd_available() is True
def test_spawn_agent_dispatches_to_systemd_when_available(
monkeypatch: pytest.MonkeyPatch,
install_dir: pathlib.Path,
) -> None:
monkeypatch.setattr(ex, "_systemd_available", lambda: True)
called: list[pathlib.Path] = []
monkeypatch.setattr(ex, "_spawn_agent_via_systemd", lambda d: called.append(d) or 999)
monkeypatch.setattr(ex, "_spawn_agent_via_popen", lambda d: pytest.fail("popen path taken"))
assert ex._spawn_agent(install_dir) == 999
assert called == [install_dir]
def test_spawn_agent_dispatches_to_popen_when_not_systemd(
monkeypatch: pytest.MonkeyPatch,
install_dir: pathlib.Path,
) -> None:
monkeypatch.setattr(ex, "_systemd_available", lambda: False)
monkeypatch.setattr(ex, "_spawn_agent_via_systemd", lambda d: pytest.fail("systemd path taken"))
monkeypatch.setattr(ex, "_spawn_agent_via_popen", lambda d: 777)
assert ex._spawn_agent(install_dir) == 777
def test_stop_agent_is_noop_under_systemd(
monkeypatch: pytest.MonkeyPatch,
install_dir: pathlib.Path,
) -> None:
"""Under systemd, stop is skipped — systemctl restart handles it atomically."""
monkeypatch.setattr(ex, "_systemd_available", lambda: True)
monkeypatch.setattr(ex, "_discover_agent_pids", lambda: pytest.fail("scanned /proc"))
monkeypatch.setattr(ex.os, "kill", lambda *a, **k: pytest.fail("sent signal"))
(install_dir / "agent.pid").write_text("12345")
ex._stop_agent(install_dir, grace=0.0) # must not raise
def test_spawn_agent_via_systemd_records_main_pid(
monkeypatch: pytest.MonkeyPatch,
install_dir: pathlib.Path,
) -> None:
calls: list[list[str]] = []
class _Out:
def __init__(self, stdout: str = "") -> None:
self.stdout = stdout
def fake_run(cmd, **kwargs): # type: ignore[no-untyped-def]
calls.append(cmd)
if "show" in cmd:
return _Out("4711\n")
return _Out("")
monkeypatch.setattr(ex.subprocess, "run", fake_run)
pid = ex._spawn_agent_via_systemd(install_dir)
assert pid == 4711
assert (install_dir / "agent.pid").read_text() == "4711"
assert calls[0][:2] == ["systemctl", "restart"]
assert calls[1][:2] == ["systemctl", "show"]