Files
felhom.eu/documentation/audits/burndown-3-2026-10-17/tools/redis6a.py
T
admin e1dd1cadd3
gates / gates (push) Successful in 7m2s
burndown 3: report, evidence, register (174 -> 168), 07 Redis RDB [DESIGN], 04 §6.3 the box's password doors
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
2026-10-17 10:28:03 +02:00

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()