#!/usr/bin/env python3 """h5_live.py seed | prove — R-1256 H5 live proof on scratch 9202 (night 2026-10-28), through the front doors. seed : install vikunja, seed project C1 through its API, put a 1 GiB ballast file in its second volume (vikunja_db, so the restore's second volume takes seconds — the put-back then comes long after the first volume's refusal). The night backup (02:30) then makes the restore point. prove: seed project C2 (newer than the backup); a created-only container holds the FIRST volume (vikunja_data) so `docker volume rm -f` fails; a watcher removes it as soon as the controller logs the refusal; press the household's restore. Want: the first volume is NOT written into (log), the restore fails, the put-back brings the live data back (C1 AND C2 read back), the app runs, NO hold. State (passwords) stays in the scratchpad, never printed.""" import json, os, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") os.environ.setdefault("SC", "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/rt-9202") os.environ.setdefault("TARGET", "9202") import box_walk as w import upgrade_fixtures_box as fb EV = "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/night/p1/h5" STATE = os.path.join(EV, ".state.json") fx = fb.FIXTURES["vikunja"] SUB = getattr(fx, "sub", "vikunja") APP = "vikunja" w.login() if sys.argv[1] == "seed": assert w.deploy(APP, SUB), "deploy failed" t1 = fx.seed(w, SUB, w.say) assert t1, "seed C1 failed" vols = w.guest("docker volume ls --format '{{.Name}}' | grep -E '^vikunja' | sort").split() w.say(f"volumes: {vols}") dbv = [v for v in vols if v.endswith("_db")][0] w.say(w.guest(f"docker run --rm -v {dbv}:/x alpine sh -c 'dd if=/dev/urandom of=/x/r1256-ballast.bin bs=1M count=1024 2>&1 | tail -1; ls -la /x'", timeout=900)) json.dump({"c1": t1, "vols": vols}, open(STATE, "w")); os.chmod(STATE, 0o600) w.say(f"C1 readback: {fx.verify(w, SUB, t1, w.say)}") elif sys.argv[1] == "prove": st = json.load(open(STATE)) snaps = w.snapshots(APP) w.say(f"snapshots offered: {json.dumps(snaps)[:400]}") assert snaps, "no restore point yet" t2 = fx.seed(w, SUB, w.say) # C2: newer than the backup — the put-back must bring it back assert t2, "seed C2 failed" st["c2"] = t2; json.dump(st, open(STATE, "w")) datav = [v for v in st["vols"] if v.endswith("_data")][0] w.say("blocker: " + w.guest(f"docker rm -f r1256-blocker >/dev/null 2>&1; docker create --name r1256-blocker -v {datav}:/x alpine true")) w.say("rm -f with the blocker (premise): " + w.guest(f"docker volume rm -f {datav} 2>&1; echo rc=$?")) w.guest("rm -f /root/r1256-watch.log; nohup sh -c 'timeout 1800 docker logs -f --since 1s felhom-controller 2>&1 | " f"grep -m1 \"Failed to remove volume {datav} before the restore\" > /root/r1256-watch.log; date -u +%T.%N >> /root/r1256-watch.log; " "docker rm -f r1256-blocker >> /root/r1256-watch.log 2>&1; date -u +%T.%N >> /root/r1256-watch.log' >/dev/null 2>&1 &") time.sleep(2) t0 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) res = w.restore(APP) w.say(f"restore result: {json.dumps(res)[:1500]}") w.say("watcher: " + w.guest("cat /root/r1256-watch.log")) w.say("controller log: " + w.guest(f"docker logs --since {t0} felhom-controller 2>&1 | grep -E 'vikunja|R-1256|put-back|put back|Failed to remove' | tail -40")) st_now = w.stack(APP) w.say(f"after: state={st_now.get('state')} hold={st_now.get('hold_reason')!r}") w.say(f"C1 readback: {fx.verify(w, SUB, st['c1'], w.say)}") w.say(f"C2 readback: {fx.verify(w, SUB, t2, w.say)}") code, d = w.ctl("GET", "/api/backup/restore-status") w.say(f"restore record: {json.dumps(d)[:800]}") w.say("blocker left: " + w.guest("docker ps -a --filter name=r1256-blocker --format '{{.Names}}'")) w.say("copies: " + w.guest("find /mnt/felhom-drives/scratch_hdd/backups/restore-undo -maxdepth 2")) elif sys.argv[1] == "teardown": w.guest("docker rm -f r1256-blocker >/dev/null 2>&1; rm -f /root/r1256-watch.log") w.remove(APP) open(os.path.join(EV, f"{sys.argv[1]}.log"), "a").write("\n".join(w.LOG) + "\n")