gates / gates (push) Successful in 7m2s
Closed R-1115, R-1111, R-1112, R-1113, R-1109, R-1110, R-1114, R-1096; opened R-1116 (decoy suites in the other runners), R-1117 (Immich/Nextcloud Redis volume, deferred). Controller 0.325.0 is the floor since 08:21 UTC. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
153 lines
7.2 KiB
Python
153 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""redis6a.py — R-1096 Part 6a on scratch guest 9202 (demo-hp), THROUGH THE PRODUCT.
|
|
|
|
Runs on DooPlex. Env: SC (0600 dir with .ctlpw), EV (evidence dir), TARGET=9202. One step per call:
|
|
install deploy a throwaway docmost (generated passwords, never printed)
|
|
seed 100 keys felhom:probe:<n> into docmost-redis (redis-cli inside the container); the expected
|
|
values go to SC/expected.txt, their sha256 to the log
|
|
check TAG read the 100 keys back and compare; INFO persistence; the redis container's Created + Cmd
|
|
restart the dashboard's restart (POST /api/stacks/docmost/restart: up -d --force-recreate)
|
|
sync POST /api/sync + rescan (the dashboard's „Sablonok frissítése"), then wait until docmost-redis
|
|
runs the catalog's new command (decision 4: the box applies a template correction itself)
|
|
backup „Mentés most" (POST /api/backup/run: db dumps + volume dumps with each stack STOPPED), polled
|
|
flush FLUSHALL in docmost-redis — the state a restore must undo
|
|
restore the backups page's „Visszaállítás" from docmost's own newest copy (POST /backup/restore)
|
|
remove remove docmost through the product (stop, remove with drive data + backups)
|
|
Nothing writes app data except redis-cli SET/FLUSHALL on the probe keys and the product's own buttons.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import secrets
|
|
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, "6a-console.txt"), "a", buffering=1)
|
|
_say = w.say
|
|
APP, SUB, RC = "docmost", "docmost-r1096", "docmost-redis"
|
|
N = 100
|
|
|
|
|
|
def say(*a):
|
|
_say(*a)
|
|
LOG.write(time.strftime("%H:%M:%S ") + " ".join(str(x) for x in a) + "\n")
|
|
|
|
|
|
w.say = say
|
|
|
|
|
|
def rcli(cmd):
|
|
return w.guest(f"docker exec -i {RC} redis-cli {cmd} 2>&1").strip()
|
|
|
|
|
|
def redis_facts():
|
|
info = rcli("INFO persistence")
|
|
keep = [l for l in info.splitlines() if l.split(":")[0] in (
|
|
"aof_enabled", "rdb_last_bgsave_status", "rdb_changes_since_last_save", "rdb_last_save_time", "loading",
|
|
"aof_last_write_status", "rdb_saves")]
|
|
c = w.guest(f"docker inspect -f '{{{{.Created}}}} {{{{json .Config.Cmd}}}}' {RC} 2>&1; docker ps --filter name=^{RC}$ --format '{{{{.Status}}}}'")
|
|
files = w.guest(f"docker exec {RC} sh -c 'ls -la /data /data/appendonlydir 2>&1'")
|
|
return keep, " | ".join(c.split("\n")).strip(), files.strip()
|
|
|
|
|
|
def check(tag):
|
|
exp = [l.rstrip("\n").split(" ", 1) for l in open(os.path.join(w.SC, "expected.txt"))]
|
|
script = "".join(f"GET {k}\n" for k, _ in exp)
|
|
out = w.guest(f"docker exec -i {RC} redis-cli <<'R1096'\n{script}R1096\n").splitlines()
|
|
got = [o.strip().strip('"') for o in out][:N]
|
|
match = sum(1 for (k, v), g in zip(exp, got) if v == g)
|
|
keep, cont, files = redis_facts()
|
|
say(f"check {tag}: {match}/{N} probe keys returned with their value; dbsize={rcli('DBSIZE')}")
|
|
say(f" INFO persistence: {', '.join(keep)}")
|
|
say(f" container: {cont}")
|
|
say(f" /data: {files}")
|
|
return match
|
|
|
|
|
|
def main():
|
|
step, args = sys.argv[1], sys.argv[2:]
|
|
w.login()
|
|
if step == "install":
|
|
ok = w.deploy(APP, SUB)
|
|
say(f"install {APP}: {ok}")
|
|
st = w.stack(APP)
|
|
say(f" state={st.get('state')} pinned={(st.get('app_config') or {}).get('pinned_images')}")
|
|
elif step == "seed":
|
|
exp = [(f"felhom:probe:{n}", f"v{n}-{secrets.token_hex(8)}") for n in range(N)]
|
|
with open(os.path.join(w.SC, "expected.txt"), "w") as fh:
|
|
for k, v in exp:
|
|
fh.write(f"{k} {v}\n")
|
|
script = "".join(f"SET {k} {v}\n" for k, v in exp)
|
|
out = w.guest(f"docker exec -i {RC} redis-cli <<'R1096'\n{script}R1096\n")
|
|
sha = hashlib.sha256("".join(f"{k}={v}\n" for k, v in exp).encode()).hexdigest()
|
|
say(f"seed: {out.count('OK')}/{N} SET OK; expected set sha256={sha}; dbsize now {rcli('DBSIZE')}")
|
|
say(f" app's own keys (non-probe): {rcli('--scan --count 1000').count(chr(10)) + 1 - N} (approx, scan)")
|
|
check("after-seed")
|
|
elif step == "check":
|
|
check(args[0])
|
|
elif step == "sync":
|
|
before = redis_facts()[1]
|
|
say(f"sync: redis before: {before}")
|
|
t0 = time.time()
|
|
w.sync_rescan()
|
|
for i in range(120):
|
|
cont = redis_facts()[1]
|
|
if "appendonly\",\"no" in cont.replace(" ", "") and "Up" in cont:
|
|
say(f" +{time.time()-t0:.0f}s docmost-redis runs the new command: {cont}")
|
|
break
|
|
if i % 6 == 0:
|
|
say(f" +{time.time()-t0:.0f}s waiting: {cont}")
|
|
w.sync_rescan()
|
|
time.sleep(10)
|
|
else:
|
|
say(" the new command never reached docmost-redis within 20 min")
|
|
st = w.stack(APP)
|
|
say(f" stack: state={st.get('state')} hold={st.get('hold_reason')!r}")
|
|
elif step == "restart":
|
|
say("restart: compose files on the box: " + " | ".join(w.guest(
|
|
f"grep -H 'redis-server' /opt/docker/stacks/{APP}/docker-compose.yml /opt/docker/stacks/{APP}/applied-compose.yml").split(chr(10))))
|
|
say(f" redis before: {redis_facts()[1]}")
|
|
code, d = w.ctl("POST", f"/api/stacks/{APP}/restart")
|
|
say(f" POST /api/stacks/{APP}/restart -> {code} {str(d)[:160]}")
|
|
for _ in range(60):
|
|
time.sleep(5)
|
|
st = w.stack(APP)
|
|
if st.get("state") == "running" and "healthy" in redis_facts()[1]:
|
|
break
|
|
say(f" stack: state={w.stack(APP).get('state')}; redis after: {redis_facts()[1]}")
|
|
say(" redis log: " + " | ".join(w.guest(f"docker logs {RC} 2>&1 | grep -iE 'DB loaded|Loading|RDB|AOF|Ready' | tail -6").split(chr(10))))
|
|
check("after-restart")
|
|
elif step == "backup":
|
|
code, d = w.ctl("POST", "/api/backup/run")
|
|
say(f"backup: POST /api/backup/run -> {code} {str(d)[:160]}")
|
|
t0 = time.time()
|
|
while time.time() - t0 < 1800:
|
|
done = w.guest("docker logs --since 30m felhom-controller 2>&1 | grep -E 'App-data backup completed|backup.*(FAILED|failed)' | tail -2")
|
|
if "completed" in done:
|
|
say(f" +{time.time()-t0:.0f}s {done.strip()}")
|
|
break
|
|
time.sleep(10)
|
|
else:
|
|
say(" no completion line in 30 min")
|
|
say(" volume dumps: " + w.guest("docker logs --since 30m felhom-controller 2>&1 | grep -iE 'docmost' | grep -iE 'volume|dump|stopp|start' | tail -8"))
|
|
say(" units: " + w.guest("find / -xdev -path '*backups/primary/docmost/volume-dumps/*redis*' -newermt '-40 min' -printf '%TT %s %p\\n' 2>/dev/null | head"))
|
|
elif step == "flush":
|
|
say(f"flush: FLUSHALL -> {rcli('FLUSHALL')}; dbsize {rcli('DBSIZE')}")
|
|
elif step == "restore":
|
|
r = w.restore(APP)
|
|
say("restore: " + json.dumps({k: r.get(k) for k in ("ok", "snapshot_id", "http", "location", "seconds")}, ensure_ascii=False))
|
|
time.sleep(20)
|
|
check("after-restore")
|
|
elif step == "remove":
|
|
say(f"remove: {w.remove(APP)}")
|
|
else:
|
|
sys.exit("unknown step " + step)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|