#!/usr/bin/env python3 """unitprobe2.py — R-1269 / D47 AFTER the fix: the sweep's order (catalog-sweep-2026-10-24/tools/unitprobe.py), then the household's way back: „Visszaállítás" on the set-aside unit of the earlier install (restore page, „Megőrzött másolatok"). Endpoint-level, the requests the pages send; nothing is planted by hand, no secret is read or printed (units are compared by sha256 of every file). usage: SC=… SWEEP_EV= TARGET=9202 DRIVE_ROOT=… python3 unitprobe2.py """ import json, os, re, 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 UNIT = "/mnt/sys_drive/felhom-data/backups/primary/{app}" ASIDE = "/mnt/sys_drive/felhom-data/backups/set-aside/{app}" def sums(path): return sw.g(f"cd {path} 2>/dev/null && find . -type f | sort | xargs -r sha256sum") def page(path): sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip() r = w.sh(["curl", "-sk", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", f"{w.BASE}{path}"]) return r.stdout or "" def post_form(path, fields): sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip() csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip() args = ["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-X", "POST", "--data-urlencode", f"_csrf={csrf}"] for k, v in fields.items(): args += ["--data-urlencode", f"{k}={v}"] r = w.sh(args + [f"{w.BASE}{path}"]) head = (r.stdout or "").splitlines() return (head[0] if head else "?"), [l for l in head if l.lower().startswith("location")] def wait_restore(rec_key, rec): t0, last = time.time(), None while time.time() - t0 < 1800: time.sleep(5) c, d = w.ctl("GET", "/api/backup/restore-status") dd = (d.get("data") or {}) if isinstance(d, dict) else {} last = dd if not dd.get("running") and time.time() - t0 > 10: break rec[rec_key] = {"seconds": round(time.time() - t0), "status": json.dumps(last, ensure_ascii=False)[:700]} def main(app): d = os.path.join(sw.EV, "_unitprobe2", 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) rec = {"app": app} w.login() t_start = int(time.time()) try: rec["install"] = w.deploy(app, sub) and w.wait_app(sub, "/", tries=90, delay=5) tok = fx.seed(w, sub, say) rec["seed"] = bool(tok) sw.backup_now(rec, say) s1 = sums(UNIT.format(app=app)) open(os.path.join(d, "unit-1-after-backup.sha256"), "w").write(s1) sw.remove(app, keep=True, say=say) t_re = time.time() rec["reinstall"] = w.deploy(app, sub, dict(w.GENERATED.get(app) or {})) aside = "" for _ in range(60): time.sleep(10) aside = sw.g(f"docker logs --since {int(t_re)} felhom-controller 2>&1 | grep -E '{app}: the recovery unit of an earlier install' | cut -c1-400") if aside.strip(): break rec["set_aside_line"] = aside.strip() say(" set aside: " + (aside.strip()[:300] or "NONE within 10 min")) dirs = sw.g(f"ls -1 {ASIDE.format(app=app)} 2>/dev/null").split() rec["set_aside_dirs"] = dirs s2 = sums(ASIDE.format(app=app) + "/" + dirs[0]) if dirs else "" open(os.path.join(d, "unit-2-set-aside.sha256"), "w").write(s2) rec["set_aside_byte_identical"] = bool(s1.strip()) and s1 == s2 say(f" the set-aside unit is install A's unit byte for byte: {rec['set_aside_byte_identical']} ({len(s1.splitlines())} files)") open(os.path.join(d, "unit-3-primary-now.sha256"), "w").write(sums(UNIT.format(app=app))) html = page("/backups/restore") m = re.search(r'action="/backup/kept/restore".*?name="id" value="([0-9a-f]{16})"', html, re.S) rec["restore_page_offers"] = bool(m) sec = html[html.find("Megőrzött másolatok"):][:1500] if "Megőrzött másolatok" in html else "" open(os.path.join(d, "restore-page-kept-section.html"), "w").write(sw.scrub(sec)) if not m: say(" the restore page offers NO restore of the set-aside unit") return head, loc = post_form("/backup/kept/restore", {"id": m.group(1), "confirm": "1"}) say(f" [6] POST /backup/kept/restore -> {head} {loc[:1]}") wait_restore("restore", rec) say(" restore status: " + rec["restore"]["status"][:300]) time.sleep(20) rec["state_after_restore"] = w.stack(app).get("state") rec["item_after_restore"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) rec["db_auth_errors_after_restore"] = sw.g( f"for c in $(docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'); do " f"docker logs --since 5m $c 2>&1 | grep -ciE 'password authentication failed|access denied for user' | sed \"s/^/$c: /\"; done").strip() say(" state after restore: %s, item: %s, db auth errors: %s" % (rec["state_after_restore"], rec["item_after_restore"], rec["db_auth_errors_after_restore"])) finally: rec["controller_lines"] = sw.scrub(sw.g( f"docker logs --since {t_start} felhom-controller 2>&1 | grep -E '{app}' | grep -iE 'set aside|restor|captured|install id|D47|D48|R-1270|refused' | cut -c1-300")) sw.remove(app, keep=False, say=say) rec["after_remove"] = sw.leftovers(app, sub) rec["set_aside_after_remove"] = sw.g(f"ls -1 {ASIDE.format(app=app)} 2>&1 | head -3").strip() json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1) if __name__ == "__main__": main(sys.argv[1])