Files
dashboards/tests/test_routectl.py
T
ducoterra 86c8bf4992 phase: 33_gateway_route_switching
Phase 33 completion pass is done. Final report:

## Phase 33 — completion report

**All 8 task files complete** (`.agents/phases/complete/33_gateway_route_switching/`), full gate green, docs verified.

### Work in this pass

1. **`tests/test_routectl.py` added** (17 tests, all green) — exercises the *real* helper binary unprivileged against a fake tree (`--root`), the real agent parser, the captured gateway fixtures, and stubbed `nginx`/`haproxy`/`systemctl` on `PATH`. Pins the full protocol (locate → state → dry-run → temp-file/fsync/`os.replace` write → checker → restore on checker failure → reload-never-restart → re-read proof → sidecar record), all three route shapes (haproxy `server`, nginx `proxy_pass`, nginx stream `map` via its sink-value id), `drift` / unknown-id / `map-default` refusals, `list`/`verify`, and the `--serve` peer gate (`SO_PEERCRED` uid check, one JSON line per request, exact body shape).

2. **Real helper defect fixed** — `deploy/dashboards-routectl.py`: `main()`'s early-return paths returned `({"ok": False, …}, EXIT_*)` tuples, so `sys.exit()` printed a Python repr instead of a JSON line (usage errors, agent-import refusal). Now prints `json.dumps(…)` and returns only the code, keeping the one-JSON-line contract.

3. **Documentation consistency fixes** (task 08 criterion 6):
   - `README.md`: garbled verdict-colour sentence on the switch ("green and to the position") → "in the **on** position and tinted `--gate-on`" (the existence-pair contract).
   - `README.md`: audit sentence now names all three event kinds — `route-disabled` / `route-enabled` / **`route-denied`** (the code stores one per refused call; dry runs store none).
   - `page-gateway.js`: replaced the stale pre-33 module docstring ("the switch is not wired") with the two-arm `GATEWAY_ALLOW_WRITES` contract.
   - `AGENTS.md`: every phase-33 claim re-verified against code (the ten gates, the 35 s/30 s budget pair, `gateway.writes_enabled`, `/api/summary`'s `disabled`, the 1.4.0 agent bullet, the gap list) — no changes needed.

### Verification
- `uv run pytest --cov`: **1970 passed**, 18 skipped — coverage **98.74%** (floor 94%)
- `ruff` clean · `pyright` 0 errors · `bash .agents/validate.sh` **exit 0** (incl. `node --check` on every page script)
- Dev server running; all 10 sources `ok`; bundles exported

### Live infrastructure
**No live changes made.** The live drill remains unexecuted per `data/logs/NOTES-33-switching.md` (no operator approval; checker binaries unavailable in the podman proxy units). All routes stay enabled, config hashes unchanged, and the sandbox rehearsal + backup tree are in place for the operator's own `DANGER` call. All changes are in the working tree only.
2026-11-01 11:50:51 -05:00

626 lines
31 KiB
Python

"""The route-switch root helper, tested unprivileged against a fake tree.
`deploy/dashboards-routectl.py` is the phase 33 writer: the dashboard reaches a
gateway config file only through it (through the agent's `POST /route`, through the
systemd-activated socket). Its safety property is the write protocol's order — locate,
state, edit, validate, reload, prove, record — and the refusals that end it before a
byte moves. So the tests run the **real helper** (`--root` pointed at a fake tree),
against the **captured** gateway configs (`tests/fixtures/gateway_*.cfg|.conf`, the
fleet's real ingress bytes, never a toy file), with `nginx` / `haproxy` / `systemctl`
faked on `PATH` as scripts that record their argv and fail on order. The assertions
are on the bytes: one line changed and nothing else, a byte-identical restore, a
refusal that wrote nothing, the state sidecar's mode at birth, the sink's header and
its removal rule, and the socket's peer gate. The helper's own `--serve` is driven the
way the unit does (the connected socket on stdin), and the structural pins (no
`shell=True`, no path from the wire, the one `systemctl` verb, the Python 3.10 floor)
are greps over the file, the `auth.py`/`certs.py` precedent.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import importlib.util
import json
import os
import pwd
import re
import socket
import stat
import subprocess
import sys
import types
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
DEPLOY = ROOT / "deploy"
FIXTURES = ROOT / "tests" / "fixtures"
HELPER = DEPLOY / "dashboards-routectl.py"
AGENT = ROOT / "agent" / "host_agent.py"
#: The captured gateway tree, the same mapping `tests/conftest.py` lays out — the
#: helper's `--root` reads the file the real ingress runs.
GATEWAY_TREE = {
"etc/haproxy/haproxy.cfg": "gateway_haproxy.cfg",
"etc/nginx/nginx.conf": "gateway_nginx_main.conf",
"etc/nginx/conf.d/http.conf": "gateway_nginx_conf_d_http.conf",
"etc/nginx/stream.d/stream.conf": "gateway_nginx_stream_d_stream.conf",
}
#: The ids the dashboard publishes for these lines (phase 30's vocabulary), and the
#: two refusals that are not ids at all.
HAPROXY_ROUTE = "haproxy:5201->web.reeselink.com:5201" # the iperf3 server line
VHOST_ROUTE = "nginx:4443->nextcloud.reeselink.com:11000" # the 4443 vhost's proxy_pass
MAP_ROUTE = "nginx:443->reeseapps-brain-of-reese.reeselink.com:443" # brain.reeseapps.com
MAP_SINK_ID = "nginx:443->127.0.0.1:44450" # while off, the map entry is addressed by its sink value
IMMICH_ROUTE = "nginx:443->reeseapps-immich.reeselink.com:443"
MAP_DEFAULT = "nginx:443->127.0.0.1:4443" # the map's default: a fallback, not a service
SINK_HEADER = "# managed by dashboards-routectl — do not edit"
IMMICH_ORIGINAL = " immich.reeseapps.com reeseapps-immich.reeselink.com:443;"
def _tree(tmp_path: Path) -> Path:
"""The captured configs, laid into a fake `--root` tree byte for byte."""
root = tmp_path / "root"
for relative, fixture in GATEWAY_TREE.items():
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes((FIXTURES / fixture).read_bytes())
return root
def _stubs(tmp_path: Path) -> tuple[Path, Path]:
"""Fake `nginx` / `haproxy` / `systemctl` on `PATH`: they record their argv in
`$FAKE_CALLS_LOG` and fail on order — `FAKE_CHECK_FAIL` makes the checker exit 1
with its own first line, `FAKE_RELOAD_FAIL` makes `reload` fail and `status`
print the unit's line."""
stubs = tmp_path / "stubs"
stubs.mkdir()
log = tmp_path / "calls.log"
for name in ("nginx", "haproxy", "systemctl"):
script = stubs / name
script.write_text(
"#!/bin/sh\n"
f'printf "%s %s\\n" "{name}" "$*" >> "$FAKE_CALLS_LOG"\n'
"case \"$1\" in\n"
" -t|-c)\n"
" if [ -n \"$FAKE_CHECK_FAIL\" ]; then\n"
f' echo "fake {name}: configuration file test failed"\n'
" exit 1\n"
" fi\n"
" ;;\n"
" reload)\n"
" if [ -n \"$FAKE_RELOAD_FAIL\" ]; then exit 1; fi\n"
" ;;\n"
" status)\n"
' if [ -n "$FAKE_RELOAD_FAIL" ]; then echo "fake unit: reload failed"; fi\n'
" ;;\n"
"esac\n"
"exit 0\n",
encoding="utf-8")
script.chmod(0o755)
return stubs, log
def _run(tmp_path: Path, stubs: Path, log: Path, args: list[str], *,
check_fail: bool = False, reload_fail: bool = False,
agent_module: str | None = None
) -> tuple[subprocess.CompletedProcess[str], dict, list[str]]:
"""One helper invocation the way the operator runs it by hand: the real binary,
the fake tree, the fake proxy commands. Returns the process, the one JSON answer
line, and the recorded calls so far (the log is appended across invocations)."""
argv = [sys.executable, str(HELPER),
"--root", str(tmp_path / "root"),
"--agent-module", str(AGENT) if agent_module is None else agent_module,
"--state", str(tmp_path / "state.json"),
"--sink-conf", "/etc/nginx/stream.d/00-dashboards-sink.conf",
"--sink-address", "127.0.0.1:44450",
"--nginx-unit", "nginx", "--haproxy-unit", "haproxy", *args]
env = {
"PATH": f"{stubs}{os.pathsep}{os.environ.get('PATH', '')}",
"FAKE_CALLS_LOG": str(log),
"FAKE_CHECK_FAIL": "1" if check_fail else "",
"FAKE_RELOAD_FAIL": "1" if reload_fail else "",
"HOME": str(tmp_path),
}
proc = subprocess.run(argv, capture_output=True, text=True, env=env, timeout=120)
lines = [line for line in proc.stdout.splitlines() if line.strip()]
answer: dict = json.loads(lines[-1]) if lines else {}
calls = log.read_text().splitlines() if log.exists() else []
return proc, answer, calls
def _serve(tmp_path: Path, tree: Path, allow_uid: str, request: bytes) -> tuple[int, str]:
"""`--serve` the way the unit does: the connected socket on fd 0/fd 1, one
request, one answer line. (The `tests/test_deploy_scripts.py` transport pin,
re-used for the wire-body gates the overview demands in this file.)"""
child, parent = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
proc = subprocess.Popen(
[sys.executable, str(HELPER), "--serve",
"--allow-uid", allow_uid,
"--agent-module", str(AGENT),
"--root", str(tree),
"--state", str(tmp_path / "serve-state.json"),
"--sink-conf", "/etc/nginx/stream.d/00-dashboards-sink.conf",
"--sink-address", "127.0.0.1:44450",
"--nginx-unit", "nginx", "--haproxy-unit", "haproxy"],
stdin=child.fileno(), stdout=child.fileno(),
stderr=subprocess.PIPE, close_fds=True)
parent.sendall(request)
answer = parent.recv(65536).decode("utf-8").strip()
parent.close()
err = proc.stderr.read().decode() if proc.stderr is not None else ""
proc.wait(timeout=60)
child.close()
if proc.returncode not in (0, 1):
raise AssertionError(
f"the helper exited {proc.returncode} instead of answering: {answer!r} {err}")
return proc.returncode, answer
def _load(name: str, path: Path) -> types.ModuleType:
"""Import a stdlib module by file path, the way the helper imports the agent."""
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None, f"no loader for {path}"
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _snapshot(tree: Path) -> dict[Path, bytes]:
return {path: path.read_bytes() for path in sorted(tree.rglob("*")) if path.is_file()}
def _sink(tree: Path) -> Path:
return tree / "etc/nginx/stream.d/00-dashboards-sink.conf"
@pytest.fixture
def tree(tmp_path: Path) -> Path:
return _tree(tmp_path)
@pytest.fixture
def stubs(tmp_path: Path) -> tuple[Path, Path]:
return _stubs(tmp_path)
def test_disabling_one_route_changes_exactly_one_line(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
"""The whole write, against the fakes: one hunk, one line, every other byte
identical — asserted on the bytes, not on a count. The reload is the one verb
the file executes, and the state sidecar names the write."""
stubs_dir, log = stubs
hap = tree / "etc/haproxy/haproxy.cfg"
before_files = _snapshot(tree)
before = hap.read_bytes()
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer == {"ok": True, "applied": True, "validated": True,
"reloaded": True, "status": "", "state": "off"}, answer
after = hap.read_bytes()
b, a = before.decode("utf-8").splitlines(), after.decode("utf-8").splitlines()
assert len(b) == len(a), "the edit is in place: one line, never an add or a remove"
changed = [i for i in range(len(b)) if b[i] != a[i]]
assert len(changed) == 1, f"exactly one line changed byte for byte, nothing else: {changed}"
index = changed[0]
assert b[index] == " server iperf3 web.reeselink.com:5201 check inter 5s"
assert a[index] == b[index] + " #off iperf3", (
"the marker is the whole trailing comment; the original stands verbatim before it")
for path, blob in before_files.items():
if path != hap:
assert path.read_bytes() == blob, f"{path} moved when one haproxy line was marked"
assert "systemctl reload haproxy" in calls, "the one verb, for the one unit"
assert "systemctl reload nginx" not in calls
assert not [c for c in calls if "status" in c], (
f"a successful reload never asks the unit for its status: {calls}")
state = json.loads((tmp_path / "state.json").read_text(encoding="utf-8"))
assert set(state) == {HAPROXY_ROUTE}
row = state[HAPROXY_ROUTE]
assert row["action"] == "disable"
assert row["file"] == "/etc/haproxy/haproxy.cfg"
assert row["written"] == b[index] + " #off iperf3"
assert row["original"] == b[index]
assert row["sha256"] == hashlib.sha256(after).hexdigest()
def test_the_vhost_arm_comments_the_proxy_pass_line_and_restores(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""The second arm: the vhost's `proxy_pass` is commented the same way, the
sibling vhost on another port is untouched, and the re-enable is byte-identical."""
stubs_dir, log = stubs
http = tree / "etc/nginx/conf.d/http.conf"
before = http.read_bytes()
proc, answer, _calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", VHOST_ROUTE, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer["state"] == "off"
lines = http.read_text().splitlines()
assert " proxy_pass http://nextcloud.reeselink.com:11000; #off nextcloud-11000" in lines
assert " proxy_pass http://nextcloud.reeselink.com:11001;" in lines, (
"the 8443 vhost's own line is a different fact; it does not move")
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", VHOST_ROUTE, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer["state"] == "on"
assert http.read_bytes() == before, "the re-enable restores the file byte for byte"
assert "systemctl reload nginx" in calls
assert "systemctl reload haproxy" not in calls
def test_a_disabled_map_entry_points_at_the_sink_and_keeps_its_original(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""The third arm: the map entry is **rewritten to the sink** (commenting it would
fall through to the map's `default` and mis-route the domain), the sink file is
created on first need with its header, and the re-enable — addressed by the sink
value the entry carries while off — restores the file byte-identical and removes
the sink the helper wrote. A `dry_run` writes nothing at all, not even the sink."""
stubs_dir, log = stubs
stream = tree / "etc/nginx/stream.d/stream.conf"
sink = _sink(tree)
original_line = " brain.reeseapps.com reeseapps-brain-of-reese.reeselink.com:443;"
before = stream.read_bytes()
# the dry run: the unified diff and nothing else — no file, no checker, no reload
proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_ROUTE])
assert proc.returncode == 0, proc.stderr
assert answer["ok"] is True and answer["applied"] is False
assert answer["changed"] is True and answer["dry_run"] is True
assert f"-{original_line}" in answer["diff"]
assert "+ brain.reeseapps.com 127.0.0.1:44450; #off brain.reeseapps.com" in answer["diff"]
assert stream.read_bytes() == before, "a dry run writes nothing"
assert not sink.exists(), "a dry run writes no file at all (not even the sink's)"
assert calls == [], f"a dry run neither validates nor reloads: {calls}"
assert not (tmp_path / "state.json").exists()
proc, answer, _calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", MAP_ROUTE, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer["state"] == "off"
assert sink.is_file(), "the sink is created on first need, before the checker runs"
sink_text = sink.read_text(encoding="utf-8")
assert sink_text.splitlines()[0] == SINK_HEADER
assert "listen 127.0.0.1:44450;" in sink_text
assert "proxy_pass" not in sink_text, "the block, not a vhost, is what the page's `sink_endpoint` recognises"
lines = stream.read_text().splitlines()
assert " brain.reeseapps.com 127.0.0.1:44450; #off brain.reeseapps.com" in lines
assert original_line not in lines
# the enable is addressed by the sink value the entry carries while off
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", MAP_SINK_ID, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer["state"] == "on"
assert stream.read_bytes() == before, "the re-enable restores the file byte-identical to the pre-drill capture"
assert not sink.exists(), "the helper removes the sink it wrote once nothing points at it"
assert json.loads((tmp_path / "state.json").read_text(encoding="utf-8")) == {}
assert "systemctl reload nginx" in calls
def test_the_map_default_is_refused_and_nothing_is_written(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""The map's `default` is the fallback for every unmapped SNI, not a service:
the id that addresses it is refused at the wire, before a byte moves or a
checker runs."""
stubs_dir, log = stubs
before = _snapshot(tree)
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", MAP_DEFAULT, "--apply"])
assert proc.returncode == 1
assert answer == {"ok": False, "applied": False, "refused": "map-default"}
assert _snapshot(tree) == before, "a refused write writes nothing"
assert calls == [], f"a wire-level refusal reaches neither the checker nor the unit: {calls}"
def test_an_unknown_id_is_refused(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
stubs_dir, log = stubs
before = _snapshot(tree)
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", "haproxy:9999->10.0.0.9:9999", "--apply"])
assert proc.returncode == 1
assert answer == {"ok": False, "detail": "not-found"}
assert _snapshot(tree) == before
assert calls == []
def test_disabling_a_disabled_route_changes_nothing_and_reloads_nothing(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""Step 2: a route that is already off is an answer, not a write — no file, no
checker, no reload. The fake `systemctl` log stays empty on the second call."""
stubs_dir, log = stubs
hap = tree / "etc/haproxy/haproxy.cfg"
_proc, _answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"])
after_first = hap.read_bytes()
seen = len(calls)
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer == {"ok": True, "applied": False, "changed": False,
"id": HAPROXY_ROUTE, "state": "off"}
assert hap.read_bytes() == after_first
assert len(calls) == seen, f"an already-off route changes nothing and reloads nothing: {calls[seen:]}"
def test_a_failing_validator_restores_the_previous_bytes(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
"""Step 5: the checker says no, the previous bytes go back byte-identical,
nothing reloads, and no state is recorded — the answer carries the checker's
own first line and the non-zero exit says it out."""
stubs_dir, log = stubs
hap = tree / "etc/haproxy/haproxy.cfg"
before = hap.read_bytes()
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"],
check_fail=True)
assert proc.returncode == 1
assert answer["ok"] is False and answer["applied"] is False
assert answer["validated"] is False and answer["restored"] is True
assert answer["detail"] == "fake haproxy: configuration file test failed"
assert hap.read_bytes() == before, "the restore is byte-identical"
assert [c for c in calls if c.startswith("haproxy -c")] == [
f"haproxy -c -f {tmp_path / 'root' / 'etc/haproxy/haproxy.cfg'}"], calls
assert not [c for c in calls if c.startswith("systemctl")], (
f"a failed validator reloads nothing: {calls}")
assert not (tmp_path / "state.json").exists(), "a failed write records no state"
def test_a_failing_reload_says_so_and_leaves_the_file_applied(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""Step 6: the config is correct and the reload is not — the file stays
applied, the answer says so with the unit's own status line, and the state is
still recorded (the write happened; the reload is the operator's next action)."""
stubs_dir, log = stubs
hap = tree / "etc/haproxy/haproxy.cfg"
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"],
reload_fail=True)
assert proc.returncode == 0, "a reload failure is an answered call, not a crash"
assert answer["ok"] is True and answer["applied"] is True
assert answer["validated"] is True and answer["reloaded"] is False
assert answer["status"] == "fake unit: reload failed"
assert " #off iperf3" in hap.read_text(), "the applied file stays applied"
assert "systemctl reload haproxy" in calls
assert "systemctl status haproxy" in calls, "the answer carries the unit's own line"
state = json.loads((tmp_path / "state.json").read_text(encoding="utf-8"))
assert HAPROXY_ROUTE in state, "the write happened; the state says so"
def test_reload_is_never_a_restart() -> None:
"""The only verb this file executes is `systemctl reload <unit>`: a restart
severs every live connection through the fleet's only ingress. Pinned at the
call position (the docstring's negation is backticked, never an argv)."""
src = HELPER.read_text(encoding="utf-8")
verbs = re.findall(r'\["systemctl", "(\w+)"', src)
assert verbs == ["reload", "status"], f"the only systemctl verbs: {verbs}"
assert not re.search(r'["\']restart["\']', src), (
"no quoted `restart` anywhere: the docstring refuses it in words, the argv never carries it")
def test_the_helper_imports_the_agents_parser_and_refuses_without_it(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""The parser is the agent's, imported by file path: a helper that cannot
import it refuses to run at all, names the path, and writes nothing — two
parsers reading one config are two opinions about the same file."""
stubs_dir, log = stubs
before = _snapshot(tree)
missing = tmp_path / "no-such-agent.py"
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"],
agent_module=str(missing))
assert proc.returncode == 1
assert answer == {"ok": False,
"detail": f"cannot import the agent module {missing}: not a file"}
assert _snapshot(tree) == before
assert calls == []
def test_state_is_0600_and_drift_is_refused(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
"""The sidecar's mode is its birth mode, and `enable` restores only what this
helper wrote: a hand-edited line is `drift` — a refusal with no byte change,
never a guess. The other arm is a line this helper never wrote (no state row)."""
stubs_dir, log = stubs
hap = tree / "etc/haproxy/haproxy.cfg"
_proc, _answer, _calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", HAPROXY_ROUTE, "--apply"])
state_path = tmp_path / "state.json"
assert stat.S_IMODE(state_path.stat().st_mode) == 0o600, "the mode is the birth mode"
off_line = " server iperf3 web.reeselink.com:5201 check inter 5s #off iperf3"
assert off_line in hap.read_text(encoding="utf-8")
edited = hap.read_text(encoding="utf-8").replace(
off_line, " server iperf3 web.reeselink.com:5201 check inter 5s backup")
hap.write_text(edited, encoding="utf-8")
seen = len(_calls)
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", HAPROXY_ROUTE, "--apply"])
assert proc.returncode == 1
assert answer["ok"] is False and answer["applied"] is False and answer["drift"] is True
assert answer["detail"] == "the line was edited after the helper wrote it"
assert hap.read_text(encoding="utf-8") == edited, "drift is refused, not guessed at: no byte moves"
assert len(calls) == seen, f"a drift refusal touches neither the checker nor the unit: {calls[seen:]}"
# the other arm: a line this helper never wrote (no state row at all)
(tmp_path / "state.json").unlink()
vhost = tree / "etc/nginx/conf.d/http.conf"
before_vhost = vhost.read_bytes()
proc, answer, _calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", VHOST_ROUTE, "--apply"])
assert proc.returncode == 1
assert answer["ok"] is False and answer["drift"] is True
assert answer["detail"] == "no state row: this helper did not write the line"
assert vhost.read_bytes() == before_vhost
def test_serve_refuses_a_peer_that_is_not_the_agent_uid(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
"""The second look, in the fake-peer test: a connected caller whose uid is not
`--allow-uid` is refused before the body is read — the gate never opens for a
caller it was not given."""
if hasattr(os, "geteuid") and os.geteuid() == 0:
pytest.skip("running as root: the foreign-uid arm needs a foreign uid")
code, answer = _serve(tmp_path, tree, "root",
(json.dumps({"id": HAPROXY_ROUTE, "action": "show"}) + "\n").encode())
assert code == 1
body = json.loads(answer)
assert body["ok"] is False
# the refusal names the caller's own uid and the user it was not: the gate
# opened for no one, and the dialer gets the reason, not a timeout
assert body["detail"] == f"peer: uid {os.geteuid()} is not root", (
f"the refusal names the uid and the user: {body}")
def test_serve_accepts_exactly_id_action_dry_run(tree: Path, stubs: tuple[Path, Path],
tmp_path: Path) -> None:
"""The wire's gate, from the accepted side: a named peer's `show` is answered,
a wider body is refused with the wire's own sentence, and a `dry_run` write
answers the diff without writing anything (not even the sink's file)."""
if hasattr(os, "geteuid") and os.geteuid() == 0:
pytest.skip("running as root: the accept arm needs the caller to be the allowed uid")
me = pwd.getpwuid(os.geteuid()).pw_name
code, answer = _serve(tmp_path, tree, me,
(json.dumps({"id": HAPROXY_ROUTE, "action": "show"}) + "\n").encode())
assert code == 0
body = json.loads(answer)
assert body == {"ok": True, "id": HAPROXY_ROUTE, "state": "on",
"file": "/etc/haproxy/haproxy.cfg"}, body
code, answer = _serve(tmp_path, tree, me,
(json.dumps({"id": HAPROXY_ROUTE, "action": "show",
"command": "reboot"}) + "\n").encode())
assert code == 1
body = json.loads(answer)
assert body == {"ok": False, "detail": "body: exactly {id, action, dry_run}"}
hap = tree / "etc/haproxy/haproxy.cfg"
before = hap.read_bytes()
code, answer = _serve(tmp_path, tree, me,
(json.dumps({"id": HAPROXY_ROUTE, "action": "disable",
"dry_run": True}) + "\n").encode())
assert code == 0
body = json.loads(answer)
assert body["ok"] is True and body["applied"] is False and body["dry_run"] is True
assert "diff" in body
assert hap.read_bytes() == before, "a dry run over the wire writes nothing"
assert not _sink(tree).exists()
def test_no_shell_and_no_path_from_the_wire() -> None:
"""The structural pins (the `auth.py`/`certs.py` precedent): every subprocess
call is a fixed argv list — no `shell=True` — and the wire takes an id, never a
path, a line number or a pattern; `--serve` accepts exactly `{id, action,
dry_run}` and every other key is a refusal."""
src = HELPER.read_text(encoding="utf-8")
assert not re.search(r"subprocess\.\w+\([^)]*shell\s*=", src, re.S), (
"no `shell=` at a call position (the docstring's negation is backticked; "
"every call is a fixed argv list — `shell=True` included)")
flags = re.findall(r'add_argument\(\s*"([^"]+)"', src)
assert flags, "the flags must be there for the pin to mean anything"
assert not {flag for flag in flags if flag in ("--file", "--line", "--pattern")}, (
f"the wire takes an id, never a path/line/pattern; the flags are {flags}")
assert 'set(body) - {"id", "action", "dry_run"}' in src, (
"the wire's gate: anything wider than {id, action, dry_run} is a refusal")
assert '"body: exactly {id, action, dry_run}"' in src
def test_the_sink_file_is_left_alone_when_someone_elses_wrote_it(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""An operator's file at the sink's path is not the helper's to overwrite:
the `map` disable refuses before the config write, and the file is untouched."""
stubs_dir, log = stubs
sink = _sink(tree)
operator_file = "# an operator's file\nserver {\n listen 127.0.0.1:44451;\n}\n"
sink.write_text(operator_file, encoding="utf-8")
before = _snapshot(tree)
proc, answer, calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", MAP_ROUTE, "--apply"])
assert proc.returncode == 1
assert answer == {"ok": False,
"detail": "sink-file-not-ours: /etc/nginx/stream.d/00-dashboards-sink.conf"}
assert _snapshot(tree) == before, "the refusal writes nothing (the operator's file is byte-identical)"
assert calls == [], f"the refusal happens before the checker and the unit: {calls}"
def test_the_sink_is_removed_only_when_it_is_empty_of_our_routes(
tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None:
"""Two routes off: two lines carry the sink value, and enabling by that id is
`ambiguous` — two facts, never a guess, because no line number crosses the
wire. After the operator restores one line by hand, the enable succeeds and the
sink goes with it. The remains-arm is pinned on the helper's own function: a
map entry still pointing at the sink keeps the file (the wire cannot reach that
arm — the ambiguity above is why)."""
stubs_dir, log = stubs
stream = tree / "etc/nginx/stream.d/stream.conf"
sink = _sink(tree)
_proc, _answer, _calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", MAP_ROUTE, "--apply"])
_proc, _answer, _calls = _run(tmp_path, stubs_dir, log,
["disable", "--id", IMMICH_ROUTE, "--apply"])
assert sink.is_file()
proc, answer, _calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", MAP_SINK_ID, "--apply"])
assert proc.returncode == 1
assert answer["ok"] is False
assert "ambiguous" in answer["detail"], f"two lines at the sink are two facts: {answer}"
text = stream.read_text(encoding="utf-8")
text = text.replace(" immich.reeseapps.com 127.0.0.1:44450; #off immich.reeseapps.com",
IMMICH_ORIGINAL)
stream.write_text(text, encoding="utf-8")
proc, answer, _calls = _run(tmp_path, stubs_dir, log,
["enable", "--id", MAP_SINK_ID, "--apply"])
assert proc.returncode == 0, proc.stderr
assert answer["state"] == "on"
assert not sink.exists(), "the helper removes the sink it wrote once no route still points at it"
# the remains-arm, on the helper's own function: a route still points at the sink
helper = _load("routectl_under_test", HELPER)
agent = _load("agent_under_test", AGENT)
ctx = argparse.Namespace(root=str(tree),
haproxy_config="/etc/haproxy/haproxy.cfg",
nginx_config="/etc/nginx/nginx.conf",
state_file=str(tmp_path / "sink-state.json"),
sink_conf="/etc/nginx/stream.d/00-dashboards-sink.conf",
sink_address="127.0.0.1:44450",
nginx_unit="nginx", haproxy_unit="haproxy",
actor="manual", allow_uid="nobody")
write = helper.Write(ctx, agent, "nginx", 443, "127.0.0.1", 44450, MAP_SINK_ID)
sink.write_text(f"{SINK_HEADER}\nserver {{\n listen 127.0.0.1:44450;\n}}\n",
encoding="utf-8")
documents = {"/etc/nginx/stream.d/stream.conf":
"map $ssl_preread_server_name $targetBackend {\n"
" default 127.0.0.1:4443;\n"
" a.reeseapps.com 127.0.0.1:44450;\n"
"}\n"}
write.remove_sink_if_empty(documents)
assert sink.is_file(), "a route still points at the sink: it stays"
def test_the_helper_parses_on_python_3_10() -> None:
"""The helper is installed beside the agent and must run on a host's Python
3.10: the floor is in the header, and the file carries no 3.11+ syntax."""
src = HELPER.read_text(encoding="utf-8")
assert "# requires: python >=3.10" in src
ast.parse(src, feature_version=(3, 10))