#!/usr/bin/env python3 """pullprobe.py — D48 and R-1270 live, on a throwaway app this run installs (never a household's unit). 1. install , seed one item through the front door, „Mentés most"; 2. keep a byte copy of the app's unit beside it (backups/d48-test-original/, outside backups/primary); 3. D48: the unit's pin edited to a tag that does not exist (compose, manifest, data stamps — the same ref everywhere, so the version check passes and the pull is what decides) → „Visszaállítás": refused, the app's containers never restarted (StartedAt the same before and after), the household's sentence recorded; 4. the original unit back (sha256 of every file equal), a normal restore → success, the item reads back; 5. R-1270: the unit's pin edited to an image that pulls but exits at once (busybox) → „Visszaállítás": the record is NOT ok, `not_started`, the household's sentence; the undo copy kept; then the original back and a normal restore; 6. remove with data; the test copy removed. Endpoint-level for every household act; the unit edits are the only hand-made change (`pct exec`, files only). usage: SC=… SWEEP_EV=… TARGET=9202 DRIVE_ROOT=… python3 pullprobe.py [] """ import json, os, 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 NS = "/mnt/sys_drive/felhom-data" MISSING_TAG = "0.0.0-felhom-d48-missing" def started(app): 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}} {{.State.Status}}' $c; done").strip() def sums(path): return sw.g(f"cd {path} 2>/dev/null && find . -type f | sort | xargs -r sha256sum") def restore(app, rec, key, say): sw.restore_unit(app, rec, say) rec[key] = rec.pop("restore") say(f" {key}: " + rec[key].get("status", "")[:400]) def main(app, repo, exiting): d = os.path.join(sw.EV, "_pullprobe", 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} unit = f"{NS}/backups/primary/{app}" orig = f"{NS}/backups/d48-test-original/{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) sw.g(f"rm -rf {orig}; mkdir -p {NS}/backups/d48-test-original && cp -a {unit} {orig}") s0 = sums(unit) rec["unit_files"] = len(s0.splitlines()) def edit(to_ref): # the same ref in the definition, the manifest (pins + data block) and the data stamps — files only return sw.g(f"cd {unit} && sed -i -E 's#{repo}:[^\"[:space:]]*#{to_ref}#g' compose/docker-compose.yml manifest.json data-stamps.json " f"&& grep -c '{to_ref}' compose/docker-compose.yml manifest.json data-stamps.json") def put_original(): sw.g(f"rm -rf {unit} && cp -a {orig} {unit}") same = sums(unit) == s0 say(f" the original unit is back byte for byte: {same}") return same # 3. D48 say(" [D48] the unit's pin → " + repo + ":" + MISSING_TAG + " :: " + edit(f"{repo}:{MISSING_TAG}").replace("\n", " ")) before = started(app) restore(app, rec, "d48_restore", say) after = started(app) rec["d48_started_before"], rec["d48_started_after"] = before, after rec["d48_app_untouched"] = bool(before) and before == after say(f" [D48] containers never restarted: {rec['d48_app_untouched']} ({before})") rec["d48_item_still_there"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) # 4. normal restore rec["original_back_1"] = put_original() restore(app, rec, "normal_restore_1", say) rec["item_after_normal_restore_1"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) say(f" normal restore: item back {rec['item_after_normal_restore_1']}, state {w.stack(app).get('state')}") # 5. R-1270 if exiting: sw.backup_now(rec, say) # the unit again after the restore (its data is the app's now) sw.g(f"rm -rf {orig}; cp -a {unit} {orig}") s0 = sums(unit) say(" [R-1270] the unit's pin → " + exiting + " (pulls, exits at once) :: " + edit(exiting).replace("\n", " ")) restore(app, rec, "r1270_restore", say) rec["r1270_state"] = w.stack(app).get("state") rec["r1270_undo_copies"] = sw.g(f"ls -1 {NS}/backups/restore-undo/{app} 2>&1").strip() say(f" [R-1270] state {rec['r1270_state']}, kept undo copy: {rec['r1270_undo_copies']}") rec["original_back_2"] = put_original() restore(app, rec, "normal_restore_2", say) rec["item_after_normal_restore_2"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) say(f" normal restore 2: item back {rec['item_after_normal_restore_2']}, state {w.stack(app).get('state')}") finally: rec["controller_lines"] = sw.scrub(sw.g( f"docker logs --since {t_start} felhom-controller 2>&1 | grep -E '{app}' | grep -iE 'D48|R-1270|refused|downloaded|restor|not start|late' | cut -c1-320")) sw.remove(app, keep=False, say=say) rec["after_remove"] = sw.leftovers(app, sub) rec["test_copy_removed"] = sw.g(f"rm -rf {orig}; rmdir {NS}/backups/d48-test-original 2>/dev/null; ls -d {orig} 2>&1").strip() rec["restore_undo_left"] = sw.g(f"ls -1 {NS}/backups/restore-undo/{app} 2>&1").strip() json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1) if __name__ == "__main__": main(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "")