#!/usr/bin/env python3 """drill.py — R-1105 Part 2 on demo-hp guest 9201, THROUGH THE PRODUCT (the endpoints the dashboard's buttons post). Runs on DooPlex. Env: SC (0600 dir with .ctlpw), EV (evidence dir), TARGET=9201. Steps (one per invocation): restore-old the product's off-site restore („Visszaállítás a távoli tárolóból", unit only) BEFORE any 0.18 backup: restic 0.18.1 restores the newest snapshot, which restic 0.14.0 made offsite-run „Távoli mentés most" (POST /backup/offbox/run: dumps + units + push), polled to its end restore APP the household's full off-site restore: prepare (size), confirm → download, then „Teljes visszaállítás" (POST /backup/offbox/reconstitute), each polled to its end delcopy APP remove the verification copy (POST /backup/offbox/verify-copy/delete) Nothing here writes to an app's data except what those buttons write. """ import json import os import re import sys import time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import box_walk as w # noqa: E402 EV = os.environ["EV"] LOG = open(os.path.join(EV, "drill-console.txt"), "a", buffering=1) _say = w.say def say(*a): _say(*a) LOG.write(time.strftime("%H:%M:%S ") + " ".join(str(x) for x in a) + "\n") w.say = say def form(path, fields): """A dashboard FORM post with the session's CSRF — exactly what the button sends.""" 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}"], timeout=180) head = (r.stdout or "").split("\n")[0].strip() loc = next((l.split(":", 1)[1].strip() for l in (r.stdout or "").split("\n") if l.lower().startswith("location:")), "") say(f"POST {path} {fields} -> {head} {loc}") return head, loc def wait_restore(cap=3600): t0, last = time.time(), None while time.time() - t0 < cap: code, d = w.ctl("GET", "/api/backup/restore-status") dd = (d or {}).get("data") or d or {} cur = (dd.get("running"), dd.get("op"), dd.get("ok"), (dd.get("message") or "")[:300]) if cur != last: say(f" +{time.time()-t0:7.1f}s restore-status {cur}") last = cur if not dd.get("running") and time.time() - t0 > 4: return dd time.sleep(3) say(" restore did not end within the cap") return {} def offsite_status(): return w.ctl("GET", "/backup/offbox/status")[1] def offsite_run(cap=3600): before = offsite_status() say(f"off-site before: status={before.get('status')} last_run={before.get('last_run')} snapshots={before.get('snapshots')}") form("/backup/offbox/run", {}) t0, last = time.time(), None while time.time() - t0 < cap: s = offsite_status() p = s.get("progress") or {} cur = (s.get("status"), p.get("phase"), p.get("current_app"), round(p.get("percent") or 0)) if cur != last: say(f" +{time.time()-t0:7.1f}s {cur} files {p.get('files_done')}/{p.get('total_files')} bytes {p.get('done_human')}/{p.get('total_human')}") last = cur if s.get("last_run") != before.get("last_run") and not p.get("active") and s.get("status") != "running": say(f"off-site after: status={s.get('status')} last_run={s.get('last_run')} duration={s.get('last_duration')} " f"snapshots={s.get('snapshots')} size={s.get('repo_size_human')} error={s.get('last_error')!r}") return s time.sleep(3) say("off-site run did not end within the cap") return {} def main(): step = sys.argv[1] w.login() if step == "restore-old": app = sys.argv[2] form("/backup/offbox/restore", {"app": app}) say(json.dumps(wait_restore())) elif step == "offsite-run": offsite_run() elif step == "restore": app = sys.argv[2] head, loc = form("/backup/offbox/restore", {"app": app, "mode": "full"}) m = re.search(r"full_size=([^&]+)", loc) say(f" prepared: size {m.group(1) if m else '?'}") form("/backup/offbox/restore", {"app": app, "mode": "full", "confirm": "1"}) say(json.dumps(wait_restore())) form("/backup/offbox/reconstitute", {"app": app, "confirm": "1"}) say(json.dumps(wait_restore())) elif step == "delcopy": form("/backup/offbox/verify-copy/delete", {"stack": sys.argv[2], "confirm": "1"}) else: sys.exit(f"unknown step {step}") if __name__ == "__main__": main()