Everything is verified and green. Final report: **Phase 34 final verification pass — complete** - Verified all 6 completed tasks' work against the live fleet and hermetic suite: `llm:radiance` job registered, `llm_engines()` registry drives 404 gate / history defaults / `/api/config` (additive, byte-identical gufo/vllm keys), radiance field map + `/stats` sidecar + 7-position spec curve, `/llm` strip/curve/rows. - **Defect found & fixed (pre-existing, task 05):** the vLLM-only sentence "vLLM exposes no per-slot JSON…" rendered on the **radiance** panel (`slotSection`'s non-gufo branch). Fix: vLLM line gated to `engine === 'vllm'`; radiance's panel slot section renders nothing (its session home is the strip) — `page-llm.js` + the served-byte pin in `tests/test_api.py` (now asserts the radiance branch is empty and the sentence can't leak). Browser-verified; re-shot `data/logs/34-llm-radiance-rest.png` (the 4.5-min wait found no in-flight request, so the busy shot remains the earlier pass's — its strip rows still stand; only the panel text predates the fix). - Tests: `uv run pytest` → **2031 passed, 19 skipped** (100%); `uv run pytest --cov` → **98.73%** (floor 94%; config 100%, llm 99%, collectors 98%, api 99%); `uv run ruff check src tests agent && uv run pyright` → clean; `node --check` OK; `uv pip list --outdated` → empty. - `uv run pytest -m live --live` → **13 passed, 2 skipped** (vLLM unset — correct; live-write opt-in — correct); the read-only radiance test passed. - Completion criteria: (1) engines `gufo`+`radiance`, `fleet.engines_live` 2, `config.llm.radiance.enabled` true — **pass (live curl)**; (2) unset → 404 no-dial + no job — **pass (spy-pinned, green)**; (3) fixtures present, both families (79 `radiance:` / 198 `vllm:` lines), drift pin — **pass**; (4) panel/curve/rows — **pass (re-shot rest screenshot)**; (5) live radiance test — **pass**; (6) suite/coverage/lint/`bash .agents/validate.sh` (exit 0) — **pass**; (7) docs — below; (8) gufo/vLLM behaviour unchanged — **pass (existing pins untouched)**. - Documentation: **README.md** full review — env table, LLM-engines section, architecture diagram, capture-script and `/llm` section all name radiance correctly; no inconsistencies found, no changes needed. **AGENTS.md** — the three task-06 fixes (fleet line, route row, module bullets) are in place and all other statements match reality; no changes needed. - Dev server healthy (`./scripts/dev-server.sh status`: all sources ok, `llm:radiance` answering); all changes left in the working tree for the harness. - Next pending phase: none — 34 was the last phase in `todo/`.
3575 lines
182 KiB
Python
3575 lines
182 KiB
Python
"""Contract tests for the scripts an operator runs: `deploy/`, `scripts/dev-*.sh`/`.py`, and
|
|
`scripts/capture-gateway-config.sh`.
|
|
|
|
The capture script gets the same treatment as the installer because it can do the same kind of
|
|
damage quietly: pointed at a gateway it cannot read, it is six empty files where six verbatim
|
|
captures were, and every parser test that reads them then passes against nothing. So it is run
|
|
here, against a stub `ssh` and a fake remote tree, and the assertions are about what lands on
|
|
disk — including the one edit it makes (a globally routable socket address becomes documentation
|
|
space) and the thing it never does (rewrite a config).
|
|
|
|
There is no shell test framework here — and adding one would buy nothing a subprocess does
|
|
not — but phase 10 rests on a sentence that lives in **two** files: the sudoers grant in
|
|
`deploy/agent-sudoers`, and the hint `deploy/install-agent.sh` prints when it is run without
|
|
root by the unprivileged `ops` account. Two copies of one instruction is exactly what drifts
|
|
silently, and the failure is an install that cannot happen on a host nobody is sitting at.
|
|
So these tests read the file, run the script the way the operator does (unprivileged, which
|
|
exits before the script can touch anything), and require the two lines to be the same line
|
|
apart from the path the script was invoked through.
|
|
|
|
The same reasoning covers `scripts/dev-certs-export.py` and the `scripts/dev-server.sh` call that
|
|
runs it (phase 24): the exporter is what turns the durable CA into a file an operator can `scp`,
|
|
and the composition — *after* the readiness probe, *as* a warning line — is as much of the
|
|
contract as the bytes it writes. So both files are run here and read here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import difflib
|
|
import importlib.util
|
|
import ipaddress
|
|
import json
|
|
import os
|
|
import pwd
|
|
import re
|
|
import shutil
|
|
import socket
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
import uvicorn
|
|
|
|
from auth_contract import ADMIN_PASSWORD, FAST_PBKDF2
|
|
from dashboards import auth, certs
|
|
from dashboards.app import create_app
|
|
from dashboards.config import Settings, Target
|
|
from dashboards.store import SampleStore
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
DEPLOY = ROOT / "deploy"
|
|
SCRIPTS = ROOT / "scripts"
|
|
INSTALLER = DEPLOY / "install-agent.sh"
|
|
SUDOERS = DEPLOY / "agent-sudoers"
|
|
BASH = shutil.which("bash") or "/bin/bash"
|
|
|
|
#: The grant as the file documents it. `/home/ops/agent-install` is the canonical staging
|
|
#: path: a monitored host has no repo checkout, so `deploy/` and `agent/` are copied there
|
|
#: (phase 10 task 02) and every host ends up with the same absolute path to grant.
|
|
CANONICAL_GRANT = "ops ALL=(root) NOPASSWD: /home/ops/agent-install/deploy/install-agent.sh"
|
|
|
|
#: The prefix a correct hint line starts with — user, run-as, and NOPASSWD, everything but
|
|
#: the path, which is the one thing the installer is allowed to substitute.
|
|
GRANT_PREFIX = "ops ALL=(root) NOPASSWD: "
|
|
|
|
#: Everything a non-root operator needs to hear, besides the grant line itself.
|
|
HINT_MUST_SAY = (
|
|
"visudo -cf", # check the syntax before installing it
|
|
"install -m 0440 -o root -g root", # and install it the way sudoers files are installed
|
|
"/etc/sudoers.d/dashboards-agent", # the destination both files name
|
|
"deploy/agent-sudoers", # the pointer back to the documented file
|
|
)
|
|
|
|
|
|
def _run(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(args, capture_output=True, text=True, cwd=cwd, check=False)
|
|
|
|
|
|
def _grant_lines(text: str) -> list[str]:
|
|
"""The sudoers *rules* in a file — comments start with `#`, so this never matches prose."""
|
|
return [line for line in text.splitlines() if line.startswith(GRANT_PREFIX)]
|
|
|
|
|
|
def _installer_without_root(*flags: str) -> subprocess.CompletedProcess[str]:
|
|
"""Run the installer the way `ops` does.
|
|
|
|
Skipped as root on purpose: root would go past `need_root` and really install a systemd
|
|
unit on the machine running pytest.
|
|
"""
|
|
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
|
pytest.skip("running as root — the installer would install a real unit here")
|
|
return _run([BASH, str(INSTALLER), *flags])
|
|
|
|
|
|
# --- deploy/agent-sudoers ---------------------------------------------------------------
|
|
|
|
|
|
def test_agent_sudoers_exists_and_is_a_config_file_not_a_script() -> None:
|
|
assert SUDOERS.is_file(), "deploy/agent-sudoers is missing (phase 10 task 01)"
|
|
assert not os.access(SUDOERS, os.X_OK), "a sudoers file is data; only install-agent.sh runs"
|
|
|
|
|
|
def test_agent_sudoers_is_exactly_one_grant_line_at_the_canonical_path() -> None:
|
|
"""One rule, the installer, the staging path — nothing else may be granted."""
|
|
lines = _grant_lines(SUDOERS.read_text())
|
|
assert lines == [CANONICAL_GRANT], (
|
|
"deploy/agent-sudoers must grant exactly one command, the staged installer:\n" + "\n".join(lines)
|
|
)
|
|
assert "/home/ops/agent-install" in SUDOERS.read_text()
|
|
|
|
|
|
def test_agent_sudoers_grants_no_shell_editor_or_second_path() -> None:
|
|
"""The 'deliberately NOT granted' block has to stay a comment, not become a rule."""
|
|
text = SUDOERS.read_text()
|
|
not_granted = text.split("deliberately NOT granted", 1)[-1]
|
|
for never in ("visudo", "sh", "tee", "systemctl", "usermod", "upscmd", "upsrw"):
|
|
assert never in not_granted, f"the file must name {never!r} as deliberately not granted"
|
|
assert not_granted.lstrip().startswith("-"), "the block heading is followed by the list"
|
|
# Every line of that block is a comment: a stray rule there would be a second grant.
|
|
assert _grant_lines(not_granted) == []
|
|
# And it names the token file, because "we did not grant reading it" is the point.
|
|
assert "/etc/dashboards-agent.token" in not_granted
|
|
|
|
|
|
def test_agent_sudoers_documents_the_why_and_the_two_stage_install() -> None:
|
|
"""House style (deploy/nut-sudoers): why it is needed, then check-first install steps."""
|
|
text = SUDOERS.read_text()
|
|
assert "WHY THIS IS NEEDED" in text
|
|
for step in HINT_MUST_SAY:
|
|
assert step in text, f"deploy/agent-sudoers must document {step!r}"
|
|
# The installed copy is checked too — the file being valid is not the same as the
|
|
# installed file being valid (nut-sudoers' third step, and the reason stays).
|
|
assert text.count("visudo -cf") >= 2
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("visudo") is None, reason="visudo is not installed on this box")
|
|
def test_agent_sudoers_passes_the_syntax_check_it_documents() -> None:
|
|
"""The file tells the operator to run `visudo -cf` on it, so it must survive that."""
|
|
result = _run([shutil.which("visudo") or "/usr/sbin/visudo", "-cf", str(SUDOERS)])
|
|
assert result.returncode == 0, f"visudo -cf failed:\n{result.stdout}{result.stderr}"
|
|
|
|
|
|
# --- deploy/install-agent.sh without root -----------------------------------------------
|
|
|
|
|
|
def test_installer_without_root_exits_1_and_prints_the_grant_line() -> None:
|
|
result = _installer_without_root("--bind", "127.0.0.1")
|
|
assert result.returncode == 1, "the non-root refusal keeps its exit code (1)"
|
|
assert result.stdout == "", "the refusal is a failure: it goes to stderr, not stdout"
|
|
lines = _grant_lines(result.stderr)
|
|
assert lines == [f"{GRANT_PREFIX}{Path(os.path.realpath(INSTALLER))}"], (
|
|
"the hint must print one grant line naming this script's resolved path:\n" + result.stderr
|
|
)
|
|
|
|
|
|
def test_hint_and_the_sudoers_file_are_the_same_line_apart_from_the_path() -> None:
|
|
"""The pinned version of the diff in data/logs/NOTES-sudoers.md."""
|
|
printed = _installer_without_root("--bind", "127.0.0.1").stderr
|
|
hint = _grant_lines(printed)[0]
|
|
canonical = _grant_lines(SUDOERS.read_text())[0]
|
|
assert hint.startswith(GRANT_PREFIX) and canonical.startswith(GRANT_PREFIX)
|
|
assert hint.removeprefix(GRANT_PREFIX).endswith("/deploy/install-agent.sh")
|
|
assert hint.removeprefix(GRANT_PREFIX).removeprefix(str(DEPLOY.parent)) == (
|
|
canonical.removeprefix(GRANT_PREFIX).removeprefix("/home/ops/agent-install")
|
|
), "same user, same (root), same NOPASSWD, same script — only the checkout path differs"
|
|
|
|
|
|
def test_hint_resolves_a_relative_invocation_to_an_absolute_path() -> None:
|
|
"""`bash deploy/install-agent.sh` from the repo root still prints a path sudo can match."""
|
|
result = _run([BASH, "deploy/install-agent.sh", "--bind", "10.0.0.5"], cwd=ROOT)
|
|
assert result.returncode == 1
|
|
assert _grant_lines(result.stderr) == [f"{GRANT_PREFIX}{Path(os.path.realpath(INSTALLER))}"]
|
|
|
|
|
|
def test_installer_names_the_two_stage_install_and_points_at_the_file() -> None:
|
|
stderr = _installer_without_root("--bind", "127.0.0.1").stderr
|
|
for phrase in HINT_MUST_SAY:
|
|
assert phrase in stderr, f"the non-root hint must say {phrase!r}, not just 'run as root'"
|
|
# The pointer is the file next to *this* script, wherever the checkout lives.
|
|
assert str(Path(os.path.realpath(SUDOERS))) in stderr
|
|
|
|
|
|
def test_installer_hint_repeats_the_command_that_was_asked_for() -> None:
|
|
"""Copy-pasteable: the hint ends with the same invocation, with the flags it was given."""
|
|
stderr = _installer_without_root("--uninstall").stderr
|
|
assert f"sudo {Path(os.path.realpath(INSTALLER))} --uninstall" in stderr
|
|
|
|
|
|
def test_installer_help_points_at_the_sudoers_grant() -> None:
|
|
"""The header comment *is* the help text, so the grant is discoverable from --help."""
|
|
result = _run([BASH, str(INSTALLER), "--help"])
|
|
assert result.returncode == 0
|
|
assert "deploy/agent-sudoers" in result.stdout
|
|
|
|
|
|
# --- the tunnel machinery stays retired (phase 10) ---------------------------------------
|
|
|
|
#: Names and ports that only ever existed to serve the ssh-tunnel dev transport, which phase
|
|
#: 10 deleted. Every pattern is written as a character class (`[d]ev-…`) so that **this file
|
|
#: cannot match its own grep**: the phase criterion demands the repo stay free of those
|
|
#: literals, and a guard that reintroduces one to forbid it is the oldest trick there is.
|
|
#: Scope excludes `.agents/` (phase files and run reports are history — rewriting a finished
|
|
#: phase's record to satisfy a grep would falsify it) and `tests/fixtures/` (verbatim
|
|
#: upstream captures: a sector counter or a wheel hash may contain those digits by accident,
|
|
#: and rule 7 forbids editing a capture to please a grep).
|
|
TUNNEL_PATTERNS = ("[d]ev-tunnels", "DEV_[T]UNNELS", "DEV_[F]ORWARDS", r"912[123]\b", r"[3]494\b")
|
|
|
|
#: Where those names must not appear: every file that *tells someone what to run*.
|
|
#: `tests/fixtures/` is excluded (verbatim captures, see above) and so is `.agents/` —
|
|
#: phase files and run reports are history, and rewriting a finished phase's record to
|
|
#: satisfy a grep would falsify it.
|
|
TUNNEL_FREE_ROOTS = ("scripts", "deploy", "src", "agent", "web")
|
|
|
|
|
|
def _tunnel_scope_files() -> list[Path]:
|
|
files: list[Path] = []
|
|
for top in TUNNEL_FREE_ROOTS:
|
|
for path in (ROOT / top).rglob("*"):
|
|
if path.is_file() and path.suffix in (".py", ".sh", ".md", ".css", ".js", ".html"):
|
|
files.append(path)
|
|
files.append(ROOT / "README.md")
|
|
files.append(ROOT / "AGENTS.md")
|
|
files.append(ROOT / ".env.example")
|
|
files.append(ROOT / ".env.dev.example")
|
|
for path in (ROOT / "tests").glob("*.py"):
|
|
files.append(path)
|
|
return files
|
|
|
|
|
|
def test_dev_tunnel_script_is_deleted() -> None:
|
|
"""The tunnel script is gone, not shelved: git history keeps it if it is ever needed."""
|
|
# Name built at runtime so this file never carries the literal the grep forbids.
|
|
script = ROOT / "scripts" / ("dev-" + "tunnels.sh")
|
|
assert not script.exists(), (
|
|
"the retired ssh-tunnel dev script is back — phase 10 deleted it deliberately"
|
|
)
|
|
|
|
|
|
def test_no_operational_file_mentions_the_tunnel_machinery() -> None:
|
|
"""A stale tunnel-env instruction is a setup step nobody can complete any more."""
|
|
offenders: list[str] = []
|
|
for path in _tunnel_scope_files():
|
|
text = path.read_text(errors="replace")
|
|
for pattern in TUNNEL_PATTERNS:
|
|
if re.search(pattern, text):
|
|
offenders.append(f"{path.relative_to(ROOT)}: /{pattern}/")
|
|
assert not offenders, "tunnel-era names/ports are back:\n" + "\n".join(offenders)
|
|
|
|
|
|
# --- the mTLS registration is the documented default (phase 24) ---------------------------
|
|
|
|
#: The three flags that make a host's **install** its registration moment: the run that installs
|
|
#: the agent also pulls that host's bundle from the dashboard that owns the authority. The
|
|
#: token-mode install is still supported (a host nobody has bundled keeps its bearer token), but
|
|
#: it is the compatibility path now, and a setup file that leads with it is a recipe for a fleet
|
|
#: of `http://` agents — which is the exact state `TODO.md:3` asked to be rid of.
|
|
REGISTRATION_FLAGS = ("--fetch-bundle", "--cert-name", "--session")
|
|
|
|
#: The files that tell an operator what to type, per host.
|
|
REGISTRATION_DOCS = ("README.md", "AGENTS.md", ".env.example", ".env.dev.example")
|
|
|
|
|
|
def _host_target_lines(path: Path) -> list[str]:
|
|
"""Every `HOST_TARGETS` assignment line in a file, comments included.
|
|
|
|
The commented example in `.env.example` counts, because it is the line a reader copies.
|
|
"""
|
|
return [line for line in path.read_text().splitlines()
|
|
if re.search(r"^\s*#?\s*(export\s+)?HOST_TARGETS\s*=", line)]
|
|
|
|
|
|
@pytest.mark.parametrize("name", REGISTRATION_DOCS)
|
|
def test_the_setup_docs_teach_registration_with_a_bundle(name: str) -> None:
|
|
"""The per-host recipe a reader copies is the bundle fetch, not the bearer token."""
|
|
text = (ROOT / name).read_text()
|
|
for flag in REGISTRATION_FLAGS:
|
|
assert flag in text, f"{name} stopped naming {flag} — the fetch is no longer documented"
|
|
|
|
|
|
@pytest.mark.parametrize("name", (".env.example", ".env.dev.example"))
|
|
def test_the_template_target_urls_are_https_and_credential_free(name: str) -> None:
|
|
"""A `HOST_TARGETS` line an operator copies is `https://` with no userinfo.
|
|
|
|
Pinning the *example* is what keeps this true: prose can and should still document the
|
|
token-mode shape (it is a supported dial, `collectors.fetch_snapshot` keeps its path byte-for-
|
|
byte), but the line that gets pasted into a real config must not seed a non-mTLS fleet. The
|
|
empty `HOST_TARGETS=` is allowed — unconfigured is a different fact from misconfigured.
|
|
"""
|
|
lines = _host_target_lines(ROOT / name)
|
|
assert lines, f"{name} lost its HOST_TARGETS documentation entirely"
|
|
for line in lines:
|
|
value = line.split("=", 1)[1]
|
|
urls = re.findall(r"\w+://[^\s\"']+", value)
|
|
if not urls:
|
|
continue # the empty assignment
|
|
bad = [u for u in urls if not u.startswith("https://") or "@" in u]
|
|
assert not bad, f"{name} seeds a non-mTLS or userinfo-bearing target: {bad}"
|
|
|
|
|
|
# --- shell syntax (the only gate these scripts get) -------------------------------------
|
|
|
|
|
|
#: Names a shell function must never take. Bash resolves a **function** before an external
|
|
#: command of the same name, so defining `install()` in a script that then calls
|
|
#: `install -d /somewhere` is not a directory creation — it is the function calling itself,
|
|
#: forever. That is not hypothetical: `deploy/install-agent.sh` did exactly this, and the
|
|
#: first real install of phase 10 spun on the SMART block for two minutes and installed
|
|
#: nothing (no unit, no agent file, no token) while looking like a working script to `bash -n`.
|
|
SHADOW_FORBIDDEN = frozenset(
|
|
{
|
|
"install",
|
|
"test",
|
|
"time",
|
|
"date",
|
|
"sort",
|
|
"grep",
|
|
"sed",
|
|
"tee",
|
|
"type",
|
|
"ulimit",
|
|
"wait",
|
|
"kill",
|
|
"cd",
|
|
"pwd",
|
|
}
|
|
)
|
|
|
|
_FUNCTION_DEF_RE = r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(\)"
|
|
|
|
|
|
def _function_names(script: Path) -> list[str]:
|
|
return re.findall(_FUNCTION_DEF_RE, script.read_text(), flags=re.MULTILINE)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"script",
|
|
sorted([*DEPLOY.glob("*.sh"), *(ROOT / "scripts").glob("*.sh")]),
|
|
ids=lambda p: str(p.relative_to(ROOT)),
|
|
)
|
|
def test_shell_script_has_valid_syntax(script: Path) -> None:
|
|
result = _run([BASH, "-n", str(script)])
|
|
assert result.returncode == 0, f"bash -n failed for {script}:\n{result.stderr}"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"script",
|
|
sorted([*DEPLOY.glob("*.sh"), *(ROOT / "scripts").glob("*.sh")]),
|
|
ids=lambda p: str(p.relative_to(ROOT)),
|
|
)
|
|
def test_no_shell_function_is_named_after_a_coreutil_it_could_be_asked_to_run(script: Path) -> None:
|
|
"""`install()` + `install -d` is infinite recursion, and `bash -n` cannot see it."""
|
|
shadowed = SHADOW_FORBIDDEN.intersection(_function_names(script))
|
|
assert not shadowed, (
|
|
f"{script.relative_to(ROOT)} defines function(s) {sorted(shadowed)} that shadow an "
|
|
"external command: bash runs the function, so any call meant for the program recurses"
|
|
)
|
|
|
|
|
|
# --- the install path, run for real against a sandboxed root boundary --------------------
|
|
|
|
#: The commands of `install()` that would touch the machine running pytest. Each one is a
|
|
#: PATH stub that records its arguments and succeeds — the same trick `ScriptedNutClient` is
|
|
#: for the UPS wire: fake the privileged boundary, run the real logic.
|
|
ROOT_BOUNDARY_STUBS = ("id", "getent", "useradd", "usermod", "chown", "systemctl", "sudo")
|
|
|
|
#: The three absolute destinations the installer writes to, rewritten into a temp directory
|
|
#: so the test exercises the body instead of the operator's `/usr` and `/etc`.
|
|
SANDBOX_REWRITES = {
|
|
'AGENT_DIR="/usr/local/lib/dashboards-agent"': 'AGENT_DIR="{root}/lib"',
|
|
'TOKEN_FILE="/etc/dashboards-agent.token"': 'TOKEN_FILE="{root}/token"',
|
|
'CERT_FILE="/etc/dashboards-agent.certs"': 'CERT_FILE="{root}/certs"',
|
|
'UNIT="/etc/systemd/system/$SERVICE.service"': 'UNIT="{root}/dashboards-agent.service"',
|
|
'ROUTECTL_TARGET="/usr/local/sbin/dashboards-routectl.py"':
|
|
'ROUTECTL_TARGET="{root}/sbin/dashboards-routectl.py"',
|
|
'ROUTECTL_SOCKET_UNIT="/etc/systemd/system/dashboards-routectl.socket"':
|
|
'ROUTECTL_SOCKET_UNIT="{root}/dashboards-routectl.socket"',
|
|
'ROUTECTL_SERVICE_UNIT="/etc/systemd/system/dashboards-routectl@.service"':
|
|
'ROUTECTL_SERVICE_UNIT="{root}/dashboards-routectl@.service"',
|
|
'ROUTECTL_SOCKET_PATH="/run/dashboards-routectl.sock"':
|
|
'ROUTECTL_SOCKET_PATH="{root}/run/dashboards-routectl.sock"',
|
|
'ROUTECTL_STATE_DIR="/var/lib/dashboards-routectl"':
|
|
'ROUTECTL_STATE_DIR="{root}/var-lib/dashboards-routectl"',
|
|
}
|
|
|
|
|
|
def _sandbox_installer(tmp_path: Path) -> tuple[Path, Path, dict[str, str]]:
|
|
"""A copy of the installer whose writes land in `tmp_path`, plus a stubbed PATH.
|
|
|
|
Returns (script, stub call log, env). `agent/host_agent.py` is stubbed next to the copy
|
|
because the script resolves the agent as `../agent/host_agent.py`.
|
|
"""
|
|
text = INSTALLER.read_text()
|
|
target = tmp_path / "target"
|
|
for old, new in SANDBOX_REWRITES.items():
|
|
assert old in text, f"the installer moved {old!r} — update SANDBOX_REWRITES with it"
|
|
text = text.replace(old, new.format(root=target))
|
|
|
|
script = tmp_path / "deploy" / "install-agent.sh"
|
|
script.parent.mkdir(parents=True)
|
|
script.write_text(text)
|
|
(tmp_path / "agent").mkdir()
|
|
(tmp_path / "agent" / "host_agent.py").write_text("# stub agent source\n", encoding="utf-8")
|
|
# The routectl sources resolve as `$SCRIPT_DIR/…`, so the real files land next to the
|
|
# copy: the guard's `[ -f "$_src" ]` check and the re-run `cmp` both need them.
|
|
for _name in ("dashboards-routectl.py", "dashboards-routectl.socket",
|
|
"dashboards-routectl@.service"):
|
|
shutil.copyfile(DEPLOY / _name, script.parent / _name)
|
|
|
|
stubs = tmp_path / "stubs"
|
|
stubs.mkdir()
|
|
call_log = tmp_path / "stub-calls.log"
|
|
for name in ROOT_BOUNDARY_STUBS:
|
|
stub = stubs / name
|
|
# `id -u` must answer 0 or need_root() refuses before the body runs; every other call
|
|
# just records itself, which is what the assertions below read.
|
|
body = ['#!/bin/sh', f'printf "%s %s\\n" "{name}" "$*" >> "$STUB_LOG"']
|
|
if name == "id":
|
|
body.append('[ "$1" = "-u" ] && echo 0')
|
|
body.append("exit 0")
|
|
stub.write_text("\n".join(body) + "\n", encoding="utf-8")
|
|
stub.chmod(0o755)
|
|
|
|
env = {**os.environ, "PATH": f"{stubs}{os.pathsep}{os.environ['PATH']}", "STUB_LOG": str(call_log)}
|
|
return script, call_log, env
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_install_path_installs_into_its_own_paths_and_prints_the_token_once(tmp_path: Path) -> None:
|
|
"""The install body end to end: files, unit, token, and no recursion.
|
|
|
|
The exit code is the regression test for the `install()` shadow: while the function was
|
|
named after `/usr/bin/install`, this never returned at all.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
try:
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--bind", "10.0.0.5", "--port", "9120"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
pytest.fail(f"the installer did not return (recursion? partial output: {exc.stdout!r})")
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
|
|
target = tmp_path / "target"
|
|
assert (target / "lib" / "host_agent.py").is_file(), "install -m never landed the agent"
|
|
unit = (target / "dashboards-agent.service").read_text()
|
|
token_path = target / "token"
|
|
token = token_path.read_text().strip()
|
|
|
|
assert "User=dashboards-agent" in unit
|
|
assert f"--token-file {token_path}" in unit, "the unit must use the file, never --token"
|
|
assert "--host 10.0.0.5 --port 9120" in unit
|
|
# A token-only install runs no TLS, so it gets neither the bundle flag nor the directory
|
|
# the bundle unpacks into (the agent falls back to a private mkdtemp without one).
|
|
assert "--cert-bundle" not in unit, "no bundle installed means no TLS flag on a token install"
|
|
assert "RuntimeDirectory=dashboards-agent" not in unit
|
|
# No device filter, on purpose (phase 17): a strict device policy denies every node the
|
|
# allow-list fails to match, and systemd matches those globs by *name* against
|
|
# /proc/devices — where kernel 7.2 lists NVMe as `blkext`, so the old `/dev/nvme*` entry
|
|
# locked the whole device namespace on driveripper. Device access is DAC (the `disk` group
|
|
# the installer grants) plus SELinux, the standard SMART-monitoring arrangement.
|
|
assert "DevicePolicy" not in unit, (
|
|
"a strict device policy with a name-matched allow-list is the deny-all device filter "
|
|
"that locked driveripper's SMART reads — the unit must carry none"
|
|
)
|
|
assert "DeviceAllow" not in unit, "no device filter means no allow-list either"
|
|
# PrivateDevices=false survives as the explicit statement of the systemd default: a
|
|
# private /dev with no device list dies at spawn (status 208/STDIN), because systemd
|
|
# opens /dev/null for the service stdin before it execs python.
|
|
assert "PrivateDevices=false" in unit, (
|
|
"the explicit default is what keeps a private-/dev sandbox from dying at STDIN"
|
|
)
|
|
assert token and len(token) >= 32, "a generated token is expected"
|
|
assert token in result.stdout, "first install prints the token, and never again"
|
|
assert f'HOST_TARGETS="hostname=http://{token}@10.0.0.5:9120"' in result.stdout
|
|
|
|
calls = call_log.read_text()
|
|
assert "systemctl daemon-reload" in calls
|
|
assert "systemctl enable --now dashboards-agent" in calls
|
|
assert "chown root:dashboards-agent" in calls, "the token file must become group-readable"
|
|
|
|
|
|
#: A line the unit heredoc may legitimately write: a comment, a `[Section]` header, or a
|
|
#: systemd directive assignment. Anything else is the unquoted heredoc having *expanded*
|
|
#: something — see `test_the_written_unit_is_only_comments_and_directives`.
|
|
_UNIT_LINE_RE = re.compile(r"^(?:#|\[[A-Za-z]+\]|[A-Za-z]+=)")
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_written_unit_is_only_comments_and_directives(tmp_path: Path) -> None:
|
|
"""The unit heredoc is unquoted so `$TLS_UNIT_LINES` and `$TLS_EXEC_ARGS` can expand.
|
|
|
|
That is also what executes its **comments**: writing the phase-17 rationale with backticks
|
|
around `disk`, `blkext` and `/dev/nvme*` made the install run them (`disk: command not
|
|
found`, smartctl's version banner) and paste the output into the unit file, with `bash -n`
|
|
and every text assertion still happy. Quote the heredoc and phase 15's interpolated lines
|
|
stop working, so the guard is on the *result*: a unit line is a comment, a section header,
|
|
or a directive assignment — never the output of something the prose mentioned.
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--bind", "127.0.0.1", "--port", "9199"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
offenders = [line for line in unit.splitlines() if line.strip() and not _UNIT_LINE_RE.match(line)]
|
|
assert not offenders, "the unit heredoc expanded its own text into the unit file:\n" + "\n".join(
|
|
offenders
|
|
)
|
|
assert result.stderr == "", "installing must not execute anything the unit's comments mention"
|
|
|
|
|
|
# --- the template's unit: no device filter, and the whole unit pinned (phase 17 task 04) ------
|
|
|
|
#: The ten lines the pre-phase template wrote between ``PrivateDevices=false`` and
|
|
#: ``ProtectSystem=strict`` — the device filter phase 17 removes. Pinned as the *old* text, in
|
|
#: order, because the fix is the absence of exactly these lines: re-adding
|
|
#: ``DeviceAllow=/dev/nvme* rw`` with ``DevicePolicy=strict`` is the deny-all returning, and a
|
|
#: test that only asserted "`DevicePolicy` is absent today" would pass while somebody added
|
|
#: the allow-list back under a default `auto` policy that ignores it.
|
|
DEVICE_FILTER_LINES = (
|
|
"DevicePolicy=strict",
|
|
"DeviceAllow=/dev/null rw",
|
|
"DeviceAllow=/dev/zero rw",
|
|
"DeviceAllow=/dev/full rw",
|
|
"DeviceAllow=/dev/random rw",
|
|
"DeviceAllow=/dev/urandom rw",
|
|
"DeviceAllow=/dev/nvme* rw",
|
|
"DeviceAllow=/dev/sd* rw",
|
|
"DeviceAllow=/dev/vd* rw",
|
|
"DeviceAllow=/dev/sg* rw",
|
|
)
|
|
|
|
#: The unit the template wrote **before** phase 17, rendered for a default token install
|
|
#: (bind `10.0.0.5`, port `9120`, no bundle), with the sandbox root spelled `<ROOT>`.
|
|
#:
|
|
#: This is the whole unit and not a highlight reel: it is the reference the default install is
|
|
#: compared against, so a template line added, dropped, reordered or reworded *outside* this
|
|
#: phase's remit fails a test instead of landing in the next host's service file. The comment
|
|
#: block is included verbatim (it is what the operator reads in `systemctl cat`) even though
|
|
#: phase 17 task 01 rewrote that prose on purpose — which is why the comparison below reads
|
|
#: directives, and the new rationale is pinned separately for the two facts it must carry.
|
|
PRE_PHASE_DEFAULT_UNIT = """[Unit]
|
|
Description=Homelab host agent (read-only system metrics)
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=dashboards-agent
|
|
ExecStart=/usr/bin/env python3 <ROOT>/target/lib/host_agent.py --host 10.0.0.5 --port 9120 --token-file <ROOT>/target/token
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
Environment=PYTHONUNBUFFERED=1
|
|
SyslogIdentifier=dashboards-agent
|
|
|
|
# The agent reads /proc, /sys and statvfs() and writes nothing, so everything writable can
|
|
# be taken away. smartctl is the one thing that needs a real /dev node, so instead of
|
|
# PrivateDevices the unit closes all devices and reopens only the disk nodes, read/write
|
|
# at the cgroup layer while the code (and smartctl -i -A -H) stays read-only by design.
|
|
#
|
|
# The pseudo nodes are not decoration: DevicePolicy=strict denies every node that is not
|
|
# listed, and systemd needs /dev/null for the service's stdin before it ever execs python —
|
|
# without it the unit dies at spawn with "Failed at step STDIN" (status 208), which is how
|
|
# the first phase-10 install failed on a live host running systemd 257. /dev/zero, full,
|
|
# random and urandom are the rest of the set a PrivateDevices= sandbox would have provided,
|
|
# and none of them is a disk.
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
PrivateDevices=false
|
|
DevicePolicy=strict
|
|
DeviceAllow=/dev/null rw
|
|
DeviceAllow=/dev/zero rw
|
|
DeviceAllow=/dev/full rw
|
|
DeviceAllow=/dev/random rw
|
|
DeviceAllow=/dev/urandom rw
|
|
DeviceAllow=/dev/nvme* rw
|
|
DeviceAllow=/dev/sd* rw
|
|
DeviceAllow=/dev/vd* rw
|
|
DeviceAllow=/dev/sg* rw
|
|
ProtectSystem=strict
|
|
ProtectHome=read-only
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
RestrictSUIDSGID=true
|
|
RestrictNamespaces=true
|
|
RestrictRealtime=true
|
|
LockPersonality=true
|
|
MemoryDenyWriteExecute=true
|
|
CapabilityBoundingSet=
|
|
SystemCallFilter=@system-service
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
"""
|
|
|
|
|
|
def _unit_heredoc() -> str:
|
|
"""The template's unit heredoc, exactly as the script holds it.
|
|
|
|
Scoped on purpose: the rest of the script is prose about flags, and a pin that read the
|
|
whole file for `DeviceAllow` would be satisfied by deleting a sentence rather than by the
|
|
absence of a *unit line*.
|
|
"""
|
|
text = INSTALLER.read_text()
|
|
assert "<<UNIT_FILE" in text, "the unit is still written from a heredoc"
|
|
heredoc = text.split("<<UNIT_FILE\n", 1)[1].split("\nUNIT_FILE\n", 1)[0]
|
|
assert "[Service]" in heredoc and "CapabilityBoundingSet" in heredoc, (
|
|
"the heredoc region extracted from the script is not the unit"
|
|
)
|
|
return heredoc
|
|
|
|
|
|
def _unit_directives(unit: str) -> list[str]:
|
|
"""The lines systemd acts on — every line that is neither a comment nor blank.
|
|
|
|
Comments carry the phase's reasoning and *are* part of the file, but they are also the one
|
|
thing phase 17 task 01 rewrote deliberately (the old block justified the filter; the new
|
|
one explains its absence). Directives are what a host runs, so those are compared exactly.
|
|
"""
|
|
return [line for line in unit.splitlines() if line.strip() and not line.startswith("#")]
|
|
|
|
|
|
def test_the_template_unit_has_no_device_filter_lines() -> None:
|
|
"""The fix is the absence, so the absence is what is pinned.
|
|
|
|
Three layers, because the regression has three shapes: a `DevicePolicy=strict` or
|
|
`DeviceAllow=` line back in the heredoc (the deny-all returning on kernel 7.2, where
|
|
systemd matches those globs by *name* against `/proc/devices` and lists NVMe as `blkext`,
|
|
so `/dev/nvme*` matched nothing and strict denied the whole namespace);
|
|
`PrivateDevices=false` removed as "redundant" because it is the systemd default (a private
|
|
`/dev` with no device list dies at spawn with `status=208/STDIN`); and a rationale comment
|
|
that stops naming the mechanism, which is how the next operator re-adds the filter while
|
|
believing they are hardening the unit.
|
|
"""
|
|
text = INSTALLER.read_text()
|
|
# The phase's completion criterion is this exact grep, so it is pinned as one: neither
|
|
# directive may appear anywhere in the script, prose included.
|
|
assert "DevicePolicy" not in text, "no device policy line anywhere in the installer"
|
|
assert "DeviceAllow" not in text, "no device allow-list anywhere in the installer"
|
|
|
|
heredoc = _unit_heredoc()
|
|
offenders = [line for line in heredoc.splitlines() if re.match(r"^\s*(DevicePolicy|DeviceAllow)\s*=", line)]
|
|
assert offenders == [], "the unit heredoc grew a device filter line again:\n" + "\n".join(offenders)
|
|
assert "PrivateDevices=false" in heredoc, (
|
|
"the explicit default is the line that keeps the unit from dying at STDIN; the filter "
|
|
"going away does not make it redundant"
|
|
)
|
|
# The rewritten rationale has to be an explanation, not a shrug (task 01's own criterion).
|
|
comment = "\n".join(line for line in heredoc.splitlines() if line.startswith("#"))
|
|
assert "/proc/devices" in comment and "blkext" in comment, (
|
|
"the comment must name the mechanism, or the next reader cannot tell an omitted filter "
|
|
"from a forgotten one"
|
|
)
|
|
assert "208" in comment, "and the STDIN failure that PrivateDevices=false still prevents"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_default_install_unit_is_the_pre_phase_unit_minus_the_filter_lines(tmp_path: Path) -> None:
|
|
"""No flags writes the unit this script has always written, minus exactly ten lines.
|
|
|
|
`AGENTS.md` invariant 3 in deployment clothes: a host that needs nothing new must not get a
|
|
new unit from an unrelated phase. The reference is the whole pre-phase unit recorded above,
|
|
so every directive is compared (not a keyword spot-check) and the only allowed difference is
|
|
the device filter's absence — asserted as the ordered list comparison *and* as each of the ten
|
|
lines being absent by name, so a filter line silently renamed rather than removed fails too.
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
installed = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
|
|
reference = PRE_PHASE_DEFAULT_UNIT.replace("<ROOT>", str(tmp_path))
|
|
expected = [line for line in _unit_directives(reference) if line not in DEVICE_FILTER_LINES]
|
|
assert len(expected) == len(_unit_directives(reference)) - len(DEVICE_FILTER_LINES), (
|
|
"DEVICE_FILTER_LINES must match ten real lines of the reference unit — a filter line "
|
|
"this list no longer names is a filter that would pass this test"
|
|
)
|
|
got = _unit_directives(installed)
|
|
drift = "\n".join(
|
|
difflib.unified_diff(
|
|
expected, got, fromfile="pre-phase unit minus the filter", tofile="installed", lineterm=""
|
|
)
|
|
)
|
|
assert got == expected, f"the default unit drifted from the pre-phase unit by more than the filter:\n{drift}"
|
|
|
|
# And the ten lines are gone rather than reworded into something equivalent.
|
|
assert not [line for line in installed.splitlines() if line.startswith(DEVICE_FILTER_LINES)]
|
|
|
|
|
|
# --- the kernel-gate unit: --as-root / --caps (phase 17 task 02) ---------------------------
|
|
|
|
#: The exact sentence `--as-root` alone prints. It is not a refusal — a capability-less root is
|
|
#: a legitimate if unusual choice — but it is the fact gap 10 paid for: the first root-user
|
|
#: attempt on the kernel-7.2 host still read `n/a` on all 16 drives, because the bounding set
|
|
#: bounds even root (`CapEff 0`). Pinned verbatim so a paraphrase that drops "not uid" is a
|
|
#: test failure rather than an operator rediscovering it.
|
|
AS_ROOT_NOTE = (
|
|
"note: --as-root with the default empty CapabilityBoundingSet leaves the process "
|
|
"capability-less (the kernel gates check capabilities, not uid); pair it with --caps "
|
|
"for a kernel-gated host"
|
|
)
|
|
|
|
#: `--caps` on the dedicated service user is not the kernel-gate fix (a non-root process cannot
|
|
#: hold these capabilities across exec), so the combination is refused rather than written.
|
|
CAPS_REFUSAL = "--caps needs --as-root: a capability grant on the service user is not the kernel-gate fix"
|
|
|
|
#: The two capabilities the kernel names for the two gates (NVMe SMART log / SG_IO), and the
|
|
#: `driveripper` recipe as `--help` must teach it (phase 17 completion criterion).
|
|
KERNEL_CAPS = "CAP_SYS_RAWIO,CAP_SYS_ADMIN"
|
|
KERNEL_GATE_RECIPE = f"--as-root --caps {KERNEL_CAPS}"
|
|
|
|
|
|
def _install(script: Path, env: dict[str, str], *flags: str) -> subprocess.CompletedProcess[str]:
|
|
"""One sandboxed installer run: fake root boundary, real script body."""
|
|
try:
|
|
return subprocess.run(
|
|
[BASH, str(script), *flags],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired as exc: # pragma: no cover - the recursion regression
|
|
pytest.fail(f"the installer did not return (flags {flags}, partial output: {exc.stdout!r})")
|
|
|
|
|
|
def _directive(unit: str, key: str) -> list[str]:
|
|
"""Every line of the unit assigning `key` — a duplicate assignment is a heredoc bug."""
|
|
return [line for line in unit.splitlines() if line.split("=", 1)[0] == key]
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_default_unit_keeps_the_service_user_and_an_empty_bounding_set(tmp_path: Path) -> None:
|
|
"""No flags = the unit this script wrote before the flags existed, byte-for-byte.
|
|
|
|
`CapabilityBoundingSet=` with nothing after the `=` is the capability-less default and what
|
|
every host whose drives answer through the `disk` group runs; it is also the line
|
|
`--caps` replaces, so its exact spelling is pinned here rather than as `in unit`.
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199")
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(unit, "User") == ["User=dashboards-agent"]
|
|
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="]
|
|
assert AS_ROOT_NOTE not in result.stdout, "the note belongs to --as-root alone"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_as_root_with_caps_writes_the_kernel_gate_unit(tmp_path: Path) -> None:
|
|
"""The two lines that reproduce the working `driveripper` unit, exactly.
|
|
|
|
Comma on the command line, space in the unit: that is systemd's own spelling for
|
|
`CapabilityBoundingSet`, and it is the whole reason the flag takes a list rather than the
|
|
line. Everything else in the unit — the token file, its `0640 root:dashboards-agent`, the
|
|
hardening block — is unchanged by these flags.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
kernel_gate = ["--bind", "10.0.0.5", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
|
|
result = _install(script, env, *kernel_gate)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(unit, "User") == ["User=root"]
|
|
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"]
|
|
assert AS_ROOT_NOTE not in result.stdout, "--caps answers the note: the grant is there"
|
|
# The token file's logic is deliberately untouched by --as-root: root reads it either way,
|
|
# and re-deriving the mode per service user would be a second rule for no new safety.
|
|
token = tmp_path / "target" / "token"
|
|
assert stat.S_IMODE(token.stat().st_mode) == 0o640
|
|
assert "chown root:dashboards-agent" in call_log.read_text()
|
|
assert "--host 10.0.0.5 --port 9120" in unit
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_as_root_and_caps_write_the_kernel_gate_unit_lines(tmp_path: Path) -> None:
|
|
"""The whole flag contract in one place, plus the part only a comparison can pin.
|
|
|
|
The three flag shapes (`--as-root --caps LIST`, `--caps` alone, `--as-root` alone) each have
|
|
their own detailed pin nearby; what none of them shows is **how much** the recipe moves. So:
|
|
two installs in two sandboxes at the same bind and port, one default and one kernel-gated, and
|
|
their directive lists must differ by exactly the two lines this phase promises and nothing
|
|
else — the hardening block, the token file's path, the `ExecStart` and the `[Install]` section
|
|
are not these flags' business, and an `--as-root` that also slipped a `ProtectSystem` change in
|
|
would be a privilege change nobody asked for. The two half-recipes are then run in their own
|
|
sandboxes (one host re-run with different flags is the guard's decision, not the parser's).
|
|
"""
|
|
plain_script, _plain_log, plain_env = _sandbox_installer(tmp_path / "plain")
|
|
plain = _install(plain_script, plain_env, "--bind", "10.1.0.126", "--port", "9120")
|
|
assert plain.returncode == 0, f"default install failed:\n{plain.stdout}\n{plain.stderr}"
|
|
|
|
gate_script, gate_log, gate_env = _sandbox_installer(tmp_path / "gate")
|
|
recipe = _install(
|
|
gate_script, gate_env, "--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS
|
|
)
|
|
assert recipe.returncode == 0, f"the driveripper recipe failed:\n{recipe.stdout}\n{recipe.stderr}"
|
|
|
|
def directives(where: str) -> list[str]:
|
|
root = tmp_path / where
|
|
unit = (root / "target" / "dashboards-agent.service").read_text()
|
|
return _unit_directives(unit.replace(str(root), "<ROOT>"))
|
|
|
|
changed = [
|
|
f"{a} -> {b}"
|
|
for a, b in zip(directives("plain"), directives("gate"), strict=True)
|
|
if a != b
|
|
]
|
|
assert changed == [
|
|
"User=dashboards-agent -> User=root",
|
|
"CapabilityBoundingSet= -> CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN",
|
|
], "--as-root --caps may write these two lines and nothing else"
|
|
assert "chown root:dashboards-agent" in gate_log.read_text(), (
|
|
"--as-root leaves the token file's ownership alone: root reads it either way, and the "
|
|
"grant must not widen because of a flag that is about capabilities"
|
|
)
|
|
|
|
# The two half-recipes, in their own sandboxes (a differing re-run of one host is the
|
|
# guard's business, not the flag parser's).
|
|
caps_only_script, caps_only_log, caps_only_env = _sandbox_installer(tmp_path / "caps-only")
|
|
caps_only = _install(
|
|
caps_only_script, caps_only_env, "--bind", "127.0.0.1", "--port", "9199", "--caps", KERNEL_CAPS
|
|
)
|
|
assert caps_only.returncode == 1, "--caps without --as-root is refused"
|
|
assert CAPS_REFUSAL in caps_only.stderr
|
|
assert not caps_only_log.exists(), "and refused before a single privileged command"
|
|
|
|
root_only_script, _root_only_log, root_only_env = _sandbox_installer(tmp_path / "root-only")
|
|
root_only = _install(root_only_script, root_only_env, "--bind", "127.0.0.1", "--port", "9199", "--as-root")
|
|
assert root_only.returncode == 0, f"--as-root alone must proceed:\n{root_only.stderr}"
|
|
assert AS_ROOT_NOTE in root_only.stdout, "with the capability-less note"
|
|
root_unit = (tmp_path / "root-only" / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(root_unit, "User") == ["User=root"]
|
|
assert _directive(root_unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="]
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_as_root_alone_installs_and_prints_the_capability_note(tmp_path: Path) -> None:
|
|
"""A capability-less root proceeds — with the reminder, on stdout, and no refusal."""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--as-root")
|
|
assert result.returncode == 0, f"--as-root alone must proceed:\n{result.stdout}\n{result.stderr}"
|
|
assert AS_ROOT_NOTE in result.stdout, "the note is the operator's only warning here"
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(unit, "User") == ["User=root"]
|
|
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet="], (
|
|
"--as-root alone writes no capability grant: the note is the whole effect"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("caps", "refusal"),
|
|
[
|
|
("CAP_SYS_RAWIO", CAPS_REFUSAL),
|
|
("CAP_SYS_RAWIO,CAP_SYS_ADMIN", CAPS_REFUSAL),
|
|
("", "--caps needs at least one capability name"),
|
|
(" , ", "--caps needs at least one capability name"),
|
|
],
|
|
ids=["one-cap", "two-caps", "empty", "punctuation-only"],
|
|
)
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_caps_without_as_root_is_refused_before_anything_is_written(
|
|
tmp_path: Path, caps: str, refusal: str
|
|
) -> None:
|
|
"""Exit 1, the reason on stderr, and no side effect at all.
|
|
|
|
The check sits *before* `need_root` (the `--cert-bundle` validator's reasoning): a wrong
|
|
flag combination gets its own sentence instead of a sudoers hint, and nothing after it —
|
|
user, group membership, token, unit — can have run.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--caps", caps)
|
|
assert result.returncode == 1, "the refusal keeps exit 1"
|
|
assert result.stdout == "", "a refusal is a failure: nothing on stdout"
|
|
assert refusal in result.stderr
|
|
assert GRANT_PREFIX not in result.stderr, "a flag mistake is not a sudoers problem"
|
|
target = tmp_path / "target"
|
|
assert not (target / "dashboards-agent.service").exists(), "refused means nothing written"
|
|
assert not (target / "token").exists()
|
|
assert not call_log.exists(), "no privileged command ran either"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_caps_without_an_argument_is_refused_rather_than_crashing(tmp_path: Path) -> None:
|
|
"""`--caps` last on the command line: exit 1 naming the flag, not an `unbound variable`."""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps")
|
|
assert result.returncode == 1
|
|
assert result.stdout == ""
|
|
assert "--caps needs a capability list" in result.stderr
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_caps_list_is_normalised_to_systemd_spelling(tmp_path: Path) -> None:
|
|
"""Empty entries and stray whitespace never reach the unit as a double space…
|
|
|
|
…and a `*` stays a literal: the list is split with pathname expansion switched off, so an
|
|
operator typo cannot substitute the working directory's file names into `User=`'s partner.
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
messy = ["--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps", "CAP_SYS_RAWIO,, CAP_SYS_ADMIN "]
|
|
result = _install(script, env, *messy)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(unit, "CapabilityBoundingSet") == ["CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"]
|
|
|
|
script_glob, _log, env_glob = _sandbox_installer(tmp_path / "glob")
|
|
star = ["--bind", "127.0.0.1", "--port", "9199", "--as-root", "--caps", "*"]
|
|
result_glob = _install(script_glob, env_glob, *star)
|
|
assert result_glob.returncode == 0, f"install failed:\n{result_glob.stdout}\n{result_glob.stderr}"
|
|
unit_glob = (tmp_path / "glob" / "target" / "dashboards-agent.service").read_text()
|
|
assert _directive(unit_glob, "CapabilityBoundingSet") == ["CapabilityBoundingSet=*"], (
|
|
"the capability list is words, not a glob"
|
|
)
|
|
|
|
|
|
def test_help_teaches_the_kernel_gate_recipe() -> None:
|
|
"""Completion criterion: the `driveripper` flags are learnable from `--help` alone.
|
|
|
|
`--help` *is* the header comment, so the sentence that explains why a root service needs
|
|
its capabilities named is pinned here too — it is the one thing an executor on a fresh
|
|
clone cannot infer from a unit file.
|
|
"""
|
|
result = _run([BASH, str(INSTALLER), "--help"])
|
|
assert result.returncode == 0
|
|
assert KERNEL_GATE_RECIPE in result.stdout, "the recipe line must be copy-pasteable"
|
|
assert "--as-root run the service as root" in result.stdout
|
|
assert "--caps LIST CapabilityBoundingSet line for the unit" in result.stdout
|
|
assert "capabilities, not uid" in result.stdout, "the kernel gate is the why of both flags"
|
|
assert "capability-less" in result.stdout, "--as-root without --caps has to say what it is"
|
|
|
|
|
|
def test_the_status_probe_is_blind_to_who_the_service_runs_as() -> None:
|
|
"""`--status` probes the agent over the wire; the unit's user is invisible to it, on purpose.
|
|
|
|
A status path that read `AS_ROOT`/`CAPS` would make a health probe depend on flags the
|
|
operator may not have passed on this invocation — the probe answers about the port.
|
|
"""
|
|
status_body = INSTALLER.read_text().split("status() {", 1)[1].split("\n}", 1)[0]
|
|
assert "AS_ROOT" not in status_body
|
|
assert "CAPS" not in status_body
|
|
|
|
|
|
def test_token_file_is_readable_by_the_service_user_and_nobody_else(tmp_path: Path) -> None:
|
|
"""`--token-file` is a path the *agent* opens, as its own user.
|
|
|
|
systemd does not read the secret for the service, so a 0600 root-owned file makes the
|
|
agent exit before it binds. 0640 root:dashboards-agent is the smallest mode that works,
|
|
and it is still unreadable by `ops` (which is why deploy/agent-sudoers does not grant it).
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--bind", "10.0.0.5"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
mode = stat.S_IMODE((tmp_path / "target" / "token").stat().st_mode)
|
|
assert mode == 0o640, f"token file mode is {oct(mode)}, expected 0o640 (group = the agent user)"
|
|
assert not mode & 0o007, "the token file must never be world-readable"
|
|
|
|
|
|
# --- the re-run guard: no silent clobber (phase 17 task 03) ----------------------------------
|
|
|
|
def _refusal(unit: Path) -> str:
|
|
"""The sentence a refused re-run prints, with the unit path substituted.
|
|
|
|
Pinned verbatim because it is the operator's only explanation of why a routine re-run
|
|
stopped, and because the diff follows it on **stdout** rather than stderr: the diff is the
|
|
payload of the answer (an operator pipes it into a pager or a file), and splitting a
|
|
sentence from its diff across two streams only interleaves them.
|
|
"""
|
|
return f"refusing to overwrite {unit} (generated unit differs); run with --force to apply:"
|
|
|
|
|
|
#: One line the operator can plausibly have edited on the host — `RestartSec` is a real knob,
|
|
#: it is not a line any flag writes, and it sits far from every other assertion in these tests.
|
|
OPERATOR_EDIT = ("RestartSec=5", "RestartSec=7 # hand-tuned on the host after a flapping start")
|
|
|
|
|
|
def _hand_edit(unit_path: Path) -> str:
|
|
"""Edit the installed unit the way an operator does, and return what it now holds."""
|
|
text = unit_path.read_text()
|
|
assert OPERATOR_EDIT[0] in text, "the template unit lost the line the operator edits"
|
|
edited = text.replace(*OPERATOR_EDIT)
|
|
unit_path.write_text(edited, encoding="utf-8")
|
|
return edited
|
|
|
|
|
|
def _systemctl_calls(call_log: Path) -> list[str]:
|
|
"""Only the `systemctl` lines of the stub log — the ones that touch the running service.
|
|
|
|
The privileged calls before the guard (`id`, `getent`, `usermod`, `chown`) do run again on
|
|
a refused re-run: they are the idempotent ones the guard's reasoning accepts. `systemctl`
|
|
is what a refused run must not do — no `daemon-reload`, no `enable --now` — so that is the
|
|
half compared here.
|
|
"""
|
|
return [line for line in call_log.read_text().splitlines() if line.startswith("systemctl ")]
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_rerun_that_would_change_the_installed_unit_refuses(tmp_path: Path) -> None:
|
|
"""install → hand-edit → re-run: exit 1, the diff on stdout, nothing applied.
|
|
|
|
This is the incident the guard exists for: `driveripper`'s unit was fixed by hand, and the
|
|
installer rewrote it unconditionally. A refused run must leave the unit on disk as the
|
|
operator left it and must not reload or re-enable anything, or the refusal arrives after
|
|
the thing it is refusing.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
token_path = tmp_path / "target" / "token"
|
|
|
|
first = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
|
|
edited = _hand_edit(unit_path)
|
|
token_before = token_path.read_bytes()
|
|
reloads_before = _systemctl_calls(call_log)
|
|
assert reloads_before, "the first install did reload and enable the service"
|
|
|
|
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert result.returncode == 1, "a differing re-run refuses with exit 1"
|
|
assert result.stderr == "", "the refusal and its diff are one answer: stdout, not split"
|
|
assert _refusal(unit_path) in result.stdout
|
|
assert "--- " in result.stdout and "+++ " in result.stdout, "the diff is unified"
|
|
assert f"-{OPERATOR_EDIT[1]}" in result.stdout, "the diff shows the operator's line going away"
|
|
assert "+RestartSec=5" in result.stdout, "and the template's line coming back"
|
|
assert unit_path.read_text() == edited, "a refused run leaves the installed unit byte-identical"
|
|
# The reuse path re-reads the existing secret and rewrites the same bytes with the same
|
|
# mode; its *value* — what invariant 6 and the HOST_TARGETS entry depend on — never moves,
|
|
# which is the guarantee here (mtime is not: the write happens before the guard).
|
|
assert token_path.read_bytes() == token_before, "the reused token must not change"
|
|
assert stat.S_IMODE(token_path.stat().st_mode) == 0o640
|
|
assert _systemctl_calls(call_log) == reloads_before, (
|
|
"a refused run must not daemon-reload or enable: that would restart the service the "
|
|
"refusal exists to leave alone"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_force_prints_the_diff_then_writes_the_generated_unit(tmp_path: Path) -> None:
|
|
"""`--force` is the opt-in, and the diff still arrives: an operator approves what they saw."""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
_install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
_hand_edit(unit_path)
|
|
|
|
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120", "--force")
|
|
assert result.returncode == 0, f"--force must proceed:\n{result.stdout}\n{result.stderr}"
|
|
assert _refusal(unit_path) not in result.stdout, "--force is not a refusal"
|
|
assert f"-{OPERATOR_EDIT[1]}" in result.stdout and "+RestartSec=5" in result.stdout, (
|
|
"the diff is printed even when the answer is yes"
|
|
)
|
|
unit = unit_path.read_text()
|
|
assert "RestartSec=5" in unit and OPERATOR_EDIT[1] not in unit, "the generated unit is written"
|
|
assert _systemctl_calls(call_log).count("systemctl enable --now dashboards-agent") == 2, (
|
|
"the overwrite is followed by the usual reload/enable"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_an_unchanged_rerun_is_silent_and_proceeds(tmp_path: Path) -> None:
|
|
"""Idempotence is the normal path: same flags, same unit, no diff, no complaint, exit 0."""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
first = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
|
|
installed = unit_path.read_text()
|
|
|
|
result = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert result.returncode == 0, f"an identical re-run must proceed:\n{result.stdout}\n{result.stderr}"
|
|
assert _refusal(unit_path) not in result.stdout
|
|
assert "+++ " not in result.stdout, "nothing differs, so nothing is printed about differences"
|
|
assert "reusing the existing token" in result.stdout, "the token idiom is unchanged"
|
|
assert unit_path.read_text() == installed
|
|
assert _systemctl_calls(call_log).count("systemctl enable --now dashboards-agent") == 2
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_re_run_that_differs_refuses_without_force_and_applies_with_it(tmp_path: Path) -> None:
|
|
"""The refuse → force → silent sequence on **one** host, in that order.
|
|
|
|
Each leg has its own detailed pin beside it; what those cannot show is the transition — a
|
|
guard that refuses correctly the first time and then never lets an identical re-run through
|
|
silently is the same bug seen from the other side (an operator who has to type `--force`
|
|
every deploy stops reading the diff). So: install, hand-edit, re-run (refused, unit left
|
|
exactly as edited, no new `systemctl` call), re-run with `--force` (diff first, generated
|
|
unit on disk, service re-enabled), then re-run plain once more (silent, unit unmoved).
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
flags = ["--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
|
|
|
|
first = _install(script, env, *flags)
|
|
assert first.returncode == 0, f"first install failed:\n{first.stdout}\n{first.stderr}"
|
|
edited = _hand_edit(unit_path)
|
|
reloads = len(_systemctl_calls(call_log))
|
|
|
|
refused = _install(script, env, *flags)
|
|
assert refused.returncode == 1, "leg 1: a differing re-run refuses"
|
|
assert _refusal(unit_path) in refused.stdout and "+++ " in refused.stdout
|
|
assert unit_path.read_text() == edited, "leg 1: the hand edit survives"
|
|
assert len(_systemctl_calls(call_log)) == reloads, "leg 1: nothing was reloaded or re-enabled"
|
|
|
|
applied = _install(script, env, *flags, "--force")
|
|
assert applied.returncode == 0, f"leg 2: --force failed:\n{applied.stdout}\n{applied.stderr}"
|
|
assert "+++ " in applied.stdout, "leg 2: the diff is printed even on a yes"
|
|
assert unit_path.read_text() != edited, "leg 2: the generated unit replaced the hand edit"
|
|
assert "RestartSec=5" in unit_path.read_text()
|
|
generated = unit_path.read_text()
|
|
# three calls per applied install: daemon-reload, enable --now, and the is-active check
|
|
assert len(_systemctl_calls(call_log)) == reloads + 3, "leg 2: reload + enable, as a normal install"
|
|
|
|
settled = _install(script, env, *flags)
|
|
assert settled.returncode == 0, f"leg 3: a silent re-run failed:\n{settled.stdout}\n{settled.stderr}"
|
|
assert "refusing to overwrite" not in settled.stdout and "+++ " not in settled.stdout
|
|
assert "reusing the existing token" in settled.stdout
|
|
assert unit_path.read_text() == generated, "leg 3: the silent path leaves the applied unit in place"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_fresh_install_never_meets_the_guard(tmp_path: Path) -> None:
|
|
"""No installed unit → no comparison, no output about one, and `--force` changes nothing.
|
|
|
|
The guard is about a *re*-run. A first install must stay exactly the run it was before this
|
|
task, with or without `--force` — hence the two sandboxes writing byte-identical units.
|
|
"""
|
|
plain_script, _plain_log, plain_env = _sandbox_installer(tmp_path / "plain")
|
|
forced_script, _forced_log, forced_env = _sandbox_installer(tmp_path / "forced")
|
|
|
|
plain = _install(plain_script, plain_env, "--bind", "10.0.0.5", "--port", "9120")
|
|
forced = _install(forced_script, forced_env, "--bind", "10.0.0.5", "--port", "9120", "--force")
|
|
assert plain.returncode == 0, f"a first install failed:\n{plain.stdout}\n{plain.stderr}"
|
|
assert forced.returncode == 0, f"--force broke a first install:\n{forced.stdout}\n{forced.stderr}"
|
|
for stdout in (plain.stdout, forced.stdout):
|
|
assert "refusing to overwrite" not in stdout
|
|
assert "+++ " not in stdout
|
|
|
|
def rendered(where: str) -> str:
|
|
"""The unit that sandbox wrote, with its own sandbox root name blanked out.
|
|
|
|
The two runs live in different directories, so the absolute paths in `ExecStart` and
|
|
`--token-file` differ while every *decision* in the unit is the same — and the decision
|
|
is what `--force` must not touch.
|
|
"""
|
|
root = tmp_path / where
|
|
return (
|
|
(root / "target" / "dashboards-agent.service")
|
|
.read_text()
|
|
.replace(str(root), "<sandbox>")
|
|
)
|
|
|
|
assert rendered("plain") == rendered("forced"), "--force writes the unit the default install writes"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_kernel_gate_reproduction_refuses_once_and_then_holds(tmp_path: Path) -> None:
|
|
"""The `driveripper` case end to end: a token-mode unit is *not* the kernel-gate unit.
|
|
|
|
Switching a host to `--as-root --caps` is a real change to a real unit, so it asks first,
|
|
applies with `--force`, and is silent afterwards — the flag line in `--help` is a
|
|
reproduction command only if running it twice lands the same unit and says nothing twice.
|
|
"""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
kernel_gate = ["--bind", "10.1.0.126", "--port", "9120", "--as-root", "--caps", KERNEL_CAPS]
|
|
_install(script, env, "--bind", "10.1.0.126", "--port", "9120")
|
|
token_mode = unit_path.read_text()
|
|
|
|
asked = _install(script, env, *kernel_gate)
|
|
assert asked.returncode == 1, "a flag change to an installed unit is a change, so it asks"
|
|
assert _refusal(unit_path) in asked.stdout
|
|
assert "-User=dashboards-agent" in asked.stdout and "+User=root" in asked.stdout
|
|
assert "+CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN" in asked.stdout
|
|
assert unit_path.read_text() == token_mode, "the question must not also be the answer"
|
|
|
|
applied = _install(script, env, *kernel_gate, "--force")
|
|
assert applied.returncode == 0, f"--force failed:\n{applied.stdout}\n{applied.stderr}"
|
|
applied_unit = unit_path.read_text()
|
|
assert _directive(applied_unit, "User") == ["User=root"]
|
|
assert _directive(applied_unit, "CapabilityBoundingSet") == [
|
|
"CapabilityBoundingSet=CAP_SYS_RAWIO CAP_SYS_ADMIN"
|
|
]
|
|
|
|
again = _install(script, env, *kernel_gate, "--force")
|
|
assert again.returncode == 0, f"the same command twice failed:\n{again.stdout}\n{again.stderr}"
|
|
assert "+++ " not in again.stdout, "once applied, the same flags are the zero-noise path"
|
|
assert unit_path.read_text() == applied_unit
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_uninstall_is_still_deliberate_and_never_asks(tmp_path: Path) -> None:
|
|
"""The guard belongs to `do_install`: `--uninstall` removes a hand-edited unit without one.
|
|
|
|
A guard on the teardown path would be a guard that keeps a unit nobody wants, and
|
|
`uninstall()` does not render a unit at all.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
unit_path = tmp_path / "target" / "dashboards-agent.service"
|
|
_install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
_hand_edit(unit_path)
|
|
|
|
result = _install(script, env, "--uninstall")
|
|
assert result.returncode == 0, f"uninstall refused:\n{result.stdout}\n{result.stderr}"
|
|
assert "refusing to overwrite" not in result.stdout
|
|
assert not unit_path.exists(), "uninstall still removes the unit, edited or not"
|
|
assert "systemctl disable --now dashboards-agent" in _systemctl_calls(call_log)[-3:]
|
|
|
|
|
|
def test_the_guard_sits_between_the_render_and_the_unit_write() -> None:
|
|
"""Text pin for the ordering the behaviour depends on.
|
|
|
|
The comparison must read the *rendered* unit (every flag, bundle and capability decision
|
|
applied) and must come before the write, the `daemon-reload` and the `enable`; a `cat >
|
|
"$UNIT" <<UNIT_FILE` would put the clobber back while still passing every behavioural test
|
|
above on a host with no installed unit.
|
|
"""
|
|
text = INSTALLER.read_text()
|
|
assert "do_install() {" in text, "the install body is still the function that writes the unit"
|
|
# Scoped to do_install: `uninstall()` also reloads the daemon, and an unscoped index would
|
|
# compare offsets across two different functions.
|
|
body = text.split("do_install() {", 1)[1]
|
|
render = body.index('cat > "$TMP_UNIT" <<UNIT_FILE')
|
|
guard = body.index('! cmp -s "$UNIT" "$TMP_UNIT"')
|
|
write = body.index('cat "$TMP_UNIT" > "$UNIT"')
|
|
reload = body.index("systemctl daemon-reload")
|
|
enable = body.index('systemctl enable --now "$SERVICE"')
|
|
assert render < guard < write < reload < enable
|
|
assert 'cat > "$UNIT" <<' not in text, "the template may only write $UNIT after comparing it"
|
|
assert 'if [ "$FORCE" -eq 1 ]; then' in text, "--force is the guard's own opt-in branch"
|
|
assert "rm -f \"$TMP_UNIT\"" in text, "the rendered temp file is cleaned up on every path"
|
|
assert 'mktemp "${TMPDIR:-/tmp}/dashboards-agent-unit.XXXXXX"' in text, (
|
|
"the diff's +++ side must name the generated unit, not an anonymous temp file"
|
|
)
|
|
|
|
|
|
#: The `--force` help line's sentence (phase 17 completion criterion: the flags are learnable
|
|
#: from `--help`, and the diff-first promise is the reason an operator trusts `--force`).
|
|
FORCE_HELP_SENTENCE = "overwrite an existing unit even if the generated unit differs"
|
|
|
|
|
|
def test_help_documents_force_and_keeps_the_flag_block_aligned() -> None:
|
|
stdout = _run([BASH, str(INSTALLER), "--help"]).stdout
|
|
lines = [line for line in stdout.splitlines() if FORCE_HELP_SENTENCE in line]
|
|
assert len(lines) == 1, f"exactly one --force flag line is expected, got {lines}"
|
|
assert lines[0].lstrip().startswith("--force")
|
|
assert "(prints the" in lines[0] and "diff first)" in stdout
|
|
# One column for every flag's description: the block is a table, and `--force` landing at
|
|
# its own column is how `--help` stays readable as a reference rather than a paragraph.
|
|
as_root = next(line for line in stdout.splitlines() if line.lstrip().startswith("--as-root"))
|
|
assert lines[0].index(FORCE_HELP_SENTENCE) == as_root.index("run the service as root")
|
|
|
|
|
|
# --- the certificate bundle (phase 15 task 06) --------------------------------------------
|
|
|
|
#: Where the installed bundle lives — one constant in the script, pinned here so a rename
|
|
#: fails a test instead of stranding every already-written unit file.
|
|
CERT_FILE_PATH = "/etc/dashboards-agent.certs"
|
|
|
|
|
|
def _fake_bundle_text(revision: str = "3") -> str:
|
|
"""A syntactically real bundle: agent cert, agent key, root CA, and the rev line.
|
|
|
|
Shape only. The installer's validator greps PEM blocks by text exactly like
|
|
`resolve_bundle` does (a monitored host has no `cryptography`), so a well-formed *fake*
|
|
is all an unprivileged validation run needs; the functional probe tests below use a
|
|
real bundle minted by `dashboards.certs`.
|
|
"""
|
|
|
|
def block(kind: str, body: str) -> str:
|
|
return f"-----BEGIN {kind}-----\n{body}\n-----END {kind}-----\n"
|
|
|
|
return (
|
|
f"# BUNDLE-REV {revision}\n"
|
|
+ block("CERTIFICATE", "AGENTCERT")
|
|
+ block("PRIVATE KEY", "AGENTKEY")
|
|
+ block("CERTIFICATE", "ROOTCA")
|
|
)
|
|
|
|
|
|
def _installer_status(stdout: str) -> str:
|
|
"""The line `status()` prints as `health: …`, parsed as JSON when it is JSON."""
|
|
for line in stdout.splitlines():
|
|
if line.startswith("health: "):
|
|
body = line.removeprefix("health: ").strip()
|
|
try:
|
|
return str(json.loads(body)["ok"])
|
|
except (json.JSONDecodeError, KeyError):
|
|
return body
|
|
pytest.fail(f"the probe printed no health line:\n{stdout}")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("shape", "reason"),
|
|
[
|
|
("missing", "cannot be read"),
|
|
("empty", "is empty"),
|
|
("no-revision", "BUNDLE-REV"),
|
|
("two-keys", "the agent certificate, the agent key and the root CA"),
|
|
],
|
|
)
|
|
def test_a_bad_cert_bundle_is_refused_with_its_reason_before_anything_else(
|
|
tmp_path: Path, shape: str, reason: str
|
|
) -> None:
|
|
"""Completion criterion: `--cert-bundle /nonexistent` refuses with the reason, touches nothing.
|
|
|
|
The validation runs *before* `need_root`, so the failure names the file rather than
|
|
sending the operator to add a sudoers line for a typo in the flag's argument — and
|
|
nothing after the validation (user, files, unit) can have run.
|
|
"""
|
|
if shape == "missing":
|
|
path = tmp_path / "absent.bundle"
|
|
elif shape == "empty":
|
|
path = tmp_path / "empty.bundle"
|
|
path.write_text("\n", encoding="ascii")
|
|
elif shape == "no-revision":
|
|
path = tmp_path / "revless.bundle"
|
|
path.write_text(_fake_bundle_text().replace("# BUNDLE-REV 3\n", ""), encoding="ascii")
|
|
else: # a bundle with the root *key* in it is exactly what the shape check refuses
|
|
path = tmp_path / "leaky.bundle"
|
|
extra = "-----BEGIN PRIVATE KEY-----\nROOTKEY\n-----END PRIVATE KEY-----\n"
|
|
path.write_text(_fake_bundle_text() + extra, encoding="ascii")
|
|
result = _installer_without_root("--cert-bundle", str(path))
|
|
assert result.returncode == 2, "the bundle refusal keeps its own exit code (2)"
|
|
assert result.stdout == "", "a refusal goes to stderr, not stdout"
|
|
assert reason in result.stderr
|
|
assert str(path) in result.stderr, "the message names the file the operator passed"
|
|
assert GRANT_PREFIX not in result.stderr, "a bad bundle is not a sudoers problem"
|
|
|
|
|
|
def test_a_well_formed_bundle_passes_validation_and_reaches_the_root_check(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Validation is shape-only and never dials: the next refusal is the usual sudoers one."""
|
|
path = tmp_path / "agent.bundle"
|
|
path.write_text(_fake_bundle_text(), encoding="ascii")
|
|
result = _installer_without_root("--bind", "10.0.0.5", "--cert-bundle", str(path))
|
|
assert result.returncode == 1
|
|
assert _grant_lines(result.stderr), "the bundle validated; what remains is the root boundary"
|
|
|
|
|
|
def test_installer_help_documents_the_cert_bundle_and_its_path() -> None:
|
|
result = _run([BASH, str(INSTALLER), "--help"])
|
|
assert result.returncode == 0
|
|
assert "--cert-bundle" in result.stdout
|
|
assert CERT_FILE_PATH in result.stdout
|
|
|
|
|
|
def test_the_bundle_lands_at_the_token_file_idiom_in_the_script() -> None:
|
|
"""`0640 root:$AGENT_USER` + chown: the agent opens the bundle itself and re-reads it."""
|
|
text = INSTALLER.read_text()
|
|
assert f'CERT_FILE="{CERT_FILE_PATH}"' in text
|
|
assert 'install -m 0640 "$BUNDLE_SRC" "$CERT_FILE"' in text
|
|
assert 'chown root:"$AGENT_USER" "$CERT_FILE"' in text
|
|
# 0600 would fail the unit the way it would fail the token file: the service user opens
|
|
# the bundle at startup and re-reads the denylist live. Pin the reasoning, not just the mode.
|
|
assert "0600 root-owned file would fail the unit" in text
|
|
assert "re-reads it when its stamp changes" in text
|
|
|
|
|
|
def test_the_tls_unit_lines_come_from_one_bundle_branch() -> None:
|
|
"""Flag + RuntimeDirectory are emitted together, or the unit is a token-mode unit."""
|
|
text = INSTALLER.read_text()
|
|
match = re.search(r'\n if \[ -f "\$CERT_FILE" \]; then(.*?)\n fi\n', text, re.DOTALL)
|
|
assert match, "the TLS unit lines must come from one `if [ -f \"$CERT_FILE\" ]` branch"
|
|
block = match.group(1)
|
|
assert 'TLS_EXEC_ARGS=" --cert-bundle $CERT_FILE"' in block
|
|
assert "RuntimeDirectory=dashboards-agent" in block
|
|
assert "${TLS_EXEC_ARGS}" in text, "ExecStart must carry the flag from that branch"
|
|
|
|
|
|
def test_status_probes_tls_with_the_bundle_ca_and_no_client_cert() -> None:
|
|
"""The status branch: unit says --cert-bundle → python3 ssl probe, never openssl, no cert."""
|
|
status_body = INSTALLER.read_text().split("status() {", 1)[1].split("\n}", 1)[0]
|
|
assert "grep -q -- '--cert-bundle'" in status_body
|
|
assert "PROTOCOL_TLS_CLIENT" in status_body
|
|
assert "load_verify_locations" in status_body
|
|
assert "https://" in status_body
|
|
assert "load_cert_chain" not in status_body, "the installer holds no client certificate"
|
|
assert 'http://$BIND:$PORT/health' in status_body, "token mode keeps the plain probe"
|
|
assert "openssl" not in INSTALLER.read_text(), "no new binary dependency on a monitored host"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_bundle_install_lands_the_file_the_unit_flag_and_the_runtime_directory(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The install body with a bundle: byte-identical file at 0640, TLS unit, https hint."""
|
|
bundle = tmp_path / "agent.bundle"
|
|
bundle.write_text(_fake_bundle_text(), encoding="ascii")
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--bind", "10.0.0.5", "--port", "9120", "--cert-bundle", str(bundle)],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
|
|
cert_path = tmp_path / "target" / "certs"
|
|
assert cert_path.is_file(), "install -m never landed the bundle"
|
|
assert cert_path.read_text(encoding="ascii") == _fake_bundle_text(), (
|
|
"the installed bundle must be byte-identical to the downloaded one"
|
|
)
|
|
mode = stat.S_IMODE(cert_path.stat().st_mode)
|
|
assert mode == 0o640, f"bundle mode is {oct(mode)}, expected 0o640 (group = the agent user)"
|
|
assert not mode & 0o007, "the bundle holds the agent's key: never world-readable"
|
|
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert f"--cert-bundle {cert_path}" in unit
|
|
assert "RuntimeDirectory=dashboards-agent" in unit, (
|
|
"ProtectSystem=strict leaves the agent no writable file but this, and the bundle "
|
|
"unpacks its PEM blocks there"
|
|
)
|
|
assert "--token-file" in unit, "the dual-mode line stays — rollout is host by host"
|
|
assert 'HOST_TARGETS="hostname=https://10.0.0.5:9120"' in result.stdout, (
|
|
"a TLS install points the dashboard at https, with no token in the URL"
|
|
)
|
|
|
|
calls = call_log.read_text()
|
|
assert calls.count("chown root:dashboards-agent") == 2, "token and bundle both chowned"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_an_unrelated_rerun_reuses_the_installed_bundle(tmp_path: Path) -> None:
|
|
"""The token-reuse idiom: a re-run without the flag neither rewrites nor drops the bundle."""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
target = tmp_path / "target"
|
|
target.mkdir(parents=True)
|
|
cert_path = target / "certs"
|
|
cert_path.write_text(_fake_bundle_text(revision="7"), encoding="ascii")
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--bind", "10.0.0.5"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"re-run failed:\n{result.stdout}\n{result.stderr}"
|
|
assert cert_path.read_text(encoding="ascii") == _fake_bundle_text(revision="7")
|
|
assert "reusing the installed certificate bundle" in result.stdout
|
|
unit = (target / "dashboards-agent.service").read_text()
|
|
assert f"--cert-bundle {cert_path}" in unit, "an installed bundle keeps the unit in TLS mode"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_uninstall_removes_the_bundle_with_the_token(tmp_path: Path) -> None:
|
|
"""An uninstalled agent leaves no private key behind."""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
target = tmp_path / "target"
|
|
(target / "lib").mkdir(parents=True)
|
|
leftovers = {
|
|
target / "token": "tok\n",
|
|
target / "certs": _fake_bundle_text(),
|
|
target / "lib" / "host_agent.py": "# stub\n",
|
|
target / "dashboards-agent.service": "[Unit]\n",
|
|
}
|
|
for path, body in leftovers.items():
|
|
path.write_text(body, encoding="ascii")
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--uninstall"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"uninstall failed:\n{result.stdout}\n{result.stderr}"
|
|
for path in leftovers:
|
|
assert not path.exists(), f"uninstall left {path} on disk"
|
|
assert "systemctl disable --now dashboards-agent" in call_log.read_text()
|
|
|
|
|
|
# --- the --status probe, end to end against the real TLS agent -----------------------------
|
|
|
|
|
|
class DictMeta:
|
|
"""The three-method `certs.MetaStore` slice, backed by a dict (the `test_auth.py` idiom)."""
|
|
|
|
def __init__(self) -> None:
|
|
self.rows: dict[str, str] = {}
|
|
|
|
def get_meta(self, key: str) -> str | None:
|
|
return self.rows.get(key)
|
|
|
|
def set_meta(self, key: str, value: str) -> None:
|
|
self.rows[key] = value
|
|
|
|
def delete_meta(self, key: str) -> int:
|
|
return 1 if self.rows.pop(key, None) is not None else 0
|
|
|
|
|
|
def _mint_bundle() -> str:
|
|
"""A real bundle from the dashboard's own CA — the artifact the operator downloads."""
|
|
store = DictMeta()
|
|
state = certs.ensure_certs(store)
|
|
state = certs.issue_agent_cert(store, state, "localhost", ["127.0.0.1", "localhost"])
|
|
entry = state.agent("localhost")
|
|
assert entry is not None
|
|
return entry.bundle
|
|
|
|
|
|
def _serve_tls_agent(host_agent: Any, bundle_text: str, where: Path) -> tuple[Any, int]:
|
|
"""The real agent handler on a TLS-wrapped loopback port, exactly as the unit runs it."""
|
|
path = where / "installed.certs"
|
|
path.write_text(bundle_text, encoding="ascii")
|
|
bundle, error = host_agent.TlsBundle.load(str(path))
|
|
assert error is None and bundle is not None
|
|
host_agent.Handler.collector = host_agent.Collector("/", smart=False)
|
|
host_agent.Handler.token = None
|
|
host_agent.Handler.bundle = bundle
|
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), host_agent.Handler)
|
|
httpd.socket = bundle.context.wrap_socket(httpd.socket, server_side=True)
|
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
return httpd, httpd.server_address[1]
|
|
|
|
|
|
def _run_status(script: Path, port: int, env: dict[str, str]) -> str:
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--status", "--bind", "127.0.0.1", "--port", str(port)],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, f"--status exited {result.returncode}:\n{result.stderr}"
|
|
return result.stdout
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_tls_status_probe_reads_health_with_the_ca_and_no_client_cert(
|
|
tmp_path: Path, host_agent_module: Any
|
|
) -> None:
|
|
"""The task-02 ASSUMPTION, proven through the installer: CERT_OPTIONAL + the /health
|
|
exemption let a probe that holds only the bundle's CA read /health on a TLS agent."""
|
|
bundle = _mint_bundle()
|
|
httpd, port = _serve_tls_agent(host_agent_module, bundle, tmp_path)
|
|
try:
|
|
script, _call_log, env = _sandbox_installer(tmp_path / "installer")
|
|
target = tmp_path / "installer" / "target"
|
|
target.mkdir(parents=True)
|
|
(target / "dashboards-agent.service").write_text(
|
|
f"ExecStart=python3 host_agent.py --cert-bundle {target / 'certs'}\n", encoding="ascii"
|
|
)
|
|
(target / "certs").write_text(bundle, encoding="ascii")
|
|
assert _installer_status(_run_status(script, port, env)) == "True"
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
host_agent_module.Handler.bundle = None
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_tls_status_probe_verifies_the_chain_and_says_no_to_another_authority(
|
|
tmp_path: Path, host_agent_module: Any
|
|
) -> None:
|
|
"""The probe loads the CA for a reason: a server signed by a different root is not health."""
|
|
httpd, port = _serve_tls_agent(host_agent_module, _mint_bundle(), tmp_path)
|
|
try:
|
|
script, _call_log, env = _sandbox_installer(tmp_path / "installer")
|
|
target = tmp_path / "installer" / "target"
|
|
target.mkdir(parents=True)
|
|
(target / "dashboards-agent.service").write_text(
|
|
f"ExecStart=python3 host_agent.py --cert-bundle {target / 'certs'}\n", encoding="ascii"
|
|
)
|
|
(target / "certs").write_text(_mint_bundle(), encoding="ascii") # a *different* CA
|
|
stdout = _run_status(script, port, env)
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
host_agent_module.Handler.bundle = None
|
|
line = _installer_status(stdout)
|
|
assert line != "True" and line.startswith("(no answer"), f"the probe must refuse: {line}"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_tls_status_probe_names_the_bundle_when_it_cannot_read_it(tmp_path: Path) -> None:
|
|
"""The bundle is 0640 root:agent — an unprivileged --status cannot read it, and says so."""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
target = tmp_path / "target"
|
|
target.mkdir(parents=True)
|
|
(target / "dashboards-agent.service").write_text(
|
|
f"ExecStart=python3 host_agent.py --cert-bundle {target / 'certs'}\n", encoding="ascii"
|
|
)
|
|
cert = target / "certs"
|
|
cert.write_text(_fake_bundle_text(), encoding="ascii")
|
|
cert.chmod(0o000)
|
|
line = _installer_status(_run_status(script, 1, env))
|
|
assert "cannot read" in line and "run --status as root" in line, line
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("curl") is None, reason="curl is not installed on this box")
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_token_mode_status_probe_stays_plain_http(tmp_path: Path) -> None:
|
|
"""The other branch: no bundle in the unit → the unchanged plain probe, not https."""
|
|
script, _call_log, env = _sandbox_installer(tmp_path)
|
|
target = tmp_path / "target"
|
|
target.mkdir(parents=True)
|
|
(target / "dashboards-agent.service").write_text(
|
|
"ExecStart=python3 host_agent.py --token-file /etc/dashboards-agent.token\n",
|
|
encoding="ascii",
|
|
)
|
|
result = subprocess.run(
|
|
[BASH, str(script), "--status", "--bind", "127.0.0.1", "--port", "1"],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0
|
|
assert _installer_status(result.stdout).startswith("(no answer"), (
|
|
"token mode must probe plain http — nothing listens here, and the refusal is the point"
|
|
)
|
|
|
|
|
|
# --- scripts/dev-certs-export.py (phase 24 task 01) ---------------------------------------
|
|
|
|
#: The dev exporter, and the dev server that calls it. Both are **run** below and **read**
|
|
#: below: the bytes a host installs are the contract of the first, and the composition of the
|
|
#: second (export *after* the readiness probe, failure *as* a warning) is the contract of the
|
|
#: dev loop that has to stop producing `http://` agents.
|
|
EXPORTER = ROOT / "scripts" / "dev-certs-export.py"
|
|
DEV_SERVER = ROOT / "scripts" / "dev-server.sh"
|
|
|
|
#: The invariant-3 pair, as two configured hosts: one with a bundle in `meta`, one without.
|
|
ISSUED_HOST = "issuedhost"
|
|
UNISSUED_HOST = "unissuedhost"
|
|
|
|
|
|
def _load_exporter() -> Any:
|
|
"""The exporter as a module — it is a script, not a package member, so import it by path."""
|
|
spec = importlib.util.spec_from_file_location("dev_certs_export", EXPORTER)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@contextmanager
|
|
def served_dashboard(tmp_path: Path) -> Iterator[tuple[str, SampleStore]]:
|
|
"""A real `create_app(...)` on a real socket, with one host's bundle already issued.
|
|
|
|
uvicorn in a thread rather than `TestClient`, because the unit under test is an **HTTP
|
|
client**: the exporter signs in over the public route and reads the session token back out
|
|
of a `Set-Cookie` header, and a transport-level test client can pass that without ever
|
|
serialising a header. Port 0, so nothing here can collide with a port another test bound.
|
|
|
|
The store is injected and closed here, and the two `HOST_TARGETS` entries point at a port
|
|
nothing listens on: the app under test must never dial, and a dead target is what keeps
|
|
this hermetic. Every other source is switched off explicitly, so an operator running the
|
|
suite with `.env.dev` sourced still gets a test of two hosts and no fleet.
|
|
"""
|
|
db = tmp_path / "export.sqlite3"
|
|
store = SampleStore(db)
|
|
state = certs.ensure_certs(store)
|
|
certs.issue_agent_cert(store, state, ISSUED_HOST, san=[ISSUED_HOST])
|
|
settings = Settings(
|
|
db_path=str(db),
|
|
admin_password=ADMIN_PASSWORD,
|
|
pbkdf2_iterations=FAST_PBKDF2,
|
|
host_targets=(Target(name=ISSUED_HOST, url="http://127.0.0.1:1"),
|
|
Target(name=UNISSUED_HOST, url="http://127.0.0.1:1")),
|
|
host_interval=3600,
|
|
host_timeout=0.2,
|
|
gufo_url="",
|
|
vllm_url="",
|
|
nut_host="",
|
|
)
|
|
server = uvicorn.Server(uvicorn.Config(create_app(settings, store), host="127.0.0.1",
|
|
port=0, log_level="error", lifespan="on"))
|
|
thread = threading.Thread(target=server.run, daemon=True, name="dev-certs-export-test")
|
|
thread.start()
|
|
try:
|
|
for _ in range(2_000):
|
|
if server.started:
|
|
break
|
|
time.sleep(0.01)
|
|
assert server.started, "the test dashboard never came up (see the captured stderr)"
|
|
port = int(server.servers[0].sockets[0].getsockname()[1])
|
|
yield f"http://127.0.0.1:{port}", store
|
|
finally:
|
|
server.should_exit = True
|
|
thread.join(timeout=60)
|
|
store.close()
|
|
|
|
|
|
def _export(base: str, out_dir: Path, names: list[str], *,
|
|
password: str | None = ADMIN_PASSWORD) -> subprocess.CompletedProcess[str]:
|
|
"""Run the exporter exactly the way `dev-server.sh` does, as a subprocess."""
|
|
args = [sys.executable, str(EXPORTER), "--url", base, "--out-dir", str(out_dir)]
|
|
if password is not None:
|
|
args += ["--password", password]
|
|
if names:
|
|
args += ["--names", *names]
|
|
return _run(args)
|
|
|
|
|
|
def _served_bundle(base: str, name: str) -> bytes:
|
|
"""What `GET /api/certs/<name>/bundle` answers, fetched the way the exporter fetches it."""
|
|
with httpx.Client(base_url=base, timeout=10.0) as client:
|
|
login = client.post("/api/auth/login", json={"password": ADMIN_PASSWORD})
|
|
login.raise_for_status()
|
|
token = login.cookies[auth.SESSION_COOKIE]
|
|
answer = client.get(f"/api/certs/{name}/bundle",
|
|
headers={"authorization": f"Bearer {token}"})
|
|
answer.raise_for_status()
|
|
return answer.content
|
|
|
|
|
|
def test_the_export_writes_the_issued_bundle_byte_identical_at_0600(tmp_path: Path) -> None:
|
|
"""The task's headline: the file is the stored bundle, at a mode that keeps it private.
|
|
|
|
Byte-identity is compared against the **route** rather than the `meta` row because the
|
|
question this answers is "is the file I am about to `scp` the file the dashboard issued?",
|
|
and a bundle that re-signed between the two would make that question unanswerable.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
out = tmp_path / "certs"
|
|
result = _export(base, out, [ISSUED_HOST])
|
|
assert result.returncode == 0, result.stderr
|
|
path = out / f"{ISSUED_HOST}.bundle"
|
|
assert path.is_file(), f"no bundle exported; stdout was:\n{result.stdout}"
|
|
assert stat.S_IMODE(path.stat().st_mode) == 0o600, (
|
|
"a bundle carries a private key; 0600 is its birth mode and stays its mode"
|
|
)
|
|
assert path.read_bytes() == _served_bundle(base, ISSUED_HOST)
|
|
assert ISSUED_HOST in result.stdout and "0600" in result.stdout
|
|
|
|
|
|
def test_an_unissued_host_writes_no_file_and_names_the_certs_route(tmp_path: Path) -> None:
|
|
"""`issued: false` is a fact, not a failure: one line, no file, exit 0 (invariant 3)."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
out = tmp_path / "certs"
|
|
result = _export(base, out, [ISSUED_HOST, UNISSUED_HOST])
|
|
assert result.returncode == 0, result.stderr
|
|
assert not (out / f"{UNISSUED_HOST}.bundle").exists()
|
|
note = next(line for line in result.stdout.splitlines() if UNISSUED_HOST in line)
|
|
assert "no bundle issued" in note and "/certs" in note, note
|
|
assert "!" not in note, f"an unissued host must not read as a failure: {note}"
|
|
|
|
|
|
def test_a_name_outside_host_targets_is_a_note_and_not_a_crash(tmp_path: Path) -> None:
|
|
"""The `{"configured": false}` arm: impossible from `HOST_TARGETS`, still never a crash."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
out = tmp_path / "certs"
|
|
result = _export(base, out, ["nosuchhost"])
|
|
assert result.returncode == 0, result.stderr
|
|
assert not out.exists(), "nothing was issued, so nothing — not even a directory"
|
|
assert "HOST_TARGETS" in result.stdout
|
|
|
|
|
|
def test_a_wrong_password_exits_non_zero_and_writes_nothing(tmp_path: Path) -> None:
|
|
"""A dev server that cannot be signed in to says so loudly (and the CA stays untouched)."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
out = tmp_path / "certs"
|
|
result = _export(base, out, [ISSUED_HOST], password="definitely-not-the-password")
|
|
assert result.returncode == 2, f"a sign-in failure must not look like success: {result}"
|
|
assert not out.exists()
|
|
assert "sign-in" in result.stderr.lower()
|
|
|
|
|
|
def test_no_password_at_all_is_a_refusal_and_not_a_hang(tmp_path: Path) -> None:
|
|
"""`--password` is optional only because `$DASH_ADMIN_PASSWORD` is the dev source of it."""
|
|
env = {key: value for key, value in os.environ.items() if key != "DASH_ADMIN_PASSWORD"}
|
|
result = subprocess.run(
|
|
[sys.executable, str(EXPORTER), "--url", "http://127.0.0.1:1",
|
|
"--out-dir", str(tmp_path / "certs"), "--names", ISSUED_HOST],
|
|
capture_output=True, text=True, check=False, env=env, timeout=60,
|
|
)
|
|
assert result.returncode == 2
|
|
assert "DASH_ADMIN_PASSWORD" in result.stderr
|
|
assert not (tmp_path / "certs").exists()
|
|
|
|
|
|
def test_no_names_at_all_is_a_quiet_zero(tmp_path: Path) -> None:
|
|
"""A dashboard with no `HOST_TARGETS` has nothing to export and is not broken."""
|
|
result = _export("http://127.0.0.1:1", tmp_path / "certs", [])
|
|
assert result.returncode == 0, result.stderr
|
|
assert "nothing to export" in result.stdout
|
|
assert not (tmp_path / "certs").exists()
|
|
|
|
|
|
def test_a_second_export_is_idempotent_and_says_unchanged(tmp_path: Path) -> None:
|
|
"""`restart` runs this every time; a dev loop that rewrote keys on every boot would be noise."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
out = tmp_path / "certs"
|
|
first = _export(base, out, [ISSUED_HOST])
|
|
assert first.returncode == 0, first.stderr
|
|
path = out / f"{ISSUED_HOST}.bundle"
|
|
before = path.read_bytes()
|
|
path.chmod(0o644) # somebody loosened it; the export is what puts the mode back
|
|
second = _export(base, out, [ISSUED_HOST])
|
|
assert second.returncode == 0, second.stderr
|
|
assert path.read_bytes() == before, "the stored bundle must not move between exports"
|
|
assert stat.S_IMODE(path.stat().st_mode) == 0o600, "an existing file is re-chmodded"
|
|
assert "unchanged" in second.stdout
|
|
|
|
|
|
def test_the_export_reads_the_authority_and_never_mints_anything(tmp_path: Path) -> None:
|
|
"""The difference between `read_state` and `ensure_certs`, pinned as an observable.
|
|
|
|
"The dev server should generate certs" is the TODO's wording; the safe reading is *export
|
|
what the first boot minted*. If this script ever issued, a stray `restart` would rotate a
|
|
fleet's identities — so the serials, the revisions and the root all have to be the same
|
|
bytes after the run as before it.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, store):
|
|
def fingerprint() -> tuple[str, ...]:
|
|
listed = certs.read_state(store)
|
|
rows = tuple(f"{e.name}:{e.serial}:{e.bundle_rev}:{e.status}"
|
|
for e in certs.read_agents(store))
|
|
return (f"root:{listed.root_serial}", *rows)
|
|
|
|
before = fingerprint()
|
|
assert _export(base, tmp_path / "certs",
|
|
[ISSUED_HOST, UNISSUED_HOST, "nosuchhost"]).returncode == 0
|
|
assert fingerprint() == before, "the exporter changed the certificate authority"
|
|
|
|
|
|
def test_the_export_prints_no_key_material_and_no_password(tmp_path: Path) -> None:
|
|
"""Invariant 6, on the one path that holds both a session token and a private key."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
result = _export(base, tmp_path / "certs", [ISSUED_HOST, UNISSUED_HOST])
|
|
printed = result.stdout + result.stderr
|
|
assert "BEGIN" not in printed, "a PEM block reached the terminal (and the dev log)"
|
|
assert ADMIN_PASSWORD not in printed
|
|
assert auth.SESSION_COOKIE not in printed, "the token itself must never be printed"
|
|
|
|
|
|
def test_the_exporter_splits_names_the_way_host_targets_looks() -> None:
|
|
"""One argument with spaces in it, three arguments, commas: all the same list."""
|
|
module = _load_exporter()
|
|
assert module.split_names(["alpha beta", "gamma,delta"]) == ["alpha", "beta", "gamma", "delta"]
|
|
assert module.split_names([" alpha,,beta "]) == ["alpha", "beta"]
|
|
assert module.split_names([]) == []
|
|
assert module.split_names(None) == []
|
|
|
|
|
|
def test_the_exporter_reads_the_cookie_the_app_sets() -> None:
|
|
"""`auth.SESSION_COOKIE` is imported, not restated — a rename must not silently break dev."""
|
|
module = _load_exporter()
|
|
assert module.SESSION_COOKIE == auth.SESSION_COOKIE
|
|
|
|
|
|
def test_write_bundle_refuses_a_name_that_would_leave_the_directory(tmp_path: Path) -> None:
|
|
"""The route gates names long before this, but this is the function that picks a path."""
|
|
module = _load_exporter()
|
|
for bad in ("../escape", "nested/x", "/absolute", "", "."):
|
|
with pytest.raises(module.ExportError):
|
|
module.write_bundle(tmp_path, bad, b"secret")
|
|
assert list(tmp_path.iterdir()) == []
|
|
|
|
|
|
def test_the_bundles_only_ever_land_under_the_gitignored_data_tree() -> None:
|
|
"""No key, bundle or PEM outside `data/`: the exporter's default and the caller's flag."""
|
|
module = _load_exporter()
|
|
assert module.DEFAULT_OUT_DIR == ROOT / "data" / "certs"
|
|
gitignore = (ROOT / ".gitignore").read_text()
|
|
assert re.search(r"^data/$", gitignore, re.MULTILINE), "data/ is the gitignored tree"
|
|
dev_server = DEV_SERVER.read_text()
|
|
assignments = [line for line in dev_server.splitlines() if line.startswith("CERTDIR=")]
|
|
assert assignments == ['CERTDIR="$ROOT/data/certs"'], (
|
|
f"the dev server must name exactly one export dir, under data/: {assignments}"
|
|
)
|
|
assert '--out-dir "$CERTDIR"' in dev_server, "the export must be told where it writes"
|
|
|
|
|
|
def _shell_function(text: str, name: str) -> str:
|
|
"""One function's body, so a pin can talk about *that* block and not the whole file."""
|
|
match = re.search(rf"^{re.escape(name)}\(\) \{{$", text, re.MULTILINE)
|
|
assert match is not None, f"{name}() is no longer a plain function in dev-server.sh"
|
|
body = text[match.end():]
|
|
end = body.index("\n}")
|
|
return body[:end]
|
|
|
|
|
|
def test_dev_server_exports_the_bundles_after_the_readiness_probe() -> None:
|
|
"""The order is the whole safety of the dev loop: no bundle export against a dead server."""
|
|
text = DEV_SERVER.read_text()
|
|
probe = text.index("curl -sf -o /dev/null")
|
|
call = text.index('export_bundles "$PORT"')
|
|
assert probe < call, "the export must run only once /api/health has answered"
|
|
assert "dev-certs-export.py" in _shell_function(text, "export_bundles")
|
|
body = _shell_function(text, "export_bundles")
|
|
assert "http://127.0.0.1:${port}" in body, "the exporter dials the dev server and nothing else"
|
|
# The names are HOST_TARGETS' names, split by the caller: the exporter re-parses no config.
|
|
assert "${entry%%=*}" in body
|
|
|
|
|
|
def test_a_failed_bundle_export_never_fails_the_dev_server_start() -> None:
|
|
"""A dev-convenience failure is a warning line: the readiness probe already proved the app."""
|
|
body = _shell_function(DEV_SERVER.read_text(), "export_bundles")
|
|
assert "|| echo" in body, "a failed export must print a warning rather than propagate"
|
|
for escape in ("return 1", "exit 1", "set -e"):
|
|
assert escape not in body, f"the export can now abort `start` ({escape})"
|
|
|
|
|
|
def test_dev_server_status_prints_the_exported_bundles_block() -> None:
|
|
"""`status` answers "is this host's bundle there yet?" without a signed-in curl."""
|
|
text = DEV_SERVER.read_text()
|
|
body = _shell_function(text, "bundles")
|
|
assert "bundles exported" in body and ".bundle" in body
|
|
assert "mode" in body, "the block shows the 0600, which is the thing worth checking"
|
|
assert re.search(r"^ bundles ;;$", text, re.MULTILINE), "`status` must print the block"
|
|
assert "bundles)" in text, "`bundles` is a subcommand of its own"
|
|
|
|
|
|
def test_a_dashboard_that_never_answers_is_named_rather_than_guessed(tmp_path: Path) -> None:
|
|
"""Nothing listened: the line says which URL, the exit is non-zero, nothing is written."""
|
|
result = _export("http://127.0.0.1:1", tmp_path / "certs", [ISSUED_HOST])
|
|
assert result.returncode == 2
|
|
assert "no dashboard answered" in result.stderr and "127.0.0.1:1" in result.stderr
|
|
assert not (tmp_path / "certs").exists()
|
|
|
|
|
|
def test_an_unexpected_answer_on_the_bundle_route_is_not_read_as_unissued(tmp_path: Path) -> None:
|
|
"""Only the two 404 shapes are notes. A 401 means the caller is not authorised, which is news."""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
module = _load_exporter()
|
|
with pytest.raises(module.ExportError) as raised:
|
|
module.fetch_bundle(base, ISSUED_HOST, "not-a-session-token")
|
|
assert "401" in str(raised.value)
|
|
|
|
|
|
def test_one_host_the_route_refuses_never_stops_the_others(tmp_path: Any,
|
|
monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The aggregation `main` owns: every name is attempted, and a failure moves the exit code."""
|
|
module = _load_exporter()
|
|
attempted: list[str] = []
|
|
|
|
def fake_fetch(base: str, name: str, token: str) -> bytes | str:
|
|
attempted.append(name)
|
|
if name == "bad":
|
|
raise module.ExportError("bad: GET /api/certs/bad/bundle answered 500: boom")
|
|
return b"bundle-bytes"
|
|
|
|
monkeypatch.setattr(module, "sign_in", lambda base, password: "a-token")
|
|
monkeypatch.setattr(module, "fetch_bundle", fake_fetch)
|
|
code = module.main(["--url", "http://127.0.0.1:1", "--password", "p",
|
|
"--names", "good bad later", "--out-dir", str(tmp_path / "certs")])
|
|
assert attempted == ["good", "bad", "later"], "one refused host must not end the run"
|
|
assert code == 1, "a bundle nobody exported is a host that stays on http:// — say so"
|
|
assert (tmp_path / "certs" / "good.bundle").is_file()
|
|
assert not (tmp_path / "certs" / "bad.bundle").exists()
|
|
|
|
|
|
# --- fetching the bundle during registration (phase 24 task 02) ---------------------------
|
|
|
|
#: `install-agent.sh --fetch-bundle <url> --cert-name <name> --session <token>`: the install
|
|
#: run pulls its own bundle from the dashboard, so a host is never registered onto a token URL
|
|
#: because an operator forgot a `curl`. The contract has three parts — the bytes that land are
|
|
#: the bytes the route serves, a refusal writes *nothing*, and the session token is a fetch
|
|
#: credential that appears in no artefact the run leaves behind.
|
|
FETCH_UNISSUED = "no bundle issued for"
|
|
FETCH_ALL_OR_NOTHING = "--fetch-bundle, --cert-name and --session go together"
|
|
FETCH_TWO_SOURCES = "two bundle sources"
|
|
FETCH_MODE_REFUSAL = "--fetch-bundle needs an install"
|
|
|
|
|
|
def _sign_in(base: str) -> str:
|
|
"""A real session token for the served dashboard — the credential `--session` carries."""
|
|
with httpx.Client(base_url=base, timeout=10.0) as client:
|
|
login = client.post("/api/auth/login", json={"password": ADMIN_PASSWORD})
|
|
login.raise_for_status()
|
|
return login.cookies[auth.SESSION_COOKIE]
|
|
|
|
|
|
def _fetch_run(tmp_path: Path, *flags: str,
|
|
) -> tuple[subprocess.CompletedProcess[str], Path, Path]:
|
|
"""One sandboxed install with a sandbox `TMPDIR`, so the fetched temp file is observable.
|
|
|
|
The fetch writes to `${TMPDIR:-/tmp}`, so pointing TMPDIR into the sandbox is what turns
|
|
"the fetched private key never stays behind" into an assertion instead of a hope — and it
|
|
catches the temp file on the refusal paths too, where nothing else was written to look at.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
tmpdir = tmp_path / "tmp"
|
|
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
result = _install(script, {**env, "TMPDIR": str(tmpdir)}, *flags)
|
|
return result, tmpdir, call_log
|
|
|
|
|
|
def _fetch(base: str, name: str, token: str) -> list[str]:
|
|
"""The three fetch flags, spelled once."""
|
|
return ["--fetch-bundle", base, "--cert-name", name, "--session", token]
|
|
|
|
|
|
@contextmanager
|
|
def served_body(*, status: int = 200, body: bytes = b"wrong",
|
|
media_type: str = "text/html") -> Iterator[str]:
|
|
"""A loopback port that answers every GET with one canned body.
|
|
|
|
Only for the shapes the real route cannot produce: a proxy's 200 error page, a 200 with no
|
|
bytes at all. The issued host, the unissued host and a refused session are all real answers
|
|
from `served_dashboard`, and those are the legs that matter most.
|
|
"""
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self) -> None:
|
|
self.send_response(status)
|
|
self.send_header("content-type", media_type)
|
|
self.send_header("content-length", str(len(body)))
|
|
self.end_headers()
|
|
if body:
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *_args: Any, **_kwargs: Any) -> None:
|
|
pass # the installer's answer is the assertion, not this server's access log
|
|
|
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
try:
|
|
yield f"http://127.0.0.1:{httpd.server_address[1]}"
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_fetched_bundle_installs_byte_identical_at_0640(tmp_path: Path) -> None:
|
|
"""The registration leg: fetch, validate, install, TLS unit — one run, no hand-carried file.
|
|
|
|
Byte-identity is against the same route's answer as task 01's export, because the claim the
|
|
installer makes is the same one: the file on the host *is* the row in `meta`. Everything
|
|
after the fetch is the existing `--cert-bundle` path, so the mode, the unit's TLS lines and
|
|
the https hint are asserted here as unchanged rather than as new.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
token = _sign_in(base)
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", "--port", "9120", *_fetch(base, ISSUED_HOST, token))
|
|
if result.returncode != 0:
|
|
pytest.fail(f"the fetch install failed:\n{result.stdout}\n{result.stderr}")
|
|
|
|
cert_path = tmp_path / "target" / "certs"
|
|
assert cert_path.read_bytes() == _served_bundle(base, ISSUED_HOST)
|
|
mode = stat.S_IMODE(cert_path.stat().st_mode)
|
|
assert mode == 0o640, f"bundle mode is {oct(mode)}, expected 0o640 (group = agent user)"
|
|
|
|
unit = (tmp_path / "target" / "dashboards-agent.service").read_text()
|
|
assert f"--cert-bundle {cert_path}" in unit
|
|
assert "RuntimeDirectory=dashboards-agent" in unit
|
|
assert 'HOST_TARGETS="hostname=https://10.0.0.5:9120"' in result.stdout
|
|
assert f"fetched the certificate bundle for {ISSUED_HOST} from {base}" in result.stdout
|
|
assert "chown root:dashboards-agent" in call_log.read_text()
|
|
assert list(tmpdir.iterdir()) == [], "the fetched private key outlived the install"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_an_unissued_bundle_is_refused_before_anything_is_written(tmp_path: Path) -> None:
|
|
"""`issued: false` is one sentence naming the fix, and the host stays exactly as it was.
|
|
|
|
This is the invariant-3 answer on the install path: the route says "configured, nothing
|
|
issued", and the installer must not read that as a bundle, as a sudoers problem, or as a
|
|
reason to create a service user. The stub log is the proof nothing privileged ran: `id -u`
|
|
is the first privileged call a real install makes, and it is absent.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
token = _sign_in(base)
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", *_fetch(base, UNISSUED_HOST, token))
|
|
assert result.returncode == 2, f"a refused fetch keeps exit 2: {result.returncode}"
|
|
assert result.stdout == "", "a refusal is a failure: nothing on stdout"
|
|
assert FETCH_UNISSUED in result.stderr and UNISSUED_HOST in result.stderr
|
|
assert "/certs" in result.stderr, "the sentence names where the certificate is issued"
|
|
assert GRANT_PREFIX not in result.stderr, "a refusal is not a sudoers problem"
|
|
assert not call_log.exists(), "refused before the root boundary: no privileged command ran"
|
|
assert not (tmp_path / "target").exists(), "a refused fetch writes no files at all"
|
|
assert list(tmpdir.iterdir()) == [], "the temp file is gone even though it was written"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_name_the_dashboard_does_not_dial_is_refused_with_its_own_404(tmp_path: Path) -> None:
|
|
"""`{"configured": false}` is a third fact, and it is not read as "nothing issued".
|
|
|
|
The route refuses a name outside `HOST_TARGETS` before any signing, so a typo in
|
|
`--cert-name` must say *that* — an operator who is told "no bundle issued" would go looking
|
|
for a certificate that was never supposed to exist for a host this dashboard does not dial.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
token = _sign_in(base)
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", *_fetch(base, "nosuchhost", token))
|
|
assert result.returncode == 2
|
|
assert "404" in result.stderr, result.stderr
|
|
assert FETCH_UNISSUED not in result.stderr, "not configured is not the same as unissued"
|
|
assert "HOST_TARGETS" in result.stderr, "the dashboard's own sentence is echoed"
|
|
assert not call_log.exists()
|
|
assert list(tmpdir.iterdir()) == []
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_refused_session_names_the_http_status(tmp_path: Path) -> None:
|
|
"""A stale or wrong session is a 401 on the wire, and the run says 401.
|
|
|
|
Not "no bundle" and not a sudoers hint: the operator's move is a fresh sign-in, and the
|
|
only way they learn that is the status the dashboard actually answered.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", *_fetch(base, ISSUED_HOST, "not-a-session-token"))
|
|
assert result.returncode == 2
|
|
assert "401" in result.stderr, result.stderr
|
|
assert "sign in required" in result.stderr, "the dashboard's own sentence is echoed"
|
|
assert not call_log.exists()
|
|
assert not (tmp_path / "target").exists()
|
|
assert list(tmpdir.iterdir()) == []
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
@pytest.mark.parametrize(
|
|
("status", "body", "reason"),
|
|
[
|
|
(200, b"<html><body>502 from the proxy</body></html>", "# BUNDLE-REV"),
|
|
(200, b"", "empty body"),
|
|
(500, b"upstream exploded", "500"),
|
|
],
|
|
ids=["proxy-error-page", "empty-200", "server-error"],
|
|
)
|
|
def test_a_body_that_is_not_a_bundle_never_becomes_an_install(
|
|
tmp_path: Path, status: int, body: bytes, reason: str
|
|
) -> None:
|
|
"""The shapes the dashboard cannot answer with — and the check that catches each one.
|
|
|
|
A 200 with a proxy's HTML page is the dangerous one: it is a *success* to any client that
|
|
only looks at the status, and installing it produces a unit that exits at startup. It falls
|
|
through to the existing shape check, which is the reason one validator serves both bundle
|
|
sources. The refusal names `--fetch-bundle` (the flag that was used), not the temp file.
|
|
"""
|
|
with served_body(status=status, body=body) as base:
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", *_fetch(base, ISSUED_HOST, "a-token"))
|
|
assert result.returncode == 2, f"{status}/{len(body)} bytes must be refused"
|
|
assert reason in result.stderr, result.stderr
|
|
assert "--fetch-bundle" in result.stderr
|
|
assert not call_log.exists()
|
|
assert not (tmp_path / "target").exists()
|
|
assert list(tmpdir.iterdir()) == []
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_dashboard_that_never_answers_stops_the_fetch(tmp_path: Path) -> None:
|
|
"""Nothing listening on the dashboard URL: the line names it, and no install happens."""
|
|
result, tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5",
|
|
*_fetch("http://127.0.0.1:1", ISSUED_HOST, "a-token"))
|
|
assert result.returncode == 2
|
|
assert "no dashboard answered" in result.stderr and "127.0.0.1:1" in result.stderr
|
|
assert not call_log.exists()
|
|
assert list(tmpdir.iterdir()) == []
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_session_token_appears_in_no_written_artefact(tmp_path: Path) -> None:
|
|
"""Completion criterion: the token is a fetch credential, not a stored one.
|
|
|
|
The test signs in for a real token and then searches **every** file the run could have
|
|
touched plus both output streams. The agent's own bearer token is expected in the token
|
|
file and on stdout — that is a different secret, deliberately printed once — so the pin is
|
|
specifically that this session value is nowhere.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
token = _sign_in(base)
|
|
assert token and token != ADMIN_PASSWORD
|
|
result, _tmpdir, call_log = _fetch_run(
|
|
tmp_path, "--bind", "10.0.0.5", "--port", "9120",
|
|
*_fetch(base, ISSUED_HOST, token))
|
|
assert result.returncode == 0, f"install failed:\n{result.stderr}"
|
|
|
|
where = tmp_path / "target"
|
|
assert token not in (where / "token").read_text(), "the token file is the agent's, not ours"
|
|
assert token not in (where / "certs").read_text(), "a bundle is not a place for a session"
|
|
assert token not in (where / "dashboards-agent.service").read_text()
|
|
assert token not in call_log.read_text(), "a privileged call must not carry it either"
|
|
assert token not in result.stdout + result.stderr
|
|
|
|
hits = [str(path.relative_to(tmp_path)) for path in tmp_path.rglob("*")
|
|
if path.is_file() and token in path.read_text(encoding="utf-8", errors="replace")]
|
|
assert hits == [], f"the session token was written to {hits}"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"flags",
|
|
[
|
|
["--fetch-bundle", "http://127.0.0.1:8090"],
|
|
["--cert-name", "deskwork"],
|
|
["--session", "a-token"],
|
|
["--fetch-bundle", "http://127.0.0.1:8090", "--cert-name", "deskwork"],
|
|
["--cert-name", "deskwork", "--session", "a-token"],
|
|
],
|
|
ids=["url", "name", "token", "url+name", "name+token"],
|
|
)
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_fetch_flags_are_all_or_nothing(tmp_path: Path, flags: list[str]) -> None:
|
|
"""Half a fetch is a refusal, not a guess: no URL, no name, or no token means nothing dials.
|
|
|
|
A URL without a name has no route to fetch, and a name without a token is a 401 the
|
|
operator would have to diagnose from a stack of unrelated output. Both are answered with
|
|
one sentence before the root boundary and before any write — the `--caps` reasoning again.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199", *flags)
|
|
assert result.returncode == 1, "a flag mistake keeps exit 1, not the fetch's 2"
|
|
assert result.stdout == ""
|
|
assert FETCH_ALL_OR_NOTHING in result.stderr
|
|
assert GRANT_PREFIX not in result.stderr, "a flag mistake is not a sudoers problem"
|
|
assert not call_log.exists(), "refused before a single privileged command"
|
|
assert not (tmp_path / "target").exists()
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_two_bundle_sources_are_refused_rather_than_ordered(tmp_path: Path) -> None:
|
|
"""`--fetch-bundle` beside `--cert-bundle` is a contradiction: one install, one answer.
|
|
|
|
"Which one wins" has no good answer here — a stale file and a fresh bundle are two
|
|
different identities, and silently preferring either would install a certificate the
|
|
operator did not mean to install. So the run stops.
|
|
"""
|
|
bundle = tmp_path / "agent.bundle"
|
|
bundle.write_text(_fake_bundle_text(), encoding="ascii")
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--bind", "127.0.0.1", "--port", "9199",
|
|
*_fetch("http://127.0.0.1:8090", "deskwork", "a-token"),
|
|
"--cert-bundle", str(bundle))
|
|
assert result.returncode == 1
|
|
assert FETCH_TWO_SOURCES in result.stderr
|
|
assert GRANT_PREFIX not in result.stderr
|
|
assert not call_log.exists()
|
|
assert not (tmp_path / "target").exists()
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--status", "--uninstall"], ids=["status", "uninstall"])
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_fetching_a_bundle_is_refused_outside_an_install(tmp_path: Path, mode: str) -> None:
|
|
"""The fetch belongs to the run that installs, or a key is fetched to be thrown away.
|
|
|
|
`--status` answers about a port and `--uninstall` removes files; neither has a use for the
|
|
bytes, and a silent "flags ignored" on the uninstall path would leave an operator believing
|
|
a bundle had been refreshed.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, mode,
|
|
*_fetch("http://127.0.0.1:8090", "deskwork", "a-token"))
|
|
assert result.returncode == 1
|
|
assert FETCH_MODE_REFUSAL in result.stderr
|
|
assert not call_log.exists(), "and refused before the mode's own privileged work"
|
|
|
|
|
|
def test_help_teaches_the_registration_fetch() -> None:
|
|
"""The three flags and the recipe are learnable from `--help`, like every other flag here."""
|
|
stdout = _run([BASH, str(INSTALLER), "--help"]).stdout
|
|
assert "--fetch-bundle URL" in stdout
|
|
assert "--cert-name NAME" in stdout
|
|
assert "--session TOKEN" in stdout
|
|
assert "GET <url>/api/certs/<cert-name>/bundle" in stdout
|
|
assert "fetch credential only" in stdout, "the token's scope has to be learnable, not folklore"
|
|
recipe = "--fetch-bundle https://dashboards.example.com --cert-name <HOST_TARGETS name>"
|
|
assert recipe in stdout.replace("\\\n", " ").replace(" ", " "), (
|
|
"the recipe line must be copy-pasteable from --help"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_sudoers_hint_never_repeats_the_session_token(tmp_path: Path) -> None:
|
|
"""The hint quotes a command, so it quotes only the flags that are safe to re-type.
|
|
|
|
`need_root` ends with `sudo <this script> …` so the operator can copy it. Handing that
|
|
string the real argv would print a bearer credential into the terminal, the ssh log and
|
|
whoever's scrollback — and the fetch runs *before* the root boundary, so this is the one
|
|
path where a good fetch and a refusal share a run. The fetch succeeding here is also the
|
|
proof of the order: an unprivileged run can only reach the hint after fetching.
|
|
"""
|
|
with served_dashboard(tmp_path) as (base, _store):
|
|
token = _sign_in(base)
|
|
result = _installer_without_root("--bind", "10.0.0.5", "--port", "9120",
|
|
*_fetch(base, ISSUED_HOST, token))
|
|
assert result.returncode == 1
|
|
assert _grant_lines(result.stderr), "the fetch succeeded, so what remains is the grant"
|
|
assert f"fetched the certificate bundle for {ISSUED_HOST}" in result.stdout
|
|
assert token not in result.stdout + result.stderr
|
|
assert "--session" not in result.stderr, "the echoed command must not name the flag either"
|
|
|
|
|
|
def test_the_session_token_is_confined_to_the_fetch() -> None:
|
|
"""Structural pin: the install body — everything that writes — cannot see the token.
|
|
|
|
`unset SESSION_TOKEN` is the promise; this is the proof that the promise is enforceable
|
|
rather than aspirational. Every file the installer writes is written inside `do_install`
|
|
(the token file, the bundle, the unit), so if the name does not appear anywhere in that
|
|
function or in the unit template, no artefact can carry it — the same style of pin
|
|
`tests/test_auth.py` and `tests/test_certs.py` hold over the two modules that hold secrets.
|
|
"""
|
|
text = INSTALLER.read_text()
|
|
assert 'unset SESSION_TOKEN' in text, "the fetch must forget the credential when it is done"
|
|
assert 'FETCH_SESSION="$SESSION_TOKEN" python3' in text, (
|
|
"the token reaches python in the environment: argv is world-readable through `ps`, so a"
|
|
" second copy on a command line would widen the exposure instead of limiting it"
|
|
)
|
|
body = text.split("do_install() {", 1)[1]
|
|
assert "SESSION_TOKEN" not in body, "nothing that writes may read the fetch credential"
|
|
assert text.count("SESSION_TOKEN") <= 8, (
|
|
"the name belongs to the parser, the all-or-nothing check and the fetch; every further"
|
|
" mention is another place a bearer token could be printed or written"
|
|
)
|
|
|
|
|
|
def test_the_fetch_and_its_shape_check_both_run_before_the_root_boundary() -> None:
|
|
"""Text pin for the order the no-partial-install promise depends on.
|
|
|
|
Behaviourally, a refusal as `ops` proves the refusal comes first; this proves the *shape*
|
|
check is not quietly moved after `need_root` (where a bad fetched bundle would produce a
|
|
sudoers hint instead of a reason, on the host where the operator can least afford it).
|
|
"""
|
|
body = INSTALLER.read_text().split("do_install() {", 1)[1]
|
|
fetch = body.index("fetch_bundle\n")
|
|
shape = body.index('validate_bundle "$BUNDLE_SRC"')
|
|
root = body.index("need_root --bind")
|
|
assert fetch < shape < root, "fetch, then shape, then root — in that order"
|
|
assert 'trap cleanup_fetch EXIT' in INSTALLER.read_text(), (
|
|
"the fetched key is cleaned up on every exit path, not only the happy one"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_a_local_bundle_still_validates_with_its_own_flag_name(tmp_path: Path) -> None:
|
|
"""The validator grew a label, and the pre-existing path must read exactly as before.
|
|
|
|
`--fetch-bundle` needed the refusal to name the flag that was used; the `--cert-bundle`
|
|
path must not start naming a flag nobody passed. That is the whole change, so this is the
|
|
whole pin — the file-local behaviour around it is covered by the phase-15 tests.
|
|
"""
|
|
path = tmp_path / "revless.bundle"
|
|
path.write_text(_fake_bundle_text().replace("# BUNDLE-REV 3\n", ""), encoding="ascii")
|
|
result = _installer_without_root("--cert-bundle", str(path))
|
|
assert result.returncode == 2
|
|
assert result.stderr.startswith("--cert-bundle"), result.stderr
|
|
assert "--fetch-bundle" not in result.stderr
|
|
|
|
|
|
# --- the gateway capture (phase 28 task 04) ------------------------------------------------
|
|
|
|
#: The script that records `tests/fixtures/gateway_*` — the ground truth the agent's `haproxy`,
|
|
#: `nginx` and `sockets` parsers are pinned to. It has no Python-side coverage to speak of (the
|
|
#: project's coverage source is `src/dashboards`), so the pins here **run it**, against a fake
|
|
#: remote reached through a stubbed `ssh`, the way the installer sandbox stubs `install`.
|
|
CAPTURE = SCRIPTS / "capture-gateway-config.sh"
|
|
|
|
#: The remote documents, and the fixture each one becomes. A new include directory on the gateway
|
|
#: means a new entry here *and* a new `case` in the script — the script refuses silently-unknown
|
|
#: globs for exactly this reason.
|
|
CAPTURE_TARGETS = (
|
|
"/etc/haproxy/haproxy.cfg",
|
|
"/etc/nginx/nginx.conf",
|
|
"/etc/nginx/conf.d/*.conf",
|
|
"/etc/nginx/stream.d/*.conf",
|
|
"/proc/net/tcp",
|
|
"/proc/net/tcp6",
|
|
)
|
|
|
|
#: The stub `ssh`. It drops the options the script passes, records every call, and answers `cat`
|
|
#: and `ls` from `$FAKE_REMOTE` as if that directory were the remote filesystem's `/`. Nothing
|
|
#: here can reach a network: the point is to exercise the script's own branches (the include
|
|
#: globs, the refusal rule, the socket-table edit) with no gateway in sight.
|
|
CAPTURE_SSH_STUB = """#!/bin/sh
|
|
printf '%s\\n' "$*" >> "$STUB_LOG"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-o) shift 2 ;;
|
|
-n) shift ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
host="$1"; shift
|
|
command="$*"
|
|
if [ "${FAIL_SSH:-0}" = "1" ]; then
|
|
echo "ssh: connect to host $host port 22: Connection refused" >&2
|
|
exit 255
|
|
fi
|
|
case "$command" in
|
|
"cat "*)
|
|
path="${command#cat }"
|
|
file="$FAKE_REMOTE$path"
|
|
[ -f "$file" ] || { echo "cat: $path: No such file or directory" >&2; exit 1; }
|
|
cat "$file"
|
|
;;
|
|
"getent "*)
|
|
# libc resolution on the fake box. Answers come from `$FAKE_REMOTE/getent/<name>.<family>`;
|
|
# a file that is not there is `getent`'s own "no such record" (exit 2, no output), which the
|
|
# script records as a status rather than a failure. Two env switches stand in for the two
|
|
# reads that are *not* a fact about the name: `GETENT_MODE=transport` is ssh dying, and
|
|
# `GETENT_MODE=missing` is a gateway with no getent to ask.
|
|
family="${command#getent }"
|
|
family="${family%% *}"
|
|
name="${command##* }"
|
|
case "${GETENT_MODE:-ok}" in
|
|
transport) echo "ssh: connect to host $host port 22: Connection refused" >&2; exit 255 ;;
|
|
missing) echo "bash: line 1: getent: command not found" >&2; exit 127 ;;
|
|
esac
|
|
file="$FAKE_REMOTE/getent/$name.${family#ahosts}"
|
|
[ -f "$file" ] || exit 2
|
|
cat "$file"
|
|
;;
|
|
"ls "*)
|
|
spec="${command#ls }"
|
|
dir="${spec%/*}"
|
|
glob="${spec##*/}"
|
|
found=0
|
|
for file in "$FAKE_REMOTE$dir"/$glob; do
|
|
[ -e "$file" ] || continue
|
|
found=1
|
|
printf '%s\\n' "${file#"$FAKE_REMOTE"}"
|
|
done
|
|
[ "$found" = 1 ] || { echo "ls: cannot access '$spec': No such file or directory" >&2; exit 2; }
|
|
;;
|
|
*)
|
|
echo "stub ssh: unsupported remote command: $command" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
"""
|
|
|
|
#: A gateway tree small enough to assert on whole, with the two shapes the parsers exist for: a
|
|
#: `use_backend` whose target is a fetch expression, and a stream `proxy_pass` of a map variable.
|
|
CAPTURE_FAKE_GATEWAY: dict[str, str] = {
|
|
"/etc/haproxy/haproxy.cfg": (
|
|
"global\n maxconn 4096\n"
|
|
"defaults\n mode tcp\n"
|
|
"frontend haproxy-22\n bind :::22 v4v6\n default_backend gitea-2222\n"
|
|
"frontend haproxy-8443\n bind :::8443 v4v6\n"
|
|
" use_backend %[req.hdr(host),lower,map_dom(/etc/haproxy/maps,b0)\n"
|
|
"backend gitea-2222\n server gitea gitea.reeselink.com:2222 check inter 5s\n"
|
|
),
|
|
"/etc/nginx/nginx.conf": (
|
|
"user nginx;\n"
|
|
"stream {\n include /etc/nginx/stream.d/*.conf;\n}\n"
|
|
"http {\n"
|
|
" include /etc/nginx/mime.types;\n"
|
|
" server { listen 80; listen [::]:80; server_name _; return 301 https://$host; }\n"
|
|
" include /etc/nginx/conf.d/*.conf;\n}\n"
|
|
),
|
|
"/etc/nginx/conf.d/http.conf": (
|
|
"server {\n"
|
|
" listen 4443 ssl proxy_protocol;\n"
|
|
" server_name nextcloud.reeseapps.com;\n"
|
|
" ssl_certificate_key /etc/letsencrypt/live/nextcloud.reeseapps.com/privkey.pem;\n"
|
|
" location / { proxy_pass http://nextcloud.reeselink.com:11000; }\n}\n"
|
|
),
|
|
"/etc/nginx/conf.d/tls.conf": (
|
|
"server {\n listen 8443 ssl;\n server_name brain.reeseapps.com;\n"
|
|
" location / { proxy_pass http://reeseapps-brain-of-reese.reeselink.com:443; }\n}\n"
|
|
),
|
|
"/etc/nginx/stream.d/stream.conf": (
|
|
"map $ssl_preread_server_name $targetBackend {\n"
|
|
" default 127.0.0.1:4443;\n"
|
|
" brain.example reeseapps-brain-of-reese.reeselink.com:443;\n}\n"
|
|
# `resolver` here, inside the stream server, is where this fleet writes it: the fixture's
|
|
# `config_directive` must come out of an *included* document, read from the bytes the run
|
|
# already captured, with no round trip of its own.
|
|
"server {\n listen 443;\n proxy_pass $targetBackend;\n resolver 10.9.8.7;\n}\n"
|
|
),
|
|
# 8.8.8.8 and 2603:6013::1 are globally routable, `::ffff:8.8.8.8` is a visitor behind a v6
|
|
# socket, and 203.0.113.7 is already documentation space. The tx/rx queue pair on the last row
|
|
# is spelled like an address column and is not one.
|
|
"/proc/net/tcp": (
|
|
" sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid"
|
|
" timeout inode\n"
|
|
" 0: 00000000:1EFF 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0"
|
|
" 0 1 1 0000000000000000 100 0 0 10 0\n"
|
|
" 1: 1B01010A:01BB 08080808:911A 01 00000000:00000000 00:00000000 00000000 101"
|
|
" 0 2 1 00000000d22dedba 20 4 28 10 -1\n"
|
|
" 2: 0100007F:23A0 017100CB:C0A8 06 08080808:08080808 00:00000000 00000000 0"
|
|
" 0 3 3 0000000000000000 20 0 0 10 0\n"
|
|
),
|
|
"/proc/net/tcp6": (
|
|
" sl local_address remote_address"
|
|
" st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n"
|
|
" 0: 00000000000000000000000000000000:01BB 00000000000000000000000000000000:0000 0A"
|
|
" 00000000:00000000 00:00000000 00000000 0 0 4 1 0000000000000000 100 0 0 10 0\n"
|
|
" 1: 1360032601010031FF005450E0B284FE:01BB 0000000000000000FFFF000008080808:C0AC 01"
|
|
" 00000000:00000000 00:00000000 00000000 101 0 5 1 0000000000000000 63 4 26 10 -1\n"
|
|
),
|
|
}
|
|
|
|
|
|
#: What the fake gateway's libc answers for the names its configs declare. The three arms are the
|
|
#: three the capture has to tell apart: an answer (private, so unscrubbed — this is what the real
|
|
#: fleet looks like), an answer that is **globally routable** (so it must become documentation
|
|
#: space), and no answer at all (a status, not a failure). `nextcloud` deliberately answers a v6
|
|
#: that is `::ffff:`-mapped, because `getent ahostsv6` synthesises that spelling for a name with no
|
|
#: AAAA record and the agent's own `getaddrinfo` never publishes it.
|
|
CAPTURE_FAKE_ANSWERS: dict[str, dict[str, str]] = {
|
|
# Private and answered twice over — the A record, and the `::ffff:` spelling `getent ahostsv6`
|
|
# synthesises for a name with no AAAA record. One address goes in; nothing moves.
|
|
"gitea.reeselink.com": {
|
|
"v4": "10.1.0.137 STREAM gitea.reeselink.com\n10.1.0.137 DGRAM \n",
|
|
"v6": "::ffff:10.1.0.137 STREAM \n",
|
|
},
|
|
# Two globally routable answers (one per family) beside one already in documentation space: the
|
|
# first two become `203.0.113.1` / `2001:db8::1` and the third stays exactly as it was, which is
|
|
# what makes a second run a no-op instead of a renumbering.
|
|
"nextcloud.reeselink.com": {
|
|
"v4": ("8.8.8.8 STREAM nextcloud.reeselink.com\n"
|
|
"203.0.113.9 STREAM nextcloud.reeselink.com\n"),
|
|
"v6": "2603:6013:3100:101:5054:ff:fe84:b2e0 STREAM nextcloud.reeselink.com\n",
|
|
},
|
|
# `reeseapps-brain-of-reese.reeselink.com` is answered by nothing on purpose: the absence of
|
|
# the file *is* the nxdomain arm, a status among successes rather than a failed run.
|
|
}
|
|
|
|
|
|
def _capture_sandbox(
|
|
tmp_path: Path,
|
|
gateway: dict[str, str] | None = None,
|
|
*,
|
|
recorded: dict[str, str] | None = None,
|
|
fail_ssh: bool = False,
|
|
fail_getent: str | None = None,
|
|
answers: dict[str, dict[str, str]] | None = None,
|
|
alias: str | None = None,
|
|
) -> tuple[Path, Path, Path, dict[str, str]]:
|
|
"""A copy of the capture script, a fake gateway, and a PATH whose `ssh` reaches only the fake.
|
|
|
|
Returns (script, fixtures dir, stub call log, env). The script is copied into
|
|
`<tmp>/scripts/` because it derives `tests/fixtures/` from its own location, which is what
|
|
keeps the repository's fixtures out of reach of a test.
|
|
|
|
The parser step needs the repo, so `agent/host_agent.py` is copied beside the copied script:
|
|
the sandbox is a checkout shape, and the plan step imports `target_names` from it exactly as
|
|
it does when the real script runs. That copy is the pin — a script that re-implemented the
|
|
parser would not need it, and one that imported a different module would not find it.
|
|
"""
|
|
script = tmp_path / "scripts" / CAPTURE.name
|
|
script.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(CAPTURE, script)
|
|
(tmp_path / "agent").mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(ROOT / "agent/host_agent.py", tmp_path / "agent/host_agent.py")
|
|
|
|
fake = tmp_path / "gateway"
|
|
for path, body in {**CAPTURE_FAKE_GATEWAY, **(gateway or {})}.items():
|
|
target = fake / path.lstrip("/")
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(body, encoding="utf-8")
|
|
for name, families in (CAPTURE_FAKE_ANSWERS if answers is None else answers).items():
|
|
for family, body in families.items():
|
|
answer = fake / "getent" / f"{name}.{family}"
|
|
answer.parent.mkdir(parents=True, exist_ok=True)
|
|
answer.write_text(body, encoding="utf-8")
|
|
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
fixtures.mkdir(parents=True, exist_ok=True)
|
|
for name, body in (recorded or {}).items():
|
|
(fixtures / name).write_text(body, encoding="utf-8")
|
|
|
|
stubs = tmp_path / "stubs"
|
|
stubs.mkdir()
|
|
stub = stubs / "ssh"
|
|
stub.write_text(CAPTURE_SSH_STUB, encoding="utf-8")
|
|
stub.chmod(0o755)
|
|
call_log = tmp_path / "ssh-calls.log"
|
|
call_log.write_text("", encoding="utf-8")
|
|
|
|
env = {**os.environ,
|
|
"PATH": f"{stubs}{os.pathsep}{os.environ['PATH']}",
|
|
"STUB_LOG": str(call_log),
|
|
"FAKE_REMOTE": str(fake),
|
|
"FAIL_SSH": "1" if fail_ssh else "0",
|
|
"GETENT_MODE": fail_getent or "ok",
|
|
# The script prepends its own root to this, which is how `agent.host_agent` resolves
|
|
# when uv has no project to run in (this sandbox is not one).
|
|
"PYTHONPATH": str(tmp_path)}
|
|
if alias is not None:
|
|
env["GATEWAY_SSH"] = alias
|
|
return script, fixtures, call_log, env
|
|
|
|
|
|
def _capture(tmp_path: Path, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
script, _fixtures, _log, env = _capture_sandbox(tmp_path, **kwargs) # type: ignore[arg-type]
|
|
return subprocess.run([BASH, str(script)], capture_output=True, text=True, env=env, check=False)
|
|
|
|
|
|
def _fixture_body(path: Path) -> str:
|
|
"""A fixture without its `#` provenance block — the bytes the host answered."""
|
|
return "".join(line for line in path.read_text().splitlines(keepends=True)
|
|
if not line.startswith("#"))
|
|
|
|
|
|
def test_the_gateway_capture_exists_and_asks_for_no_privilege() -> None:
|
|
"""Six world-readable reads plus `getent`, over an ssh key: no root, no rewrite tool.
|
|
|
|
`getent` belongs in this list because it is the seventh read and the only one that is not a
|
|
file: it is libc resolution on the box that dials, which is what `via: "libc"` claims, and it
|
|
is world-runnable for the same reason the six files are world-readable.
|
|
"""
|
|
assert CAPTURE.is_file(), "scripts/capture-gateway-config.sh is missing (phase 28 task 04)"
|
|
assert os.access(CAPTURE, os.X_OK)
|
|
text = CAPTURE.read_text()
|
|
for path in CAPTURE_TARGETS:
|
|
assert path in text, f"{path} is no longer captured"
|
|
for family in ("getent ahostsv4", "getent ahostsv6"):
|
|
assert family in text, f"{family} is no longer how the resolver document is recorded"
|
|
for tool in ("dig", "nslookup", "resolvectl", "drill"):
|
|
assert not re.search(rf"(?<![\w-]){tool}\s+[A-Za-z0-9.*_-]", text), (
|
|
f"{tool} assumes a DNS client the gateway may not ship; getent is libc, which is the "
|
|
"vantage the agent's own lookup performs")
|
|
code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
|
|
assert "sudo" not in code, (
|
|
"the capture must read exactly what dashboards-agent can read; a pass that needed root "
|
|
"would record more than the agent can see (the smartctl note in capture-fixtures.sh)"
|
|
)
|
|
for tool in ("sed ", "awk ", " tr ", "cut "):
|
|
assert tool not in text, f"{tool.strip()} would be a text filter between the read and a fixture"
|
|
|
|
|
|
def test_the_gateway_capture_dials_the_alias_and_nothing_else(tmp_path: Path) -> None:
|
|
"""No hardcoded address: every remote command goes to `$GATEWAY_SSH`, default `gateway-ops`."""
|
|
text = CAPTURE.read_text()
|
|
assert 'GATEWAY_SSH="${GATEWAY_SSH:-gateway-ops}"' in text
|
|
# Comments may name an address (`0.0.0.0` is a shape the parser has to survive); code may not.
|
|
code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
|
|
stray = [a for a in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", code)
|
|
if not a.startswith("203.0.113.")]
|
|
assert not stray, f"the script dials or embeds an address: {stray}"
|
|
|
|
_script, _fixtures, log, env = _capture_sandbox(tmp_path)
|
|
subprocess.run([BASH, str(_script)], capture_output=True, text=True, env=env, check=False)
|
|
assert _dialled_hosts(log) == {"gateway-ops"}
|
|
|
|
other = _capture_sandbox(tmp_path / "alias", alias="lab-gw")
|
|
subprocess.run([BASH, str(other[0])], capture_output=True, text=True, env=other[3], check=False)
|
|
assert _dialled_hosts(other[2]) == {"lab-gw"}
|
|
|
|
|
|
def _dialled_hosts(log: Path) -> set[str]:
|
|
"""The host argument of every ssh the stub was called for — options are not hosts."""
|
|
hosts = set()
|
|
for line in log.read_text().splitlines():
|
|
tokens = [token for token in line.split() if not token.startswith("-") and "=" not in token]
|
|
if tokens:
|
|
hosts.add(tokens[0])
|
|
return hosts
|
|
|
|
|
|
def test_the_gateway_capture_records_every_document_of_the_tree_it_read(tmp_path: Path) -> None:
|
|
"""The happy path, run: one fixture per remote document, include globs and all.
|
|
|
|
Two files in `conf.d` is deliberate. The script reads the include globs out of the captured
|
|
main config rather than a hardcoded pair of directories, so a gateway that adds a vhost file
|
|
gets it captured, and `files` in the payload stays the tree the config actually merges.
|
|
"""
|
|
result = _capture(tmp_path)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
expected = {"gateway_haproxy.cfg", "gateway_nginx_main.conf",
|
|
"gateway_nginx_conf_d_http.conf", "gateway_nginx_conf_d_tls.conf",
|
|
"gateway_nginx_stream_d_stream.conf",
|
|
"gateway_proc_net_tcp.txt", "gateway_proc_net_tcp6.txt",
|
|
# The resolver document, derived from the configs in this same set.
|
|
"gateway_resolver.json"}
|
|
assert {p.name for p in fixtures.iterdir()} == expected, result.stdout
|
|
assert _fixture_body(fixtures / "gateway_haproxy.cfg") == \
|
|
CAPTURE_FAKE_GATEWAY["/etc/haproxy/haproxy.cfg"]
|
|
assert _fixture_body(fixtures / "gateway_nginx_conf_d_tls.conf") == \
|
|
CAPTURE_FAKE_GATEWAY["/etc/nginx/conf.d/tls.conf"]
|
|
# `mime.types` is included by the config and absent on the host: no fixture, no complaint —
|
|
# the same absence-the-agent-reports rule, one level up.
|
|
assert not (fixtures / "gateway_nginx_mime.types").exists()
|
|
|
|
|
|
def test_the_gateway_capture_writes_the_domains_it_read_verbatim(tmp_path: Path) -> None:
|
|
"""Nothing is scrubbed from a config, and the domains are the proof.
|
|
|
|
The `server_name` and the `proxy_pass` target beside it are the ingress map, and the
|
|
`ssl_certificate_key` is a path rather than a key — so the file the fixture holds is the file
|
|
the gateway runs, byte for byte. The one exception in this script is the socket tables'
|
|
globally routable addresses, which `capture-fixtures.sh`'s identifier rule requires.
|
|
"""
|
|
_capture(tmp_path)
|
|
vhosts = (tmp_path / "tests" / "fixtures" / "gateway_nginx_conf_d_http.conf").read_text()
|
|
assert "server_name nextcloud.reeseapps.com;" in vhosts
|
|
assert "proxy_pass http://nextcloud.reeselink.com:11000;" in vhosts
|
|
assert "ssl_certificate_key /etc/letsencrypt/live/nextcloud.reeseapps.com/privkey.pem;" in vhosts
|
|
assert "SCRUB" not in vhosts and "REDACTED" not in vhosts
|
|
assert next((line for line in vhosts.splitlines()
|
|
if line.startswith("# Verbatim /etc/nginx/conf.d/http.conf from gateway-ops")),
|
|
None), "every fixture says which remote document it is"
|
|
|
|
|
|
def test_a_dead_gateway_leaves_the_recorded_fixtures_alone(tmp_path: Path) -> None:
|
|
"""The `slots_gufo.json` rule: a capture script must never turn six good fixtures into six
|
|
empty files, because then the parser tests pass against nothing (or fail for the wrong
|
|
reason) and the ground truth is gone."""
|
|
keep = {"gateway_haproxy.cfg": "# old\nfrontend kept\n bind :::22 v4v6\n",
|
|
"gateway_proc_net_tcp.txt": " 0: 00000000:1EFF 00000000:0000 0A x\n"}
|
|
result = _capture(tmp_path, recorded=keep, fail_ssh=True)
|
|
assert result.returncode == 1, result.stdout
|
|
for name, body in keep.items():
|
|
assert (tmp_path / "tests" / "fixtures" / name).read_text() == body, \
|
|
f"{name} was overwritten by a read that failed"
|
|
for name in keep:
|
|
assert f"✗ {name}" in result.stdout, result.stdout
|
|
assert "Connection refused" in result.stdout, "one line names the reason, per fixture"
|
|
assert sorted(p.name for p in (tmp_path / "tests" / "fixtures").iterdir()) == sorted(keep), \
|
|
"a failed capture leaves no temp files behind"
|
|
|
|
|
|
def test_an_empty_read_is_refused_the_way_a_failed_one_is(tmp_path: Path) -> None:
|
|
"""`cat` exiting 0 on an empty file is still not a capture."""
|
|
keep = {"gateway_proc_net_tcp.txt": " 0: 00000000:1EFF 00000000:0000 0A keep me\n"}
|
|
result = _capture(tmp_path, gateway={"/proc/net/tcp": ""}, recorded=keep)
|
|
assert result.returncode == 1, result.stdout
|
|
assert "gateway_proc_net_tcp.txt" in result.stdout
|
|
assert "empty" in result.stdout
|
|
assert (tmp_path / "tests" / "fixtures" / "gateway_proc_net_tcp.txt").read_text() == \
|
|
next(iter(keep.values()))
|
|
|
|
|
|
def test_an_include_dir_with_no_fixture_name_is_reported_and_fails(tmp_path: Path) -> None:
|
|
"""A config that moved its vhosts is a loud failure, not a fixture set that quietly stopped
|
|
being the tree the agent merges."""
|
|
moved = {"/etc/nginx/nginx.conf": (
|
|
"http {\n include /etc/nginx/sites-enabled/*.conf;\n}\n"
|
|
"stream {\n include /etc/nginx/stream.d/*.conf;\n}\n")}
|
|
result = _capture(tmp_path, gateway=moved)
|
|
assert result.returncode == 1, result.stdout
|
|
assert "/etc/nginx/sites-enabled/*.conf" in result.stdout
|
|
assert not (tmp_path / "tests" / "fixtures" / "gateway_nginx_sites_enabled_web.conf").exists()
|
|
|
|
|
|
def test_a_config_with_no_glob_include_says_the_vhost_fixtures_were_not_refreshed(
|
|
tmp_path: Path) -> None:
|
|
keep = {"gateway_nginx_conf_d_http.conf": "# keep\nserver {\n listen 4443;\n}\n"}
|
|
result = _capture(tmp_path, gateway={"/etc/nginx/nginx.conf": "user nginx;\nevents { }\n"},
|
|
recorded=keep)
|
|
assert result.returncode == 1, result.stdout
|
|
assert "no glob include" in result.stdout
|
|
assert (tmp_path / "tests" / "fixtures" / "gateway_nginx_conf_d_http.conf").read_text() == \
|
|
next(iter(keep.values()))
|
|
|
|
|
|
def test_the_socket_capture_publishes_no_globally_routable_endpoint(tmp_path: Path) -> None:
|
|
"""The one edit the capture makes, run rather than asserted in prose.
|
|
|
|
Two rules at once, which is what makes the socket fixtures both usable and publishable: a
|
|
globally routable address (including one behind `::ffff:`) becomes documentation space, and
|
|
everything else — every port, every private/loopback/wildcard word, every other column, and
|
|
the column widths themselves — is byte-for-byte what the kernel answered. A tx/rx queue pair
|
|
is spelled exactly like an address column and must survive untouched, which is why the edit
|
|
reads two columns and not the line.
|
|
"""
|
|
_capture(tmp_path)
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
v4 = _fixture_body(fixtures / "gateway_proc_net_tcp.txt").splitlines()
|
|
v6 = _fixture_body(fixtures / "gateway_proc_net_tcp6.txt").splitlines()
|
|
raw_v4 = CAPTURE_FAKE_GATEWAY["/proc/net/tcp"].splitlines()
|
|
raw_v6 = CAPTURE_FAKE_GATEWAY["/proc/net/tcp6"].splitlines()
|
|
assert len(v4) == len(raw_v4) and len(v6) == len(raw_v6), "a row was added or lost"
|
|
|
|
assert v4[1] == raw_v4[1], "a wildcard listener must survive untouched"
|
|
assert "08080808:911A" not in "".join(v4), "8.8.8.8 is still in the fixture"
|
|
assert "017100CB:911A" in v4[2], v4[2] # 203.0.113.1, the port kept
|
|
assert len(v4[2]) == len(raw_v4[2]), "the replacement must be exactly as wide as the original"
|
|
assert v4[3] == raw_v4[3], (
|
|
"an already-documentation address and the tx/rx queue pair on that row both moved: only "
|
|
"the two address columns may be looked at")
|
|
assert "08080808:08080808" in v4[3], v4[3]
|
|
|
|
assert "1360032601010031FF005450E0B284FE" not in "".join(v6), "a global v6 is left in"
|
|
assert "B80D0120000000000000000001000000:01BB" in v6[2], v6[2] # 2001:db8::1, port kept
|
|
assert "FFFF0000017100CB:C0AC" in v6[2] or "FFFF0000" in v6[2], \
|
|
"a v4-mapped row must stay v4-mapped, or the parser's branch for it stops being exercised"
|
|
assert v6[1] == raw_v6[1], "the wildcard v6 listener moved"
|
|
assert len(v6[2]) == len(raw_v6[2])
|
|
|
|
|
|
# --- the resolver document (phase 32 task 02) ------------------------------------------------
|
|
|
|
#: One entry per declared target of the fake gateway, in the plan's own order: haproxy's `server`
|
|
#: line first, then the nginx vhosts' `proxy_pass` targets, then the stream `map`'s entries — with
|
|
#: the map's `default` an address literal, which is why it costs no read on the remote.
|
|
CAPTURE_RESOLVER_TARGETS: tuple[tuple[str, int | None], ...] = (
|
|
("gitea.reeselink.com", 2222),
|
|
("nextcloud.reeselink.com", 11000),
|
|
("reeseapps-brain-of-reese.reeselink.com", 443),
|
|
("127.0.0.1", 4443),
|
|
)
|
|
|
|
RESOLVER_KEEP = {
|
|
"gateway_resolver.json": '{"available": true, "entries": ["keep me"]}\n',
|
|
}
|
|
|
|
def _resolver_fixture(path: Path) -> dict[str, Any]:
|
|
"""The recorded document, parsed — and a clear failure when the run refused to write it."""
|
|
assert path.is_file(), f"{path.name} was not written"
|
|
return json.loads(path.read_text())
|
|
|
|
|
|
def _resolver_run(tmp_path: Path, **kwargs: Any) -> tuple[subprocess.CompletedProcess[str], Path]:
|
|
"""One sandboxed capture run, returning its result and the fixture directory it wrote into."""
|
|
script, fixtures, _log, env = _capture_sandbox(tmp_path, **kwargs)
|
|
result = subprocess.run([BASH, str(script)], capture_output=True, text=True, env=env,
|
|
check=False)
|
|
return result, fixtures
|
|
|
|
|
|
#: The config documents of the captured tree, the same mapping `tests/conftest.py` lays down for
|
|
#: `sources/gateway.py`: an agent `--root` under which the agent can read this repo's captures.
|
|
def test_the_capture_asks_its_own_parser_and_not_its_memory() -> None:
|
|
"""The name list is `target_names()`, imported — never a second list somebody maintained.
|
|
|
|
This is the phase's rule 3 in the capture's own hands: the fixture can only ever name what the
|
|
agent will resolve, so `target_names()` changing moves the fixture with it instead of leaving a
|
|
document of names the agent stopped asking about.
|
|
"""
|
|
text = CAPTURE.read_text()
|
|
assert "from agent.host_agent import" in text, "the plan does not import the agent's parser"
|
|
assert "target_names(" in text, "the plan does not ask the agent what its configs declare"
|
|
assert "Collector(" in text, "the plan does not read the captured configs the agent reads them"
|
|
# And no domain is written into the script: the only domains in it are comments and the fake
|
|
# gateway's own fixtures live in the test, not here.
|
|
code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
|
|
assert ".reeselink.com" not in code and ".reeseapps.com" not in code, (
|
|
"a target name is hardcoded in the capture instead of derived from the configs")
|
|
|
|
|
|
def test_the_resolver_document_names_every_target_the_configs_declare(tmp_path: Path) -> None:
|
|
"""The happy path: one libc read per declared name, in the agent's order, in its own shape.
|
|
|
|
Four assertions the fixture's whole usefulness rests on: the entry set is the plan, `via` says
|
|
how the addresses were obtained, `config_directive` came out of the **captured** nginx text (so
|
|
it matches what the agent will read from the same bytes, with no extra round trip), and the
|
|
remote was asked exactly twice per name — and never about the literal the map writes.
|
|
"""
|
|
result, fixtures = _resolver_run(tmp_path)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
document = _resolver_fixture(fixtures / "gateway_resolver.json")
|
|
|
|
assert document["available"] is True
|
|
assert document["via"] == "libc"
|
|
assert document["config_directive"] == "10.9.8.7", (
|
|
"the `resolver` directive did not come out of the included stream config")
|
|
assert [(row["name"], row["port"]) for row in document["entries"]] == list(
|
|
CAPTURE_RESOLVER_TARGETS), "the entries are the plan, in the agent's order"
|
|
assert document["errors"] == []
|
|
for row in document["entries"]:
|
|
assert set(row) == {"name", "port", "status", "addresses", "ts"}
|
|
assert row["ts"] == document["ts"], "a fixed capture ts keeps the fixture byte-stable"
|
|
|
|
script, _fixtures, log, _env = _capture_sandbox(tmp_path / "ledger")
|
|
subprocess.run([BASH, str(script)], capture_output=True, text=True, env=_env, check=False)
|
|
asked = [line for line in log.read_text().splitlines() if "getent " in line]
|
|
for name, _port in CAPTURE_RESOLVER_TARGETS:
|
|
if ipaddress.ip_address(name) if name[0].isdigit() else False:
|
|
assert not [line for line in asked if name in line], (
|
|
"an address literal cost a lookup on the remote")
|
|
continue
|
|
families = [line for line in asked if "getent ahostsv" in line and line.endswith(name)]
|
|
assert len(families) == 2, f"{name} was not read once per family: {families}"
|
|
|
|
|
|
def test_the_resolver_document_scrubs_global_addresses_and_keeps_everything_else(
|
|
tmp_path: Path) -> None:
|
|
"""What the document edits, and what it must not, in one run.
|
|
|
|
Three arms at once, because they are one rule: `8.8.8.8` and a global EUI-64 become
|
|
documentation space (invariant 6 — a resolved address is as much an identifier as the socket
|
|
table's), `10.1.0.137` and an address already in documentation space stay exactly as they were
|
|
(which is what makes a re-run a no-op), and the `::ffff:` spelling `getent ahostsv6` synthesises
|
|
for a name with no AAAA record is dropped — the agent's one `getaddrinfo` never publishes it, so
|
|
recording it would claim an address the agent does not. A name nobody answers is `nxdomain`, the
|
|
status the page splits out from "resolved and idle".
|
|
"""
|
|
result, fixtures = _resolver_run(tmp_path)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
entries = {row["name"]: row for row in
|
|
_resolver_fixture(fixtures / "gateway_resolver.json")["entries"]}
|
|
|
|
private = entries["gitea.reeselink.com"]
|
|
assert private["status"] == "ok"
|
|
assert private["addresses"] == ["10.1.0.137"], private
|
|
assert not any(a.startswith("::ffff:") for a in private["addresses"]), (
|
|
"the synthesised v4-mapped spelling survived: the agent never publishes it")
|
|
|
|
moved = entries["nextcloud.reeselink.com"]
|
|
assert moved["status"] == "ok"
|
|
assert moved["addresses"] == ["203.0.113.1", "203.0.113.9", "2001:db8::1"], moved
|
|
assert "8.8.8.8" not in json.dumps(moved), "a globally routable answer is in a tracked file"
|
|
assert "2603:6013" not in json.dumps(moved), "an EUI-64 address is in a tracked file"
|
|
|
|
missing = entries["reeseapps-brain-of-reese.reeselink.com"]
|
|
assert missing["status"] == "nxdomain", "one name failing among successes is a fact, kept"
|
|
assert missing["addresses"] == [], "an unresolved name publishes no address, not 0 and not a guess"
|
|
|
|
literal = entries["127.0.0.1"]
|
|
assert literal["status"] == "literal" and literal["addresses"] == ["127.0.0.1"]
|
|
|
|
|
|
def test_the_resolver_document_is_idempotent_so_a_re_run_does_not_churn(tmp_path: Path) -> None:
|
|
"""Two runs over the same answers write the same bytes.
|
|
|
|
The `ts` is a constant and the scrub is first-seen order over a deterministic plan, so a
|
|
re-capture of a gateway nobody changed is a no-op — the property that makes `git diff` on this
|
|
directory a statement about the ingress rather than about the clock.
|
|
"""
|
|
once, first = _resolver_run(tmp_path)
|
|
assert once.returncode == 0, once.stdout + once.stderr
|
|
again, second = _resolver_run(tmp_path / "again")
|
|
assert again.returncode == 0, again.stdout + again.stderr
|
|
assert (second / "gateway_resolver.json").read_bytes() == (
|
|
first / "gateway_resolver.json").read_bytes()
|
|
|
|
|
|
@pytest.mark.parametrize("mode, needle", [
|
|
pytest.param("transport", "did not answer", id="ssh-dies"),
|
|
pytest.param("missing", "getent is not available", id="no-getent"),
|
|
])
|
|
def test_a_broken_resolver_vantage_is_not_a_fixture(tmp_path: Path, mode: str,
|
|
needle: str) -> None:
|
|
"""The configs capture fine and only the resolver is broken — and it still refuses.
|
|
|
|
This is the arm the socket rule cannot cover: `getent` answering *nothing* for every name is a
|
|
fact about the box's resolver, not about the names, and a document of `nxdomain` rows would
|
|
make every live route on the page a permanent dash and every join test a test of nothing.
|
|
"""
|
|
result, fixtures = _resolver_run(tmp_path, recorded=dict(RESOLVER_KEEP), fail_getent=mode)
|
|
assert result.returncode == 1, result.stdout
|
|
assert needle in result.stdout, result.stdout
|
|
assert "gateway_resolver.json" in result.stdout
|
|
assert (fixtures / "gateway_resolver.json").read_text() == RESOLVER_KEEP[
|
|
"gateway_resolver.json"], "a broken vantage overwrote the recorded document"
|
|
|
|
|
|
def test_not_one_name_answering_is_refused_the_way_a_dead_transport_is(tmp_path: Path) -> None:
|
|
"""Every name answered `nxdomain`: each individual read succeeded, and the run still refuses.
|
|
|
|
A resolver that returns "no records" for the whole config is a firewall, a dead upstream DNS,
|
|
or a box that has never been pointed at one — and the fixture it would produce is the same
|
|
nothing the other two arms produce.
|
|
"""
|
|
result, fixtures = _resolver_run(tmp_path, recorded=dict(RESOLVER_KEEP), answers={})
|
|
assert result.returncode == 1, result.stdout
|
|
assert "none of the" in result.stdout and "vantage is broken" in result.stdout, result.stdout
|
|
assert (fixtures / "gateway_resolver.json").read_text() == RESOLVER_KEEP[
|
|
"gateway_resolver.json"]
|
|
|
|
|
|
def test_a_dead_gateway_does_not_attempt_the_resolver_document(tmp_path: Path) -> None:
|
|
"""With no configs there is no name list, and a document of the wrong question is worse than none."""
|
|
result = _capture(tmp_path, recorded=dict(RESOLVER_KEEP), fail_ssh=True)
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
assert result.returncode == 1, result.stdout
|
|
assert "not attempted" in result.stdout, result.stdout
|
|
assert (fixtures / "gateway_resolver.json").read_text() == RESOLVER_KEEP[
|
|
"gateway_resolver.json"]
|
|
assert sorted(p.name for p in fixtures.iterdir()) == ["gateway_resolver.json"], (
|
|
"a refused resolver capture left a temp file in tests/fixtures/")
|
|
|
|
|
|
def test_the_socket_capture_is_idempotent_so_a_re_run_does_not_churn(tmp_path: Path) -> None:
|
|
"""Re-recording the same moment must not diff, or every capture moves the fixture twice.
|
|
|
|
Documentation space is not globally routable, so the second pass leaves it alone — the same
|
|
property that makes the drive-serial scrub re-runnable.
|
|
"""
|
|
once = tmp_path / "once"
|
|
_capture(once)
|
|
again = _capture_sandbox(tmp_path / "twice",
|
|
gateway={"/proc/net/tcp": _fixture_body(
|
|
once / "tests" / "fixtures" / "gateway_proc_net_tcp.txt")})
|
|
result = subprocess.run([BASH, str(again[0])], capture_output=True, text=True,
|
|
env=again[3], check=False)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert _fixture_body(again[1] / "gateway_proc_net_tcp.txt") == _fixture_body(
|
|
once / "tests" / "fixtures" / "gateway_proc_net_tcp.txt")
|
|
|
|
|
|
# --- the route-switch helper: --with-routectl / --without-routectl (phase 33 task 02) --------
|
|
|
|
#: The install that adds the helper and its two units, at the sandbox's default bind/port.
|
|
ROUTECTL_FLAGS = ("--bind", "10.0.0.5", "--port", "9120", "--with-routectl")
|
|
|
|
|
|
def _target(tmp_path: Path, where: str = "") -> Path:
|
|
return tmp_path / where / "target"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_with_routectl_installs_helper_and_units_and_leaves_the_agent_unit_byte_identical(
|
|
tmp_path: Path
|
|
) -> None:
|
|
"""The phase's central safety claim, as a bytes comparison.
|
|
|
|
Two sandboxes, the same flags, one with the helper: the rendered `dashboards-agent.service`
|
|
must be byte-identical — the temptation to "help" the agent reach the socket by relaxing
|
|
its sandbox is exactly what this task exists to close, and the comparison is the pin. The
|
|
helper and both units land in the other sandbox, byte-identical to the deployed copies, and
|
|
the install ends with the socket's activation and the agent-uid reachability check.
|
|
"""
|
|
plain_script, _plain_log, plain_env = _sandbox_installer(tmp_path / "plain")
|
|
plain = _install(plain_script, plain_env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert plain.returncode == 0, f"plain install failed:\n{plain.stdout}\n{plain.stderr}"
|
|
|
|
rc_script, rc_log, rc_env = _sandbox_installer(tmp_path / "rc")
|
|
rc = _install(rc_script, rc_env, *ROUTECTL_FLAGS)
|
|
assert rc.returncode == 0, f"the routectl install failed:\n{rc.stdout}\n{rc.stderr}"
|
|
|
|
plain_unit = (
|
|
_target(tmp_path, "plain") / "dashboards-agent.service"
|
|
).read_text().replace(str(_target(tmp_path, "plain")), "<ROOT>")
|
|
rc_unit = (
|
|
_target(tmp_path, "rc") / "dashboards-agent.service"
|
|
).read_text().replace(str(_target(tmp_path, "rc")), "<ROOT>")
|
|
assert plain_unit == rc_unit, "the agent unit must be byte-identical with and without --with-routectl"
|
|
|
|
target = _target(tmp_path, "rc")
|
|
helper = target / "sbin" / "dashboards-routectl.py"
|
|
socket_unit = target / "dashboards-routectl.socket"
|
|
service_unit = target / "dashboards-routectl@.service"
|
|
assert helper.is_file() and socket_unit.is_file() and service_unit.is_file()
|
|
assert helper.read_text() == (DEPLOY / "dashboards-routectl.py").read_text(), (
|
|
"the installed helper is a copy, not a re-render"
|
|
)
|
|
assert socket_unit.read_text() == (DEPLOY / "dashboards-routectl.socket").read_text()
|
|
assert service_unit.read_text() == (DEPLOY / "dashboards-routectl@.service").read_text()
|
|
calls = rc_log.read_text()
|
|
assert "systemctl enable --now dashboards-routectl.socket" in calls
|
|
assert "sudo -u dashboards-agent python3" in calls, "the reachability check runs as the agent uid"
|
|
assert str(target / "run" / "dashboards-routectl.sock") in calls, (
|
|
"the check connects the sandbox's socket path, as the agent uid"
|
|
)
|
|
|
|
|
|
def _unit_lines(path: Path) -> list[str]:
|
|
return [line.rstrip() for line in path.read_text().splitlines() if line.strip()]
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_allow_writes_adds_only_the_flag_to_the_agent_unit(tmp_path: Path) -> None:
|
|
"""The write path's one unit change is one argument, and nothing else moves.
|
|
|
|
Task 07's guard says a re-run diff that shows `NoNewPrivileges`, `ProtectSystem`,
|
|
`PrivateDevices` or `SystemCallFilter` moving is a stop, not a `--force` — so this is
|
|
the same diff in the sandbox: with `--allow-writes`, the generated unit differs from
|
|
the plain install's exactly in the `ExecStart` line, and every security directive is
|
|
byte-identical. The flag lands on the unit's own ExecStart, because the agent (not
|
|
the installer) is the one that must refuse a write it was not started for.
|
|
"""
|
|
script, _log, env = _sandbox_installer(tmp_path)
|
|
plain = _install(script, env, "--bind", "10.0.0.5", "--port", "9120")
|
|
assert plain.returncode == 0
|
|
plain_unit = _target(tmp_path) / "dashboards-agent.service"
|
|
plain_root = str(_target(tmp_path))
|
|
|
|
script2, _log2, env2 = _sandbox_installer(tmp_path / "w")
|
|
writes = _install(script2, env2, "--bind", "10.0.0.5", "--port", "9120",
|
|
"--allow-writes", "--with-routectl")
|
|
assert writes.returncode == 0, f"--allow-writes install failed:\n{writes.stderr}"
|
|
writes_unit = _target(tmp_path, "w") / "dashboards-agent.service"
|
|
|
|
plain_lines = _unit_lines(plain_unit)
|
|
writes_lines = _unit_lines(writes_unit)
|
|
writes_root = str(_target(tmp_path, "w"))
|
|
plain_exec = [line_ for line_ in plain_lines if line_.startswith("ExecStart=")]
|
|
writes_exec = [line_ for line_ in writes_lines if line_.startswith("ExecStart=")]
|
|
assert len(plain_exec) == 1 and len(writes_exec) == 1
|
|
writes_exec[0] = writes_exec[0].replace(writes_root, "<ROOT>")
|
|
plain_exec[0] = plain_exec[0].replace(plain_root, "<ROOT>")
|
|
assert writes_exec[0] == plain_exec[0] + " --allow-writes", (
|
|
"the flag is appended to ExecStart, nothing else changes there"
|
|
)
|
|
assert len(plain_lines) == len(writes_lines), "no other line is added or removed"
|
|
for line_ in plain_lines:
|
|
if line_.startswith("ExecStart="):
|
|
continue
|
|
assert line_.replace(plain_root, "<ROOT>") in [
|
|
other.replace(writes_root, "<ROOT>") for other in writes_lines
|
|
], f"the write path moved a unit line: {line_}"
|
|
# The four directives task 07's guard names: they must be byte-identical, not moved.
|
|
# `PrivateDevices` is `false` by documented decision (the unit carries no device filter
|
|
# at all — see the template's comment), so the pin is on the value that is there.
|
|
for directive in ("NoNewPrivileges=true", "ProtectSystem=strict",
|
|
"PrivateDevices=false", "SystemCallFilter=@system-service"):
|
|
assert directive in writes_unit.read_text(), f"the write path moved {directive}"
|
|
assert directive in plain_unit.read_text(), f"the baseline unit lacks {directive}"
|
|
|
|
# And a flag without a helper is a legitimate host state: it installs, and the agent
|
|
# answers a write with 503 rather than a write — the installer has nothing to refuse.
|
|
script3, _log3, env3 = _sandbox_installer(tmp_path / "w2")
|
|
solo = _install(script3, env3, "--bind", "10.0.0.5", "--port", "9120", "--allow-writes")
|
|
assert solo.returncode == 0, f"--allow-writes without --with-routectl must install:\n{solo.stderr}"
|
|
assert " --allow-writes" in _target(tmp_path, "w2").joinpath(
|
|
"dashboards-agent.service").read_text()
|
|
|
|
# Non-install modes do not render a unit, so the flag refuses there before any write.
|
|
result = _install(script, env, "--status", "--allow-writes")
|
|
assert result.returncode == 1
|
|
assert "needs an install" in result.stderr
|
|
|
|
|
|
def _serve_on_socket(tmp_path: Path, *, allow_uid: str,
|
|
request: bytes) -> tuple[int, str]:
|
|
"""Run the real helper as a subprocess the unit does: a connected AF_UNIX socket on
|
|
fd 0/fd 1 (`Accept=yes` + `StandardInput=socket`), `--allow-uid`, one request, one
|
|
answer line. Returns `(returncode, answer line)` — the process's exit code is the
|
|
gate's verdict as much as the line is."""
|
|
child_side, parent_side = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
proc = subprocess.Popen(
|
|
[sys.executable, str(DEPLOY / "dashboards-routectl.py"), "--serve",
|
|
"--allow-uid", allow_uid,
|
|
"--agent-module", str(ROOT / "agent" / "host_agent.py"),
|
|
"--state", str(tmp_path / "serve-state.json")],
|
|
stdin=child_side.fileno(), stdout=child_side.fileno(),
|
|
stderr=subprocess.PIPE, close_fds=True)
|
|
parent_side.sendall(request)
|
|
answer = parent_side.recv(65536).decode("utf-8").strip()
|
|
parent_side.close()
|
|
proc.wait(timeout=60)
|
|
child_side.close()
|
|
err = proc.stderr.read().decode() if proc.stderr is not None else ""
|
|
if proc.returncode not in (0, 1):
|
|
raise AssertionError(
|
|
f"the helper exited {proc.returncode} instead of answering:\n"
|
|
f"{answer}\n{err}")
|
|
return proc.returncode, answer
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_serve_answers_one_request_on_the_socket_stdin(tmp_path: Path) -> None:
|
|
"""The live box's transport, pinned: with `Accept=yes` + `StandardInput=socket`,
|
|
the accepted connection lands on fd 0, and `serve()` must find it there (fd 3 first —
|
|
the classic contract — then fd 0), name its peer, and answer one request. The
|
|
first live install (phase 33 task 07) answered a real connection with
|
|
`peer: no connected socket on this transport` because only fd 3 was looked at;
|
|
the same task found the box's kernel delivers `SO_PEERCRED` reversed
|
|
(`{pid, uid, gid}`) and `read(2)` on a socket fd that does not wake on
|
|
buffered data (`recv(2)` does) — which is why the helper calibrates the uid
|
|
slot with a same-process probe and reads the socket path with `recv`."""
|
|
me = pwd.getpwuid(os.geteuid()).pw_name
|
|
code, answer = _serve_on_socket(
|
|
tmp_path, allow_uid=me,
|
|
request=b'{"id": "haproxy:80->127.0.0.1:443", "action": "show"}\n')
|
|
body = json.loads(answer)
|
|
assert isinstance(body, dict), (
|
|
f"the answer is one JSON object on the wire (one write, no double): {answer!r}")
|
|
assert body.get("ok") is not None, f"the named peer's request was not answered: {answer}"
|
|
assert "peer:" not in str(body.get("detail", "")), (
|
|
f"the named peer's request was refused by the gate: {answer}")
|
|
assert code in (0, 1)
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_serve_refuses_a_wrong_peer_and_a_transport_without_one(
|
|
tmp_path: Path) -> None:
|
|
"""The second look fails closed in both directions: a named peer that is not
|
|
`--allow-uid` is a refusal, and a transport whose caller the kernel will not name
|
|
(`/dev/null` on stdin) is a refusal too — the gate never opens on an unnamed caller."""
|
|
me = pwd.getpwuid(os.geteuid()).pw_name
|
|
_code, answer = _serve_on_socket(tmp_path, allow_uid="root",
|
|
request=b'{"verb": "list"}\n')
|
|
body = json.loads(answer)
|
|
assert body["ok"] is False and "peer:" in body["detail"], (
|
|
f"a foreign uid must not pass the gate: {answer}")
|
|
|
|
# the wire's own gate (a correctly-peered caller): a wider body is a
|
|
# refusal with the wire's own sentence — and the answer is a JSON
|
|
# *object*, not the (dict, code) tuple a caller-side inversion once
|
|
# serialised as a list (phase 33 task 07 caught it on the live chain).
|
|
_code, answer = _serve_on_socket(tmp_path, allow_uid=me,
|
|
request=b'{"verb": "list"}\n')
|
|
body = json.loads(answer)
|
|
assert body["ok"] is False, f"a wider body must be refused: {answer}"
|
|
assert body["detail"] == "body: exactly {id, action, dry_run}", (
|
|
f"the refusal must carry the wire's own sentence: {answer}")
|
|
|
|
proc = subprocess.Popen(
|
|
[sys.executable, str(DEPLOY / "dashboards-routectl.py"), "--serve",
|
|
"--allow-uid", me, "--agent-module", str(ROOT / "agent" / "host_agent.py"),
|
|
"--state", str(tmp_path / "pipe-state.json")],
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
close_fds=True)
|
|
proc.wait(timeout=60)
|
|
out = proc.stdout.read().decode("utf-8").strip() if proc.stdout is not None else ""
|
|
assert proc.returncode == 1, (
|
|
f"an unnamed caller must exit refused (1), not answered: rc={proc.returncode} out={out!r}")
|
|
assert out == "", (
|
|
f"a pipe transport has no wire; the refusal is the exit code, not a line: {out!r}")
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_units_carry_the_grant_as_directives(tmp_path: Path) -> None:
|
|
"""The file-layer grant is a directive systemd enforces; the sandbox pins the lines.
|
|
|
|
`systemd-analyze verify` is not available in the sandbox, so the assertions are on the
|
|
directive lines: the socket is 0600 and agent-owned (the whole access control at the file
|
|
layer), the service is root with the socket on stdin and the one-state-directory
|
|
`StateDirectory`, and `ReadWritePaths` names every directory whose file the helper
|
|
replaces — `/etc/haproxy`, `/etc/nginx/conf.d`, `/etc/nginx/stream.d` — and nothing else
|
|
(the main `nginx.conf` is never written, so it must not be listed).
|
|
"""
|
|
script, _log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, *ROUTECTL_FLAGS)
|
|
assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}"
|
|
target = _target(tmp_path)
|
|
|
|
socket_unit = (target / "dashboards-routectl.socket").read_text()
|
|
# Without `Accept=yes` systemd would look for a `dashboards-routectl.service` that does
|
|
# not exist (a `Service=` line naming the template does not load on systemd 257) and
|
|
# refuse to listen — the live box's first install, phase 33 task 07.
|
|
assert _directive(socket_unit, "Accept") == ["Accept=yes"]
|
|
assert _directive(socket_unit, "ListenStream") == ["ListenStream=/run/dashboards-routectl.sock"]
|
|
assert _directive(socket_unit, "SocketMode") == ["SocketMode=0600"], (
|
|
"0600 + SocketUser is the whole access control at the file layer"
|
|
)
|
|
assert _directive(socket_unit, "SocketUser") == ["SocketUser=dashboards-agent"]
|
|
assert _directive(socket_unit, "SocketGroup") == ["SocketGroup=dashboards-agent"]
|
|
assert "WantedBy=sockets.target" in socket_unit
|
|
|
|
service = (target / "dashboards-routectl@.service").read_text()
|
|
assert _directive(service, "User") == ["User=root"]
|
|
assert "StandardInput=socket" in service and "StandardOutput=socket" in service, (
|
|
"one connection, one instance: the connected socket is stdin"
|
|
)
|
|
assert _directive(service, "StateDirectory") == ["StateDirectory=dashboards-routectl"]
|
|
assert _directive(service, "ProtectSystem") == ["ProtectSystem=read-only"]
|
|
assert _directive(service, "ReadWritePaths") == [
|
|
"ReadWritePaths=/etc/haproxy /etc/nginx/conf.d /etc/nginx/stream.d"
|
|
], "os.replace needs write on the directory: every one listed, and nothing the helper never writes"
|
|
assert _directive(service, "NoNewPrivileges") == ["NoNewPrivileges=true"], (
|
|
"the privilege comes from systemd starting it as root, not from escalation"
|
|
)
|
|
assert _directive(service, "SystemCallFilter") == ["SystemCallFilter=@system-service"]
|
|
assert _directive(service, "ProtectHome") == ["ProtectHome=read-only"]
|
|
assert _directive(service, "PrivateTmp") == ["PrivateTmp=yes"]
|
|
exec_lines = _directive(service, "ExecStart")
|
|
assert len(exec_lines) == 1 and exec_lines[0].startswith(
|
|
"ExecStart=/usr/local/sbin/dashboards-routectl.py --serve"
|
|
)
|
|
assert "--allow-uid dashboards-agent" in service, "the second look: the peer uid the helper accepts"
|
|
assert "--state /var/lib/dashboards-routectl/state.json" in service
|
|
|
|
# The on-disk modes the sandbox's real `install -m` applied.
|
|
assert stat.S_IMODE((target / "sbin" / "dashboards-routectl.py").stat().st_mode) == 0o755, (
|
|
"the helper is world-executable: the grant is the socket, not the file mode"
|
|
)
|
|
assert stat.S_IMODE((target / "dashboards-routectl.socket").stat().st_mode) == 0o644
|
|
assert stat.S_IMODE((target / "dashboards-routectl@.service").stat().st_mode) == 0o644
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_without_routectl_removes_the_units_and_not_the_config_or_the_state(tmp_path: Path) -> None:
|
|
"""The inverse keeps what the removal must keep.
|
|
|
|
The proxy configs and the helper's state directory are the operator's memory of what was
|
|
switched, so `--without-routectl` prints the still-off list first, removes the helper and
|
|
its two units, and leaves the state (and the configs) behind, ending with the reminder.
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
assert _install(script, env, *ROUTECTL_FLAGS).returncode == 0
|
|
target = _target(tmp_path)
|
|
state_dir = target / "var-lib" / "dashboards-routectl"
|
|
state_dir.mkdir(parents=True)
|
|
state_dir.joinpath("state.json").write_text('{"nginx:443->127.0.0.1:44450": {}}', encoding="utf-8")
|
|
|
|
result = _install(script, env, "--without-routectl")
|
|
assert result.returncode == 0, f"--without-routectl failed:\n{result.stdout}\n{result.stderr}"
|
|
assert not (target / "dashboards-routectl.socket").exists(), "the socket unit must be removed"
|
|
assert not (target / "dashboards-routectl@.service").exists(), "the service unit must be removed"
|
|
assert not (target / "sbin" / "dashboards-routectl.py").exists(), "the helper must be removed"
|
|
assert (state_dir / "state.json").is_file(), "the state is the memory of what was switched; it stays"
|
|
calls = call_log.read_text()
|
|
assert "systemctl disable --now dashboards-routectl.socket" in calls
|
|
assert "daemon-reload" in calls
|
|
out = result.stdout
|
|
assert "routes the helper has switched off" in out, "the still-off list prints first"
|
|
assert "still off" in out, "the reminder ends the removal, not the file list"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_with_and_without_flags_do_not_mix(tmp_path: Path) -> None:
|
|
"""One flag each direction, before any write."""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--with-routectl", "--without-routectl")
|
|
assert result.returncode == 1
|
|
assert result.stdout == ""
|
|
assert "one flag each direction" in result.stderr
|
|
assert not call_log.exists(), "refused before a single privileged command"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_with_routectl_needs_an_install(tmp_path: Path) -> None:
|
|
"""`--status` probes a port; `--uninstall` deletes the agent. Neither installs files."""
|
|
script, _log, env = _sandbox_installer(tmp_path)
|
|
result = _install(script, env, "--status", "--with-routectl")
|
|
assert result.returncode == 1
|
|
assert "needs an install" in result.stderr
|
|
|
|
|
|
def test_no_new_sudoers_line_exists() -> None:
|
|
"""The grant is a file mode systemd enforces; the pin is the absence of a sudoers rule.
|
|
|
|
`deploy/agent-sudoers` is still exactly one line (the staged installer), neither of the
|
|
two new units names sudo, and the operator page says the sentence out loud.
|
|
"""
|
|
assert _grant_lines(SUDOERS.read_text()) == [CANONICAL_GRANT], (
|
|
"phase 33 adds a write without adding a sudoers line; a new rule is the regression"
|
|
)
|
|
for name in ("dashboards-routectl.socket", "dashboards-routectl@.service"):
|
|
text = (DEPLOY / name).read_text()
|
|
# `sudo\b` is the word sudo by itself: the units' own "not a sudoers line" comment
|
|
# (the negation that is the point) does not match, a grant would.
|
|
assert not re.search(r"sudo\b", text.lower()), f"{name} grants nothing by sudoers"
|
|
block = INSTALLER.read_text().split("routectl_off() {", 1)
|
|
assert len(block) == 2, "the --without-routectl path is a function"
|
|
assert "sudoers" not in block[1].split("\n}\n", 1)[0].lower(), (
|
|
"the removal path must not touch sudoers either"
|
|
)
|
|
readme = (DEPLOY / "gateway-routectl-README.md").read_text()
|
|
assert "No sudoers line is involved" in readme
|
|
assert "0600" in readme, "the operator page names the mode that is the grant"
|
|
|
|
|
|
@pytest.mark.skipif(hasattr(os, "geteuid") and os.geteuid() == 0, reason="running as root")
|
|
def test_the_install_checks_the_agent_can_reach_the_socket(tmp_path: Path) -> None:
|
|
"""A socket the agent cannot connect to is a feature that fails at 03:00.
|
|
|
|
So the check is part of the install: with the check's `sudo -u` step failing, the install
|
|
must exit non-zero rather than report a success nobody can use, and the check must have
|
|
run (the stub log is the proof it connected as the agent uid, not just been skipped).
|
|
"""
|
|
script, call_log, env = _sandbox_installer(tmp_path)
|
|
sudo_stub = tmp_path / "stubs" / "sudo"
|
|
sudo_stub.write_text(
|
|
'#!/bin/sh\nprintf "%s %s\\n" "sudo" "$*" >> "$STUB_LOG"\nexit 1\n', encoding="utf-8"
|
|
)
|
|
sudo_stub.chmod(0o755)
|
|
result = _install(script, env, *ROUTECTL_FLAGS)
|
|
assert result.returncode == 1, "an unreachable socket is a failed install, not a silent success"
|
|
assert "cannot connect" in result.stderr
|
|
assert "sudo -u dashboards-agent python3" in call_log.read_text(), "the check ran as the agent uid"
|
|
|
|
|
|
# --- scripts/capture-fixtures.sh: the radiance pass (phase 34, task 01) --------------
|
|
#
|
|
# The script learns radiance through the same `--with-llm` gate as the other engines and
|
|
# carries no deployment detail of its own: RADIANCE_BASE_URL is the dial, `/metrics`
|
|
# joins the per-engine loop, and `/stats` + `/v1/models` are recorded in the same pass
|
|
# (radiance is a vLLM derivative; the vLLM engine and env var stay for other deployments).
|
|
# A stub `curl` answers the three endpoints from canned bodies, so the script's own
|
|
# branches run — verbatim bodies, the script's header, the at-rest hint, the
|
|
# never-overwrite-a-good-fixture rule — with no engine in sight.
|
|
|
|
RADIANCE_CAPTURE = SCRIPTS / "capture-fixtures.sh"
|
|
|
|
#: The stub `curl`: it drops the options the script passes (`-fsS`, `--max-time N`, `-o`),
|
|
#: records every call, and answers the engine's three read-only endpoints from `$CANNED_*`.
|
|
#: Nothing here can reach a network.
|
|
RADIANCE_CURL_STUB = """#!/bin/sh
|
|
printf '%s\\n' "$*" >> "$STUB_LOG"
|
|
if [ "${FAIL_CURL:-0}" = "1" ]; then
|
|
echo "curl: (7) Failed to connect" >&2
|
|
exit 7
|
|
fi
|
|
out=""
|
|
url=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--max-time) shift 2 ;;
|
|
-o) out="$2"; shift 2 ;;
|
|
-*) shift ;;
|
|
*) url="$1"; shift ;;
|
|
esac
|
|
done
|
|
[ -n "$out" ] || { echo "stub curl: no -o target" >&2; exit 2; }
|
|
case "$url" in
|
|
*/metrics) printf '%s' "$CANNED_METRICS" > "$out" ;;
|
|
*/stats) printf '%s' "$CANNED_STATS" > "$out" ;;
|
|
*/v1/models) printf '%s' "$CANNED_MODELS" > "$out" ;;
|
|
*) echo "stub curl: unexpected URL $url" >&2; exit 2 ;;
|
|
esac
|
|
"""
|
|
|
|
#: The seven per-position spec-acceptance lines: the phase pins position="0".."6".
|
|
RADIANCE_CANNED_METRICS = (
|
|
"# HELP vllm:num_requests_running Number of requests currently running on GPU.\n"
|
|
"# TYPE vllm:num_requests_running gauge\n"
|
|
'vllm:num_requests_running{model_name="juggernaut",engine="0"} 0\n'
|
|
'radiance:decode_tokens_per_second{model_name="juggernaut",engine="0"} 110.9\n'
|
|
+ "".join(
|
|
f'radiance:draft_position_acceptance_ratio{{model_name="juggernaut",engine="0",'
|
|
f'position="{i}"}} 0.7\n'
|
|
for i in range(7)
|
|
)
|
|
)
|
|
|
|
RADIANCE_CANNED_STATS = (
|
|
'{"model":"juggernaut","version":"1.15.1","uptime_s":67490,"queue_cap":24,'
|
|
'"in_flight":0,"kv_util":0.7241322994232178,"prefix_hit_rate":0.9572382569313049,'
|
|
'"draft_positions":[{"reached":701741,"accepted":516252,"rate":0.7356731329650114}],'
|
|
'"requests":[],"cards":[{"index":0,"name":"AMD Radeon AI PRO R9700",'
|
|
'"vram_used":32119517184,"vram_total":34208743424}]}'
|
|
)
|
|
|
|
RADIANCE_CANNED_STATS_BUSY = (
|
|
'{"model":"juggernaut","version":"1.15.1","uptime_s":67500,"queue_cap":24,'
|
|
'"in_flight":1,"kv_util":0.72,"prefix_hit_rate":0.95,'
|
|
'"draft_positions":[{"reached":701741,"accepted":516252,"rate":0.735}],'
|
|
'"requests":[{"id":"req-1","queue_pos":1}],'
|
|
'"cards":[{"index":0,"name":"AMD Radeon AI PRO R9700",'
|
|
'"vram_used":32119517184,"vram_total":34208743424}]}'
|
|
)
|
|
|
|
RADIANCE_CANNED_MODELS = (
|
|
'{"object":"list","data":[{"id":"juggernaut","object":"model","created":1793484879,'
|
|
'"owned_by":"radiance","max_model_len":131072,"capabilities":{"chat":true}}]}'
|
|
)
|
|
|
|
RADIANCE_BASE = "http://radiance.test:8000"
|
|
|
|
|
|
def _radiance_capture_sandbox(
|
|
tmp_path: Path,
|
|
*,
|
|
recorded: dict[str, str] | None = None,
|
|
fail_curl: bool = False,
|
|
busy: bool = False,
|
|
no_engine: bool = False,
|
|
) -> tuple[Path, Path, Path, dict[str, str]]:
|
|
"""A copy of the script, a canned radiance, and a PATH whose `curl` reaches only the canned.
|
|
|
|
Returns (script, fixtures dir, stub call log, env). The script is copied into
|
|
`<tmp>/scripts/` because it derives `tests/fixtures/` from its own location, which is
|
|
what keeps the repository's fixtures out of reach of a test.
|
|
"""
|
|
script = tmp_path / "scripts" / RADIANCE_CAPTURE.name
|
|
script.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(RADIANCE_CAPTURE, script)
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
fixtures.mkdir(parents=True, exist_ok=True)
|
|
for name, body in (recorded or {}).items():
|
|
(fixtures / name).write_text(body, encoding="utf-8")
|
|
|
|
stubs = tmp_path / "stubs"
|
|
stubs.mkdir()
|
|
stub = stubs / "curl"
|
|
stub.write_text(RADIANCE_CURL_STUB, encoding="utf-8")
|
|
stub.chmod(0o755)
|
|
log = tmp_path / "curl.log"
|
|
|
|
env = {
|
|
"PATH": f"{stubs}{os.pathsep}{os.environ['PATH']}",
|
|
"STUB_LOG": str(log),
|
|
"CANNED_METRICS": RADIANCE_CANNED_METRICS,
|
|
"CANNED_STATS": RADIANCE_CANNED_STATS_BUSY if busy else RADIANCE_CANNED_STATS,
|
|
"CANNED_MODELS": RADIANCE_CANNED_MODELS,
|
|
}
|
|
if not no_engine:
|
|
env["RADIANCE_BASE_URL"] = RADIANCE_BASE
|
|
if fail_curl:
|
|
env["FAIL_CURL"] = "1"
|
|
return script, fixtures, log, env
|
|
|
|
|
|
def _radiance_run(tmp_path: Path, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
script, _fixtures, _log, env = _radiance_capture_sandbox(tmp_path, **kwargs) # type: ignore[arg-type]
|
|
return subprocess.run([BASH, str(script), "--with-llm"], capture_output=True, text=True,
|
|
env=env, check=False)
|
|
|
|
|
|
def _dialled_urls(log: Path) -> list[str]:
|
|
urls: list[str] = []
|
|
for line in log.read_text().splitlines():
|
|
for token in line.split():
|
|
if token.startswith("http"):
|
|
urls.append(token)
|
|
return urls
|
|
|
|
|
|
def _assert_verbatim(path: Path, canned: str) -> None:
|
|
"""The engine's reply inside a fixture, claimed verbatim.
|
|
|
|
The script's header is comment text; the engine's reply is the file's complete tail —
|
|
so the reply is verbatim iff the file ends with it byte for byte and everything before
|
|
it is comment lines (the metrics body's own `# HELP` / `# TYPE` lines are part of the
|
|
reply and stay in the fixture).
|
|
"""
|
|
text = path.read_text()
|
|
assert text.endswith(canned), f"{path.name} lost the tail of the engine's reply"
|
|
prefix = text[: -len(canned)]
|
|
non_comment = [line for line in prefix.splitlines() if line and not line.startswith("#")]
|
|
assert not non_comment, f"{path.name} has non-comment text before the reply: {non_comment}"
|
|
assert prefix, f"{path.name} lost its verbatim header"
|
|
|
|
|
|
def test_the_radiance_capture_records_all_three_endpoints_verbatim(tmp_path: Path) -> None:
|
|
"""The happy path, run: three fixtures, each with the script's verbatim header, the body
|
|
byte-for-byte what the engine answered, and a dial to exactly the three read-only
|
|
endpoints — nothing else, in the documented order."""
|
|
result = _radiance_run(tmp_path)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
fixtures = tmp_path / "tests" / "fixtures"
|
|
assert {p.name for p in fixtures.iterdir()} == {
|
|
"metrics_radiance.txt", "stats_radiance.json", "models_radiance.json"}
|
|
for name in ("metrics_radiance.txt", "stats_radiance.json", "models_radiance.json"):
|
|
text = (fixtures / name).read_text()
|
|
assert text.startswith("# Verbatim"), f"{name} lost its verbatim header"
|
|
assert "./scripts/capture-fixtures.sh" in text, f"{name} names its recorder"
|
|
_assert_verbatim(fixtures / "metrics_radiance.txt", RADIANCE_CANNED_METRICS)
|
|
_assert_verbatim(fixtures / "stats_radiance.json", RADIANCE_CANNED_STATS)
|
|
_assert_verbatim(fixtures / "models_radiance.json", RADIANCE_CANNED_MODELS)
|
|
assert _dialled_urls(fixtures.parent.parent / "curl.log") == [
|
|
f"{RADIANCE_BASE}/metrics", f"{RADIANCE_BASE}/stats", f"{RADIANCE_BASE}/v1/models"]
|
|
# the capture at rest records `requests: []` — the script says so, so the operator
|
|
# re-runs with a request in flight to record its row shape
|
|
assert "requests[] was empty — re-run with a request in flight to record its shape" in result.stdout
|
|
|
|
|
|
def test_the_radiance_capture_says_nothing_to_re_run_when_a_request_is_in_flight(tmp_path: Path) -> None:
|
|
"""With a request in flight the hint is gone (it would send the operator to re-run a
|
|
capture that already has the row shape), and the header still records what it saw."""
|
|
result = _radiance_run(tmp_path, busy=True)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert "re-run with a request in flight" not in result.stdout
|
|
stats = (tmp_path / "tests" / "fixtures" / "stats_radiance.json").read_text()
|
|
assert 'requests":[{"id":"req-1","queue_pos":1}]' in stats
|
|
assert "in_flight was 1 at capture" in stats
|
|
|
|
|
|
def test_the_radiance_capture_without_an_engine_says_which_env_var_it_wants(tmp_path: Path) -> None:
|
|
"""No engine env var is not a silent no-op: the gate names the var, like the others."""
|
|
script, _fixtures, _log, env = _radiance_capture_sandbox(tmp_path, no_engine=True)
|
|
result = subprocess.run([BASH, str(script), "--with-llm"], capture_output=True, text=True,
|
|
env=env, check=False)
|
|
assert result.returncode == 1
|
|
assert "no GUFO_BASE_URL / VLLM_BASE_URL / RADIANCE_BASE_URL set" in result.stdout
|
|
|
|
|
|
def test_a_dead_radiance_engine_never_replaces_a_recorded_capture(tmp_path: Path) -> None:
|
|
"""The `slots_gufo.json` rule: a capture script must never turn three good fixtures into
|
|
three empty files, because then the shape tests pass against nothing (or fail for the
|
|
wrong reason) and the ground truth is gone."""
|
|
good = {"metrics_radiance.txt": "# old\nradiance:kept 1\n",
|
|
"stats_radiance.json": "# old\n{\"cards\":[]}\n",
|
|
"models_radiance.json": "# old\n{\"object\":\"list\"}\n"}
|
|
script, fixtures, _log, env = _radiance_capture_sandbox(tmp_path, recorded=good, fail_curl=True)
|
|
result = subprocess.run([BASH, str(script), "--with-llm"], capture_output=True, text=True,
|
|
env=env, check=False)
|
|
assert result.returncode == 1
|
|
for name, body in good.items():
|
|
assert (fixtures / name).read_text() == body, f"{name} was replaced by a dead engine"
|