69f896d3cd
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).
93 lines
3.8 KiB
Python
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)")
|