#!/usr/bin/env python3 """B (R-633) and C (R-634) live on 9202, against v0.262.0.""" import json, os, sys, time HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(os.path.dirname(HERE), "update-night-2026-09-21")) import walk as w # noqa: E402 OUT = json.load(open(os.path.join(HERE, "live262.json"))) if os.path.exists( os.path.join(HERE, "live262.json")) else {} def save(tag, rec): OUT[tag] = rec json.dump(OUT, open(os.path.join(HERE, "live262.json"), "w"), ensure_ascii=False, indent=2) print("\n== " + tag + "\n" + json.dumps(rec, ensure_ascii=False, indent=2)[:1500], flush=True) def B(): """A remove sent DURING a restore must be refused; a remove after it must be VERIFIED.""" # gokapi is crash-looping from this morning's R-633 artefact — its config volume was removed, so # its binary can never start and the stack never settles. A subject that cannot reach a steady # state proves nothing about a guard that fires between steady states. privatebin is light, # reliable and was walked all night. app, sub = "privatebin", "paste" rec = {"scenario": "B (R-633/R-626)", "app": app} w.deploy(app, sub) w.wait_app(sub, "/", tries=40) w.backup_now(app) snaps = w.snapshots(app) rec["snapshots"] = len(snaps) # start a restore and, while it runs, ask to remove r = w.restore(app) rec["restore_started"] = str(r)[:200] time.sleep(2) code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": False}) rec["remove_during_restore"] = {"http": code, "answer": d} rec["refused_as_expected"] = code not in ("200", "202") # let the restore settle, then remove properly for _ in range(60): st = w.stack(app) if st.get("state") in ("running", "unhealthy", "stopped", "not_deployed"): break time.sleep(5) time.sleep(10) code, d = w.ctl("POST", f"/api/stacks/{app}/stop") time.sleep(8) code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": True, "remove_backups": True}) rec["remove_after_restore"] = {"http": code, "verified": (d.get("data") or {}).get("verified"), "reappeared": (d.get("data") or {}).get("reappeared_removed")} time.sleep(30) rec["containers_60s_later"] = w.guest( f"docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'").strip() save("B", rec) def C(): """A half-state — app.yaml on disk, deployed=false, no containers — must be REMOVABLE.""" app = "sparkyfitness" rec = {"scenario": "C (R-634)", "app": app} # build the exact measured shape by hand on the scratch guest: the stack dir with an app.yaml # and no containers, which is what two failed deploys left behind twice. rec["setup"] = w.guest(f'''set -e mkdir -p /opt/docker/stacks/{app} cp /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/{app}/docker-compose.yml /opt/docker/stacks/{app}/ 2>/dev/null || true printf 'desired_state: stopped\\ninstalled_images:\\n' > /opt/docker/stacks/{app}/app.yaml ls -la /opt/docker/stacks/{app}/''').strip()[-300:] w.ctl("POST", "/api/stacks/rescan") time.sleep(6) st = w.stack(app) rec["before"] = {"deployed": st.get("deployed"), "state": st.get("state")} code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": True}) rec["remove"] = {"http": code, "answer": str(d)[:300]} rec["accepted"] = code == "200" time.sleep(5) rec["leftovers"] = w.guest(f"ls /opt/docker/stacks/{app}/app.yaml 2>/dev/null || echo NONE").strip() save("C", rec) if __name__ == "__main__": w.login() for fn in (B, C): try: fn() except Exception as e: save(fn.__name__ + "-ERROR", {"error": f"{type(e).__name__}: {e}"})