feat(init): decouple venv from install_dir; fail loud if no venv exists

The systemd unit templates hardcoded {{ install_dir }}/venv/bin/decnet.
On production hosts enroll_bootstrap.sh creates exactly that path so it
worked. On dev boxes where the operator runs `sudo decnet init` against
a source checkout with a differently-named venv (.venv, .311, .312),
every decnet-*.service looped forever in auto-restart with:

  Failed at step EXEC spawning .../venv/bin/decnet: No such file or
  directory

Templates now use {{ venv_dir }} as an independent Jinja2 var. `decnet
init` adds --venv-dir (explicit override), otherwise autodetects:

  1. $VIRTUAL_ENV (only when inside --install-dir, so a user-home venv
     never gets baked into a root-owned unit),
  2. {install_dir}/venv (production default; what enroll_bootstrap
     creates),
  3. {install_dir}/{.venv,.311,.312,.313} (common dev conventions).

Init aborts before any file writes if nothing resolves — an
operator-friendly error beats journalctl spam on every unit restart.

python3-venv doesn't set a persistent system variable — $VIRTUAL_ENV
lives in the activated shell only — so this has to be decided + baked
in at init time; there's no way for systemd to "inherit the current
venv" at unit start.

Test mode (--prefix) skips venv validation so the existing test suite
doesn't need to stub up a venv tree per case.
This commit is contained in:
2026-04-24 00:29:49 -04:00
parent cb692d570a
commit 51012eaa67
14 changed files with 103 additions and 17 deletions

View File

@@ -13,7 +13,7 @@ Group=decnet
SupplementaryGroups=docker
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet agent --host 0.0.0.0 --port 8765 --agent-dir /etc/decnet/agent
ExecStart={{ venv_dir }}/bin/decnet agent --host 0.0.0.0 --port 8765 --agent-dir /etc/decnet/agent
# MACVLAN/IPVLAN management + scapy raw sockets. Granted via ambient caps so
# the process starts unprivileged and keeps only these two bits.

View File

@@ -13,7 +13,7 @@ Group=decnet
SupplementaryGroups=docker
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet api
ExecStart={{ venv_dir }}/bin/decnet api
# MACVLAN/IPVLAN setup runs from the API lifespan when the embedded sniffer is on.
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW

View File

@@ -16,7 +16,7 @@ EnvironmentFile=-{{ install_dir }}/.env.local
# connect.
RuntimeDirectory=decnet
RuntimeDirectoryMode=0755
ExecStart={{ install_dir }}/venv/bin/decnet bus \
ExecStart={{ venv_dir }}/bin/decnet bus \
--socket /run/decnet/bus.sock \
--group decnet

View File

@@ -13,7 +13,7 @@ Group=decnet
SupplementaryGroups=docker
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet collect
ExecStart={{ venv_dir }}/bin/decnet collect
# No privileged network operations.
CapabilityBoundingSet=

View File

@@ -15,7 +15,7 @@ EnvironmentFile=-{{ install_dir }}/.env.local
# Replace <master-host> with the master's LAN address or hostname. The agent
# cert bundle at /etc/decnet/agent is reused — the forwarder presents the same
# worker identity when it connects to the master's listener.
ExecStart={{ install_dir }}/venv/bin/decnet forwarder \
ExecStart={{ venv_dir }}/bin/decnet forwarder \
--log-file /var/log/decnet/decnet.log \
--master-host ${DECNET_SWARM_MASTER_HOST} \
--master-port 6514 \

View File

@@ -12,7 +12,7 @@ WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
# Binds 0.0.0.0:6514 so workers across the LAN can connect. 6514 is not a
# privileged port (≥1024), so no CAP_NET_BIND_SERVICE is required.
ExecStart={{ install_dir }}/venv/bin/decnet listener \
ExecStart={{ venv_dir }}/bin/decnet listener \
--host 0.0.0.0 --port 6514 \
--ca-dir /etc/decnet/ca \
--log-path /var/log/decnet/master.log \

View File

@@ -13,7 +13,7 @@ Group=decnet
SupplementaryGroups=docker
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet mutate --watch
ExecStart={{ venv_dir }}/bin/decnet mutate --watch
CapabilityBoundingSet=
AmbientCapabilities=

View File

@@ -10,7 +10,7 @@ User=decnet
Group=decnet
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet probe
ExecStart={{ venv_dir }}/bin/decnet probe
# TCP connect probes only — no raw sockets required.
CapabilityBoundingSet=

View File

@@ -10,7 +10,7 @@ User=decnet
Group=decnet
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet profiler
ExecStart={{ venv_dir }}/bin/decnet profiler
CapabilityBoundingSet=
AmbientCapabilities=

View File

@@ -10,7 +10,7 @@ User=decnet
Group=decnet
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet sniffer
ExecStart={{ venv_dir }}/bin/decnet sniffer
# scapy needs raw packet access on the MACVLAN host interface.
CapabilityBoundingSet=CAP_NET_RAW

View File

@@ -12,7 +12,7 @@ WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
# Default bind is loopback — the controller is a master-local orchestrator
# reached by the CLI and the web dashboard, not by workers.
ExecStart={{ install_dir }}/venv/bin/decnet swarmctl --host 127.0.0.1 --port 8770
ExecStart={{ venv_dir }}/bin/decnet swarmctl --host 127.0.0.1 --port 8770
# No special capabilities — the controller issues mTLS certs and talks to
# workers over TCP on unprivileged ports.

View File

@@ -12,7 +12,7 @@ User=decnet
Group=decnet
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet updater \
ExecStart={{ venv_dir }}/bin/decnet updater \
--host 0.0.0.0 --port 8766 \
--updater-dir /etc/decnet/updater \
--install-dir {{ install_dir }} \

View File

@@ -10,7 +10,7 @@ User=decnet
Group=decnet
WorkingDirectory={{ install_dir }}
EnvironmentFile=-{{ install_dir }}/.env.local
ExecStart={{ install_dir }}/venv/bin/decnet web
ExecStart={{ venv_dir }}/bin/decnet web
# Uncomment if you bind the dashboard to a privileged port (80/443):
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE