Files
felhom.eu/documentation/tests/campaign10-evidence-2026-07-31/runner/deploy_apps.py
T
admin 69f896d3cd Campaign 10 Phase B: 27 cycles, 586 invariant checks, 0 violations
Ran the soak on the Phase A rig. Ended on its own deadline — no watchdog halt,
no atom exception, no I11 breach.

I1 28+28 pairs, I2 28+28 pairs, I3 56, I4 56, I5/I6 28 each, I7 28, I10 135,
I11 28. Zero violations. The row counts are themselves the no-silent-skip check:
I3/I4 twice per cycle (both drives), I10 = 5 secret-class fields x 27, REBOOT on
cycles 7/14/21 only.

I7 is the headline: 28 restores, 28 correct discriminators — never stale, never
empty. RTO (Tier 1, rallly, 66 MB): min 38.8s, median 42.0s, p90 42.5s, max
44.3s. That is the S band's lower end ONLY; the 5.5s spread over 28 runs says
fixed work dominates, so nothing extrapolates to M or L. RPO not measured.

Every atom and invariant was proven BY HAND before automation — the runner
asserts nothing that was not first observed live.

Caught a Phase A gap before starting: no app had HDD_PATH, so all data sat on the
system disk and I3 could never have fired. Deployed calibre-web onto adatok
first; otherwise the run would have produced 27 green cycles that tested nothing
cross-drive.

Investigated and DISPROVED a suspected defect (audit 5.2): /api/disks reports
state=attached for a physically absent drive, and intermediary.go:230 really does
compute presence from State=="attached". It is inert — planDriveGates only gates
paths under /mnt/felhom-drives/ and uses BoundUnderParent there, which was
correctly false. The gate fired; the storage page showed "Meghajtó leválasztva".
No R-n minted.

Honest gaps: 6 of ~12 atom families ran. Not run — Tier 3 (structurally
un-isolatable), abort-fs-in-place, kill-agent-mid-backup, hard-reset-mid-write,
reboot-VM, both concurrency atoms, fill-drive-near-full. I8 not checked, I9 not
automated (cited from the tester-gate run, not re-claimed). kill_controller is
NOT mid-backup and reboot_guest never interleaved with a detach. 27 cycles does
not answer the brief's question about drift at the thirty-eighth.

Teardown still OWED, including hub customer c10-soak (disposition: DELETE).
2026-08-01 15:45:13 +02:00

93 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Campaign 10 — deploy apps through the REAL endpoints.
Mirrors the controller's own generator semantics (internal/stacks/deploy.go:870 generateValue):
password:N -> N random alphanumerics
hex:N -> N random BYTES, hex-encoded (2N chars)
base64key:N-> "base64:" + base64(N random bytes)
static:X -> X
Runs on DooPlex; talks to the guest via ssh -J demo-hp + pct exec + /root/c10api.sh.
Secrets are written to a 0600 manifest, never printed.
"""
import base64, json, os, secrets, string, subprocess, sys
APPS = sys.argv[1:] or ["rallly", "homebox", "grafana", "papra"]
DOMAIN = "c10.felhom.eu"
HOME = os.path.expanduser("~/.config/campaign10")
HOSTPW = json.load(open(os.path.join(HOME, "host-recovery.json")))["password"]
def ssh(cmd, timeout=900):
full = ["sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=25", "-J", "demo-hp", "root@192.168.0.105",
"LC_ALL=C " + cmd]
env = dict(os.environ, SSHPASS=HOSTPW, LC_ALL="C")
return subprocess.run(full, capture_output=True, text=True, timeout=timeout, env=env).stdout
def api(method, path, body=None):
if body is None:
return ssh(f"pct exec 9201 -- /root/c10api.sh {method} {path} 2>/dev/null")
b64 = base64.b64encode(json.dumps(body).encode()).decode()
# body travels base64 so no quoting/locale can mangle a secret
return ssh("pct exec 9201 -- bash -c \"echo %s | base64 -d > /tmp/.b && "
"/root/c10api.sh %s %s \\\"\\$(cat /tmp/.b)\\\" --json; shred -u /tmp/.b\" 2>/dev/null"
% (b64, method, path))
def gen(spec):
kind, _, param = spec.partition(":")
if kind == "password":
al = string.ascii_letters + string.digits
return "".join(secrets.choice(al) for _ in range(int(param)))
if kind == "hex":
return secrets.token_bytes(int(param)).hex()
if kind == "base64key":
return "base64:" + base64.b64encode(secrets.token_bytes(int(param))).decode()
if kind == "static":
return param
raise ValueError("unknown generator %r" % spec)
manifest_path = os.path.join(HOME, "app-secrets.json")
manifest = json.load(open(manifest_path)) if os.path.exists(manifest_path) else {}
for app in APPS:
raw = api("GET", f"/api/stacks/{app}/deploy-fields")
try:
meta = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])["data"]["metadata"]
except Exception as e:
print(f"{app:12s} FIELDS-FAIL {e} :: {raw[:160]!r}")
continue
values, classes = {}, {}
for f in meta.get("deploy_fields", []):
ev, typ, spec, dflt = f["env_var"], f.get("type", ""), f.get("generate", ""), f.get("default", "")
if typ == "domain":
values[ev] = DOMAIN
elif spec:
values[ev] = gen(spec)
elif dflt:
values[ev] = dflt
elif f.get("required"):
print(f"{app:12s} WARN required field {ev} has no default/generator")
if typ in ("secret", "password"):
classes[ev] = {"type": typ, "data_key": bool(f.get("data_key")), "label": f.get("label", "")}
manifest[app] = {"values": values, "secret_classes": classes,
"subdomain": values.get("SUBDOMAIN", ""), "domain": DOMAIN}
out = api("POST", f"/api/stacks/{app}/deploy", {"values": values})
ok = '"ok":true' in out
print(f"{app:12s} deploy -> {'OK' if ok else 'FAIL'} "
f"secret={sum(1 for c in classes.values() if c['type']=='secret')} "
f"password={sum(1 for c in classes.values() if c['type']=='password')} "
f"data_key={sum(1 for c in classes.values() if c['data_key'])}")
if not ok:
print(" ", out.strip()[:300])
with open(manifest_path, "w") as fh:
json.dump(manifest, fh, indent=1)
os.chmod(manifest_path, 0o600)
print("\nsecrets + secret-class map written to", manifest_path, "(0600, not printed)")