1
Teardown and State
anti edited this page 2026-04-18 06:07:22 -04:00

Teardown and State

DECNET keeps the whole fleet picture in a single file, decnet-state.json, at the project root. Every command that touches a running deployment (decnet status, decnet teardown, the web dashboard, the sniffer, the collector) loads it; decnet deploy writes it.

Without this file, teardown cannot find the compose project, the sniffer cannot map IPs to deckies, and the collector does not know which containers to tail.

See also: Environment Variables, Database Drivers, Systemd.

Layout

decnet-state.json has exactly two top-level keys:

{
  "config": { ... DecnetConfig.model_dump() ... },
  "compose_path": "/absolute/path/to/decnet-compose.yml"
}
  • config — the serialised DecnetConfig pydantic model (decnet/models.py): mode, interface, subnet, gateway, ipvlan, mutate_interval, log_file, and the full deckies[] list. Each DeckyConfig entry carries name, IP, services, distro, base image, hostname, archetype, per-service config, nmap_os, and rotation timestamps.
  • compose_path — absolute path to the generated decnet-compose.yml. Teardown uses it as the -f argument to docker compose.

Example decnet-state.json

{
  "config": {
    "mode": "unihost",
    "interface": "eth0",
    "subnet": "192.168.1.0/24",
    "gateway": "192.168.1.1",
    "ipvlan": false,
    "mutate_interval": 30,
    "log_file": "/var/log/decnet/decnet.log",
    "deckies": [
      {
        "name": "decky-01",
        "ip": "192.168.1.201",
        "services": ["ssh", "smb"],
        "distro": "debian",
        "base_image": "debian:bookworm-slim",
        "build_base": "debian:bookworm-slim",
        "hostname": "fileserver-02",
        "archetype": "office-fileshare",
        "service_config": {},
        "nmap_os": "linux",
        "mutate_interval": null,
        "last_mutated": 0.0,
        "last_login_attempt": 0.0
      },
      {
        "name": "decky-02",
        "ip": "192.168.1.202",
        "services": ["rdp"],
        "distro": "ubuntu22",
        "base_image": "ubuntu:22.04",
        "build_base": "debian:bookworm-slim",
        "hostname": "WIN-DESK01",
        "archetype": null,
        "service_config": {},
        "nmap_os": "windows",
        "mutate_interval": null,
        "last_mutated": 0.0,
        "last_login_attempt": 0.0
      }
    ]
  },
  "compose_path": "/home/anti/Tools/DECNET/decnet-compose.yml"
}

API

All three helpers live in decnet/config.py:

save_state(config: DecnetConfig, compose_path: Path) -> None

Dumps {"config": config.model_dump(), "compose_path": str(compose_path)} as pretty-printed JSON (indent=2) to STATE_FILE (<project root>/decnet-state.json). Overwrites any existing file.

Called by decnet/engine/deployer.py::deploy after the compose file is written and before docker compose up.

load_state() -> tuple[DecnetConfig, Path] | None

Returns None when the file does not exist. Otherwise parses the JSON, re-hydrates DecnetConfig, and returns (config, Path(compose_path)).

Callers:

  • decnet/engine/deployer.pyteardown() and status().
  • decnet/sniffer/worker.py — builds the IP-to-decky-name map.
  • decnet/collector/worker.py — resolves the exact set of service container names to tail. Wrapped in asyncio.to_thread() to keep the event loop clean.
  • decnet/web/db/sqlmodel_repo.py — uses asyncio.to_thread(load_state) to surface deployment metadata through the dashboard API.

clear_state() -> None

unlink()s the state file if present. A no-op otherwise. Called once by teardown() after docker compose down and host-interface cleanup succeed.

How teardown cleans host interfaces

decnet/engine/deployer.py::teardown(decky_id=None) runs, in order:

  1. load_state(). If it returns None, prints No active deployment found (no decnet-state.json). and exits.
  2. If decky_id is given, docker compose stop <decky>-<svc>... then docker compose rm -f ... for that decky only. No host-interface cleanup and no state clear — the rest of the fleet is still alive.
  3. If no decky_id (full teardown):
    1. docker compose down with the compose_path from state.
    2. Compute the decky IP range with ips_to_range([d.ip for d in config.deckies]).
    3. Remove the host-side L2 interface:
      • teardown_host_ipvlan(decky_range) when config.ipvlan is true, or
      • teardown_host_macvlan(decky_range) otherwise.
    4. remove_macvlan_network(client) drops the docker network.
    5. clear_state() deletes decnet-state.json.
    6. Logs teardown complete and prints the driver that was removed.

If step 3 never runs (you ctrl-C'd, or one of the subprocess calls errored), decnet-state.json stays on disk and so do the host interfaces. Re-running sudo decnet teardown --all is idempotent and safe.

When you need sudo

Anything that touches host networking — creating or removing a MACVLAN / IPvlan parent interface, opening a raw socket for the sniffer — needs CAP_NET_ADMIN, which in practice means sudo:

  • sudo decnet deploy ... — creates the host interface, writes decnet-state.json, brings up the compose project.
  • sudo decnet teardown / sudo decnet teardown --all — removes host interfaces, clears state. Without sudo the ip-link calls fail and the state file is left behind.
  • sudo decnet teardown --id decky-01 — still needs sudo if the compose project was created by root.
  • sudo decnet sniffer --daemon — raw packet capture on the parent iface.

Read-only commands that only consult decnet-state.json and the dashboard DB do not need root:

  • decnet status
  • decnet services
  • decnet deploy --dry-run (generates the compose file only)
  • decnet api / decnet web once the deployment is up — as long as the state file and DECNET_SYSTEM_LOGS are readable by the invoking user. decnet/config.py drops root ownership of the system log when invoked via sudo precisely so the follow-up non-root commands can append to it.

Troubleshooting

  • No active deployment founddecnet-state.json is missing. Either the deploy never completed, or a previous teardown already ran.
  • Orphan host interfaces after a crash — re-run sudo decnet teardown --all. If state is gone, remove them manually with ip link del decnet-mv0 (or the ipvlan equivalent) and delete the docker network.
  • PermissionError writing the state file — you ran decnet deploy without sudo on a fresh checkout; the project root is not writable by the current user. Either chmod the directory or run as root.
  • Stale compose_path — moving the project directory after deploy breaks teardown. Tear down first, move, redeploy.