feat(swarm): unbundle master-only code from agent tarball + sync systemd units on update

Agents now ship with collector/prober/sniffer as systemd services; mutator,
profiler, web, and API stay master-only (profiler rebuilds attacker profiles
against the master DB — no per-host DB exists). Expand _EXCLUDES to drop the
full decnet/web, decnet/mutator, decnet/profiler, and decnet_web trees from
the enrollment bundle.

Updater now calls _heal_path_symlink + _sync_systemd_units after rotation so
fleets pick up new unit files and /usr/local/bin/decnet tracks the shared venv
without a manual reinstall. daemon-reload runs once per update when any unit
changed.

Fix _service_registry matchers to accept systemd-style /usr/local/bin/decnet
cmdlines (psutil returns a list — join to string before substring-checking)
so agent-mode `decnet status` reports collector/prober/sniffer correctly.
This commit is contained in:
2026-04-19 19:19:17 -04:00
parent d2cf1e8b3a
commit 2bef3edb72
8 changed files with 56 additions and 169 deletions

View File

@@ -185,9 +185,10 @@ async def test_systemd_units_shipped_and_installed(client, auth_token):
assert "etc/systemd/system/decnet-forwarder.service" in names
assert "etc/systemd/system/decnet-engine.service" in names
# Per-host microservices get their own systemd units now.
for unit in ("decnet-collector", "decnet-prober",
"decnet-profiler", "decnet-sniffer"):
# Profiler is master-only (uses the master DB) and must NOT ship.
for unit in ("decnet-collector", "decnet-prober", "decnet-sniffer"):
assert f"etc/systemd/system/{unit}.service" in names, unit
assert "etc/systemd/system/decnet-profiler.service" not in names
fwd = tf.extractfile("etc/systemd/system/decnet-forwarder.service").read().decode()
assert "--master-host 10.9.8.7" in fwd
@@ -206,7 +207,7 @@ async def test_systemd_units_shipped_and_installed(client, auth_token):
for unit in (
"decnet-agent.service", "decnet-forwarder.service",
"decnet-collector.service", "decnet-prober.service",
"decnet-profiler.service", "decnet-sniffer.service",
"decnet-sniffer.service",
):
assert unit in sh, unit
assert "decnet-updater.service" in sh
@@ -307,18 +308,13 @@ async def test_get_tgz_contents(client, auth_token, tmp_path):
assert not bad.endswith(".env"), f"leaked env file: {bad}"
assert ".env.local" not in bad, f"leaked env file: {bad}"
assert ".env.example" not in bad, f"leaked env file: {bad}"
# Master-only trees: agents don't run the FastAPI master app or the
# React frontend, so shipping them bloats the tarball and widens the
# worker's attack surface for no benefit. decnet/web/db and
# decnet/web/dependencies.py DO ship — the profiler microservice on
# the agent needs the repo singleton.
# Master-only trees: agents don't run the FastAPI master app, the
# React frontend, the mutator (swarm-wide respawn scheduler), or
# the profiler (rebuilds profiles against the master DB).
assert not bad.startswith("decnet_web/"), f"leaked frontend: {bad}"
assert bad != "decnet/web/api.py", f"leaked master API: {bad}"
assert bad != "decnet/web/swarm_api.py", f"leaked swarm API: {bad}"
assert bad != "decnet/web/ingester.py", f"leaked ingester: {bad}"
assert not bad.startswith("decnet/web/router/"), f"leaked router: {bad}"
assert not bad.startswith("decnet/web/templates/"), f"leaked tpl: {bad}"
assert not bad.startswith("decnet/web/"), f"leaked master-api: {bad}"
assert not bad.startswith("decnet/mutator/"), f"leaked mutator: {bad}"
assert not bad.startswith("decnet/profiler/"), f"leaked profiler: {bad}"
# INI content is correct
ini = tf.extractfile("etc/decnet/decnet.ini").read().decode()