D44/D45 = all picks applied (R-1265, R-1266 closed; R-1280–R-1283 opened); R-232 dated check done; release-2026-10-26 evidence: fix A/B live on rc.6, D47 set-aside alarm on rc.7
gates / gates (push) Successful in 13m3s

This commit is contained in:
admin committed 2026-10-25 17:57:11 +01:00
1 parent a86ab64f6c
commit ade357a14e
38 files changed
+4907 -58

No files matched your search

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""escrow_ceremony.py — the recovery-code ceremony on Tester 1 through the wizard's own calls (operator yes,
2026-10-25): login → GET /api/escrow/preflight → POST /api/escrow/start (password re-auth) → poll /api/escrow/status →
POST /api/escrow/claim (the ONE-SHOT code) → POST /api/escrow/confirm (the two words the agent names). The code is
NEVER printed or logged: it goes only into the 0600 file named by argv[1] (the caller hands it to `sec put`, which
shreds it). Run through `sec run TESTER1_PASSWORD -- python3 escrow_ceremony.py <codefile>`."""
import json, os, re, subprocess, sys, time
PW = os.environ["TESTER1_PASSWORD"]
SSH = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "root@192.168.0.154"]
HOST = "Host: felhom.enkicsifelhom.hu"
def run(args, inp=None):
return subprocess.run(args, input=inp, capture_output=True, text=True, timeout=300)
def base():
r = run(SSH + ["pct exec 9201 -- ip -4 -o addr show eth0"])
return "https://" + re.search(r"inet (\d+\.\d+\.\d+\.\d+)/", r.stdout).group(1)
def say(*a):
print(time.strftime("%H:%M:%SZ", time.gmtime()), *a, flush=True)
B = base()
h = run(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", HOST, "-X", "POST", "--data-urlencode", "password@-", f"{B}/login"], inp=PW).stdout
sess = re.search(r"felhom_session=[A-Za-z0-9._-]+", h).group(0)
page = run(["curl", "-sk", "-L", "-H", HOST, "-H", f"Cookie: {sess}", f"{B}/backup/escrow"]).stdout
csrf = re.search(r'<meta name="csrf-token" content="([^"]+)"', page).group(1)
def api(method, path, form=None, js=None):
a = ["curl", "-sk", "-H", HOST, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}", "-X", method, "-w", "\n%{http_code}"]
inp = None
if form is not None:
a += ["--data-urlencode", f"_csrf={csrf}", "--data-urlencode", "password@-"]
inp = form
if js is not None:
a += ["-H", "Content-Type: application/json", "--data-binary", "@-"]
inp = json.dumps(js)
out = run(a + [f"{B}{path}"], inp=inp).stdout
body, _, code = out.rpartition("\n")
try:
return code, json.loads(body)
except ValueError:
return code, {"raw": body[:200]}
c, pre = api("GET", "/api/escrow/preflight")
say("preflight", c, json.dumps(pre, ensure_ascii=False)[:300])
c, st = api("POST", "/api/escrow/start", form=PW)
say("start", c, json.dumps(st, ensure_ascii=False)[:300])
if c != "200":
sys.exit(1)
KEEP = ("phase", "claimable", "claimed", "restic_pw_sealed", "uploaded", "awaiting_confirm", "key_fingerprint", "detail")
for _ in range(120):
time.sleep(2)
c, s = api("GET", "/api/escrow/status")
dd = s.get("data") or {}
if dd.get("claimable") is True or dd.get("phase") in ("failed", "unclaimed_void"):
break
say("status", c, json.dumps({k: dd.get(k) for k in KEEP}, ensure_ascii=False))
c, cl = api("POST", "/api/escrow/claim")
d = cl.get("data") or {}
code = d.get("recovery_code", "")
say("claim", c, "code received:", bool(code), "confirm_required:", d.get("confirm_required"), "positions:", d.get("verify_positions"))
if not code:
sys.exit(2)
fd = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, code.encode())
os.close(fd)
words = code.split("-")
if d.get("confirm_required"):
p = d["verify_positions"]
c, cf = api("POST", "/api/escrow/confirm", js={"words": [words[p[0] - 1], words[p[1] - 1]]})
say("confirm", c, json.dumps(cf, ensure_ascii=False)[:300])
code, words = "", []
for _ in range(30):
time.sleep(2)
c, s = api("GET", "/api/escrow/status")
dd = s.get("data") or {}
if dd.get("uploaded") is True or dd.get("phase") in ("failed", "unconfirmed_void"):
break
say("final status", c, json.dumps({k: dd.get(k) for k in KEEP}, ensure_ascii=False))
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""stableprobe.py — fix A, the healthy half: a normal unit restore of a healthy throwaway app → the page says success,
the undo copy is there right after, and gone after the 3-minute stable window. The controller's DEBUG lines say why
any sample was not ok. usage: SC=… SWEEP_EV=… TARGET=9202 python3 stableprobe.py <app>"""
import json, os, sys, time
TOOLS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools")
sys.path.insert(0, TOOLS)
import sweep as sw # noqa: E402
w, fxm = sw.w, sw.fxm
def main(app):
d = os.path.join(sw.EV, "stableprobe", app)
os.makedirs(d, exist_ok=True)
log = open(os.path.join(d, "walk.log"), "a")
def say(*a):
line = time.strftime("%H:%M:%SZ", time.gmtime()) + " " + " ".join(str(x) for x in a)
print(line, flush=True)
log.write(sw.scrub(line) + "\n")
log.flush()
w.say = say
fx = fxm.FIXTURES[app]
sub = getattr(fx, "sub", app)
undo = f"/mnt/sys_drive/felhom-data/backups/restore-undo/{app}"
rec = {}
t0 = int(time.time())
w.login()
try:
rec["install"] = w.deploy(app, sub) and w.wait_app(sub, "/", tries=90, delay=5)
tok = fx.seed(w, sub, say)
sw.backup_now(rec, say)
sw.restore_unit(app, rec, say)
say(" restore: " + rec["restore"].get("status", "")[:400])
rec["copy_right_after"] = sw.g(f"ls -1 {undo} 2>&1").strip()
say(" the copy right after: " + rec["copy_right_after"])
for _ in range(36):
time.sleep(10)
if "No such file" in sw.g(f"ls -1 {undo} 2>&1"):
break
rec["copy_after_window"] = sw.g(f"ls -1 {undo} 2>&1").strip()
rec["item"] = bool(tok) and bool(fx.verify(w, sub, tok, say))
say(f" the copy after the window: {rec['copy_after_window']}; item {rec['item']}")
finally:
rec["lines"] = sw.scrub(sw.g(f"docker logs --since {t0} felhom-controller 2>&1 | grep -E 'stable-start sample {app}|{app}: ran stably|{app}.*not start|{app}.*did not run stably|Restore-from-unit completed: {app}' | cut -c1-260 | head -40"))
sw.remove(app, keep=False, say=say)
rec["after_remove"] = sw.leftovers(app, sub)
json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1)
if __name__ == "__main__":
main(sys.argv[1])