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
gates / gates (push) Successful in 13m3s
This commit is contained in:
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])
|
||||
Reference in new issue
Block a user