#!/usr/bin/env python3 """undocase.py — the brief's ONE undo case per app on 9202 (scratch guest). The load failing is the case (the docmost method, `audits/night-2026-09-26/D/Da-*`): an object the OLD major has and 18 does not — the `adminpack` extension (removed in PostgreSQL 17) — is created in the app's own database, the guarded Update is pressed (the drill catalog's step carries the conversion mark), the load into 18 must fail, and the BOX must undo it by itself: old pin, old datadir, `undone`, the seed read back. The extension is dropped again afterwards, so the real move that follows converts clean data. Evidence: box//undo.txt + box//undo-verdict.json. Never prints a secret. """ import json, os, sys, time import walk as w sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import upgrade_fixtures_box as fixtures, upgrade_fixtures_box28 as _f28 FX = dict(_f28.FIXTURES28); FX.update(fixtures.FIXTURES) # the box set wins, as upgrade_boxport.get() decides app, sub, svc, user, db = sys.argv[1:6] EVD = f"{w.EV}/box/{app}"; os.makedirs(EVD, exist_ok=True) log = open(f"{EVD}/undo.txt", "a", buffering=1) def say(*a): w.say(*a); log.write(" ".join(map(str, a)) + "\n") w.login() toks = json.load(open(w.SC + "/seed-tokens-box.json")) if isinstance(toks.get(app), dict) and toks[app].get("__generated"): w.GENERATED[app] = toks[app]["__generated"] psql = lambda sql: w.guest(f"docker exec {svc} psql -U {user} -d {db} -Atc \"{sql}\" 2>&1").strip() pg = lambda: w.guest(f"docker exec {svc} sh -c 'cat $PGDATA/PG_VERSION' 2>&1").strip() say(f"# undo case (the load fails) — {time.strftime('%Y-%m-%dT%H:%M:%S%z')}") st = w.stack(app); before = (st.get("app_config") or {}).get("pinned_images") say(f"before: PG_VERSION={pg()} pinned={before}") say("before: seed reads back through the front door =", FX[app].verify(w, sub, toks[app], say)) say("setup: CREATE EXTENSION adminpack ->", psql("CREATE EXTENSION IF NOT EXISTS adminpack")) say("setup: extensions now:", psql("select string_agg(extname, ' ' order by extname) from pg_extension")) ctrl = w.sh(["ssh", "demo-hp", "pct exec 9202 -- docker run --rm postgres:18-alpine sh -c 'ls /usr/local/share/postgresql/extension/ | grep -c ^adminpack || true'"]).stdout.strip() say(f"control: 18 has no adminpack: {ctrl}") since = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip() res = w.press_update(app, poll=1, cap_s=1800) for p in res.get("phases", []): say(f" phase +{p['t']}s {p['phase']} | err={p['error']}") say("result:", json.dumps({k: res.get(k) for k in ("accepted", "http", "duration_s", "final_phase", "update_error", "hold_reason", "state")})) time.sleep(10) st = w.stack(app); after = (st.get("app_config") or {}).get("pinned_images") after_pg = pg() read = FX[app].verify(w, sub, toks[app], say) say(f"after: PG_VERSION={after_pg} pinned={after} state={st.get('state')}") say("after: seed reads back through the front door =", read) conv = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update {app}|CONVERT|UNDO' | grep -v DEBUG | cut -c1-400") log.write("controller lines:\n" + conv + "\n") say("cleanup: DROP EXTENSION adminpack ->", psql("DROP EXTENSION IF EXISTS adminpack")) verdict = {"app": app, "case": "the load into 18 fails (adminpack, an object 18 lacks)", "venue": "box 9202, the product's guarded Update", "final_phase": res.get("final_phase"), "duration_s": res.get("duration_s"), "pg_version_after": after_pg, "pinned_before": before, "pinned_after": after, "seed_read_after": read, "undone_line": next((l.split("] ", 2)[-1] for l in conv.splitlines() if "UNDONE" in l), ""), "ok": res.get("final_phase") == "undone" and read is True and after == before, "measured_at": since} json.dump(verdict, open(f"{EVD}/undo-verdict.json", "w"), indent=2) say(f"RESULT undo case ok={verdict['ok']} final_phase={res.get('final_phase')} PG_VERSION={after_pg} seed_after={read}")