Files
felhom.eu/documentation/audits/debian13-restic-2026-10-16/tools/drill.py
T

117 lines
4.7 KiB
Python

#!/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()