127 lines
6.1 KiB
Python
127 lines
6.1 KiB
Python
#!/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=<evidence dir> TARGET=9202 DRIVE_ROOT=… python3 unitprobe2.py <app>
|
|
"""
|
|
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])
|