153 lines
8.0 KiB
Python
153 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""fixprobe.py — fixes A and B live on a throwaway Vikunja this run installs (never a household's data).
|
|
|
|
A (the undo copy only after a stable start): the app's unit edited to run `busybox` with `sleep 20; exit 1` (restart
|
|
policy on, no healthcheck — it reads „running" for 20 s, then crashes and restarts) → „Visszaállítás": the page says
|
|
not started yet, the undo copy stays, the operator line is logged once; the original unit back → a normal restore:
|
|
the copy is there right after, gone after the 3-minute window.
|
|
B (the `.fab` import downloads first): the app exported through the page's API, a COPY of the bundle under its own name
|
|
with the image tag edited to one that does not exist → import: refused, the containers never restarted (StartedAt), the
|
|
volumes untouched (their CreatedAt), the item still there. The test bundle and the export are removed after.
|
|
The unit / bundle edits are the only hand-made changes (`pct exec`, files only).
|
|
|
|
usage: SC=… SWEEP_EV=… TARGET=9202 DRIVE_ROOT=… python3 fixprobe.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
|
|
APP, REPO = "vikunja", "vikunja/vikunja"
|
|
NS = "/mnt/sys_drive/felhom-data"
|
|
UNIT = f"{NS}/backups/primary/{APP}"
|
|
ORIG = f"{NS}/backups/fix-a-test-original/{APP}"
|
|
UNDO = f"{NS}/backups/restore-undo/{APP}"
|
|
DRIVE = os.environ.get("DRIVE_ROOT", "/mnt/felhom-drives/scratch_hdd")
|
|
|
|
|
|
def started():
|
|
return sw.g(f"for c in $(docker ps -a --filter label=com.docker.compose.project={APP} -q); do "
|
|
"docker inspect -f '{{.Name}} {{.State.StartedAt}} {{.RestartCount}} {{.State.Status}}' $c; done").strip()
|
|
|
|
|
|
def vols():
|
|
return sw.g(f"for v in $(docker volume ls -q --filter label=com.docker.compose.project={APP}); do "
|
|
"docker volume inspect -f '{{.Name}} {{.CreatedAt}}' $v; done").strip()
|
|
|
|
|
|
def sums(p):
|
|
return sw.g(f"cd {p} 2>/dev/null && find . -type f | sort | xargs -r sha256sum")
|
|
|
|
|
|
def main():
|
|
d = os.path.join(sw.EV, "fixprobe")
|
|
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 = {}
|
|
t_start = int(time.time())
|
|
w.login()
|
|
test_fab = ""
|
|
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)
|
|
sw.g(f"rm -rf {ORIG}; mkdir -p {NS}/backups/fix-a-test-original && cp -a {UNIT} {ORIG}")
|
|
s0 = sums(UNIT)
|
|
|
|
# --- A: an app that runs 20 s, crashes, restarts
|
|
out = sw.g(f"cd {UNIT} && sed -i -E 's#{REPO}:[^\"[:space:]]*#busybox:1.36.1#g' compose/docker-compose.yml manifest.json data-stamps.json && "
|
|
"sed -i -E 's#^( image: busybox:1.36.1)$#\\1\\n command: [\"sh\", \"-c\", \"sleep 20; exit 1\"]#' compose/docker-compose.yml && "
|
|
"grep -n 'image:\\|command:' compose/docker-compose.yml")
|
|
say(" [A] the unit now runs: " + out.replace("\n", " | "))
|
|
sw.restore_unit(APP, rec, say)
|
|
rec["a_restore"] = rec.pop("restore")
|
|
say(" [A] restore: " + rec["a_restore"].get("status", "")[:400])
|
|
rec["a_containers"] = started()
|
|
rec["a_undo"] = sw.g(f"ls -1 {UNDO} 2>&1").strip()
|
|
say(f" [A] containers: {rec['a_containers']} | kept undo copy: {rec['a_undo']}")
|
|
time.sleep(60)
|
|
rec["a_containers_60s_later"] = started()
|
|
say(f" [A] 60 s later: {rec['a_containers_60s_later']}")
|
|
# --- A: the original unit back, a normal restore, the copy released after the window
|
|
sw.g(f"rm -rf {UNIT} && cp -a {ORIG} {UNIT}")
|
|
rec["original_back"] = sums(UNIT) == s0
|
|
sw.g(f"rm -rf {UNDO}") # the copy the not-started run kept (mine), so the next count is clean
|
|
sw.restore_unit(APP, rec, say)
|
|
rec["a_normal_restore"] = rec.pop("restore")
|
|
say(" [A] normal restore: " + rec["a_normal_restore"].get("status", "")[:300])
|
|
rec["a_undo_right_after"] = sw.g(f"ls -1 {UNDO} 2>&1").strip()
|
|
say(f" [A] the copy right after the success: {rec['a_undo_right_after']}")
|
|
for _ in range(30):
|
|
time.sleep(10)
|
|
left = sw.g(f"ls -1 {UNDO} 2>&1").strip()
|
|
if "No such file" in left or not left:
|
|
break
|
|
rec["a_undo_after_window"] = sw.g(f"ls -1 {UNDO} 2>&1").strip()
|
|
rec["a_item_after"] = bool(tok) and bool(fx.verify(w, sub, tok, say))
|
|
say(f" [A] after the window: {rec['a_undo_after_window'] or '(gone)'}; item {rec['a_item_after']}")
|
|
|
|
# --- B: export, a copy with a missing tag, import
|
|
c, r = w.ctl("POST", "/api/export/start", {"stack_name": APP, "dest_drive": DRIVE, "password": "", "stop_app": False})
|
|
say(f" [B] export start -> {c} {str(r)[:160]}")
|
|
for _ in range(60):
|
|
time.sleep(5)
|
|
c, st = w.ctl("GET", "/api/export/status")
|
|
stt = json.dumps(st, ensure_ascii=False)
|
|
if '"running": false' in stt or '"state": "done"' in stt or '"done"' in stt:
|
|
break
|
|
c, b = w.ctl("GET", "/api/export/bundles")
|
|
paths = re.findall(r'"path": "([^"]+vikunja[^"]*\.fab)"', json.dumps(b))
|
|
rec["b_bundle"] = paths[:1]
|
|
if not paths:
|
|
say(" [B] no bundle found: " + json.dumps(b)[:300])
|
|
return
|
|
src = paths[0]
|
|
test_fab = os.path.join(os.path.dirname(src), "vikunja-r1279-test.fab")
|
|
out = sw.g(f"set -e; t=$(mktemp -d); cd $t; f={src}; if gzip -t $f 2>/dev/null; then tar xzf $f; z=z; else tar xf $f; z=; fi; "
|
|
f"sed -i -E 's#{REPO}:[^\"[:space:]]*#{REPO}:0.0.0-felhom-r1279-missing#g' config/docker-compose.yml; "
|
|
f"grep -n 'image:' config/docker-compose.yml; tar c${{z}}f {test_fab} .; cd /; rm -rf $t; ls -la {test_fab}")
|
|
say(" [B] the test bundle: " + out.replace("\n", " | "))
|
|
before_c, before_v = started(), vols()
|
|
c, r = w.ctl("POST", "/api/export/import", {"path": test_fab, "password": ""})
|
|
say(f" [B] import -> {c} {str(r)[:160]}")
|
|
last = None
|
|
for _ in range(60):
|
|
time.sleep(5)
|
|
c, st = w.ctl("GET", "/api/export/import/status")
|
|
last = json.dumps(st, ensure_ascii=False)
|
|
if "nem lehet letölteni" in last or '"error"' in last and '"running": false' in last or '"done"' in last:
|
|
break
|
|
rec["b_import_status"] = last[:900] if last else ""
|
|
after_c, after_v = started(), vols()
|
|
rec["b_containers_before"], rec["b_containers_after"] = before_c, after_c
|
|
rec["b_volumes_before"], rec["b_volumes_after"] = before_v, after_v
|
|
rec["b_untouched"] = bool(before_c) and before_c == after_c and before_v == after_v
|
|
rec["b_item_still_there"] = bool(tok) and bool(fx.verify(w, sub, tok, say))
|
|
say(f" [B] import status: {rec['b_import_status'][:400]}")
|
|
say(f" [B] containers and volumes untouched: {rec['b_untouched']}; item {rec['b_item_still_there']}")
|
|
finally:
|
|
rec["controller_lines"] = sw.scrub(sw.g(
|
|
f"docker logs --since {t_start} felhom-controller 2>&1 | grep -E '{APP}' | grep -iE 'R-1270|R-1279|D48|stabl|not start|released|REFUSED|Import' | cut -c1-320"))
|
|
sw.g(f"rm -f {test_fab} {os.path.dirname(test_fab)}/vikunja*.fab" if test_fab else "true")
|
|
sw.remove(APP, keep=False, say=say)
|
|
rec["after_remove"] = sw.leftovers(APP, sub)
|
|
rec["cleanup"] = sw.g(f"rm -rf {ORIG} {UNDO}; rmdir {NS}/backups/fix-a-test-original 2>/dev/null; ls -d {ORIG} {UNDO} 2>&1").strip()
|
|
json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|