Files
felhom.eu/documentation/audits/night-restore-2026-10-17/tools/drill2.py
T

119 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""drill2.py — R-1106 proof on demo-hp 9201 (controller 0.324.0-rc.N), THROUGH THE PRODUCT.
Runs on DooPlex. Env: SC (0600 dir with .ctlpw), EV (evidence dir), TARGET=9201. One step per call:
install APP deploy fresh (new generated passwords)
seed APP TAG a marker row in the app's database (+ a file in Grimmory's books folder); TAG names it
check APP TAG the app's state, the marker, auth errors in the app log, the DB logins with the app's env
offsite-run „Távoli mentés most" (dumps + units + push)
remove APP MODE MODE all = drive data + backups; keepbackups = drive data gone, backups kept;
keepdata = drive data kept, backups kept
restore-offsite APP full off-site restore (prepare, confirm, „Teljes visszaállítás")
restore-onbox APP the backups page's „Visszaállítás" from the app's own unit (POST /backup/restore)
install-kept APP install choosing „A megőrzött adataimat használom" (kept_data=use)
Database probes run INSIDE the app's own containers with the container's own env (`docker exec … sh -c '…$VAR…'`):
no password leaves the box or reaches this log.
"""
import json
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import drill # noqa: E402
w = drill.w
say = drill.say
SUB = {"rallly": "rallly-r1106", "grimmory": "grimmory-r1106"}
VALUES = {"grimmory": {"HDD_PATH": "/mnt/felhom-drives/hdd_1"}}
BOOKS = "/mnt/felhom-drives/hdd_1/userdata/media/grimmory"
SQL = {
"rallly": ("rallly-postgres", "psql -U rallly -d rallly -v ON_ERROR_STOP=1 -Atc"),
"grimmory": ("grimmory-db", 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" grimmory -N -e'),
}
def sql(app, q):
"""The SQL goes in on STDIN (a heredoc), so no quote inside it can break the shell around it. (The first version
passed it with `-c "…"` inside `sh -c '…'`, and the marker's own single quotes broke it — run A1, recorded.)"""
c, cmd = SQL[app]
cmd = cmd.replace(" -Atc", " -At").replace(" -N -e", " -N")
return w.guest(f"docker exec -i {c} sh -c '{cmd}' 2>&1 <<'R1106SQL' | tail -3\n{q};\nR1106SQL\n").strip()
def seed(app, tag):
say(f"seed {app} {tag}: " + sql(app, f"create table if not exists r1106_marker(note varchar(80)); insert into r1106_marker values ('{tag}'); select note from r1106_marker"))
if app == "grimmory":
say(" file: " + w.guest(f"printf '%s\\n' '{tag}' > {BOOKS}/{tag}.txt && chown 1000:1000 {BOOKS}/{tag}.txt && sha256sum {BOOKS}/{tag}.txt").strip())
def check(app, tag):
st = w.stack(app)
say(f"check {app}: deployed={st.get('deployed')} state={st.get('state')} hold={st.get('hold_reason')!r}")
say(f" marker row: {sql(app, 'select note from r1106_marker')!r} (want {tag!r} present)")
if app == "rallly":
logs = w.guest("docker logs --since 10m rallly 2>&1 | grep -c -E 'P1000|Authentication failed' ; docker ps --filter name=^rallly$ --format '{{.Status}}'")
say(f" rallly log auth errors (10 min) / status: {' | '.join(logs.split())}")
say(" app login to its DB (the app's own DATABASE_URL user): " + w.guest(
"docker exec rallly-postgres sh -c 'PGPASSWORD=\"$POSTGRES_PASSWORD\" psql -h 127.0.0.1 -U rallly -d rallly -Atc \"select 1\"' 2>&1 | tail -1").strip())
say(" front door: " + w.guest("curl -s -o /dev/null -w '%{http_code}' --max-time 15 -H 'Host: rallly-r1106.enkisfelhom.hu' http://127.0.0.1/ 2>/dev/null; echo"))
else:
say(" root login (the product's dump user): " + w.guest(
"docker exec grimmory-db sh -c 'mariadb -uroot -p\"$MARIADB_ROOT_PASSWORD\" -N -e \"select 1\"' 2>&1 | tail -1").strip())
say(" app user login (the app's own DB user): " + w.guest(
"docker exec grimmory-db sh -c 'mariadb -ugrimmory -p\"$MARIADB_PASSWORD\" grimmory -N -e \"select 1\"' 2>&1 | tail -1").strip())
say(" app health: " + w.guest("docker ps --filter name=^grimmory$ --format '{{.Status}}'; docker exec grimmory wget -q -O- http://127.0.0.1:6060/api/v1/healthcheck 2>&1 | head -c 200; echo").strip())
say(" file: " + w.guest(f"sha256sum {BOOKS}/{tag}.txt 2>&1").strip())
def remove(app, mode):
hdd = mode == "all" or mode == "keepbackups"
bk = mode == "all"
c1, _ = w.ctl("POST", f"/api/stacks/{app}/stop")
for _ in range(24):
time.sleep(5)
if w.stack(app).get("state") != "running":
break
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": hdd, "remove_backups": bk})
say(f"remove {app} ({mode}: remove_hdd_data={hdd}, remove_backups={bk}) -> {code} {str(d)[:200]}")
if app == "grimmory" and hdd:
# the household's books folder is never removed by the product (decision 67); the household deletes its file
say(" household deletes the books-folder files: " + w.guest(f"rm -fv {BOOKS}/r1106-*.txt | wc -l").strip())
def main():
step, args = sys.argv[1], sys.argv[2:]
w.login()
if step == "install":
say(f"install {args[0]}: {w.deploy(args[0], SUB[args[0]], VALUES.get(args[0]))}")
elif step == "install-kept":
os.environ["KEPT"] = "use"
say(f"install {args[0]} with kept_data=use: {w.deploy(args[0], SUB[args[0]], VALUES.get(args[0]))}")
elif step == "seed":
seed(args[0], args[1])
elif step == "check":
check(args[0], args[1])
elif step == "offsite-run":
drill.offsite_run()
elif step == "remove":
remove(args[0], args[1])
elif step == "restore-offsite":
app = args[0]
head, loc = drill.form("/backup/offbox/restore", {"app": app, "mode": "full"})
drill.form("/backup/offbox/restore", {"app": app, "mode": "full", "confirm": "1"})
say(json.dumps(drill.wait_restore(), ensure_ascii=False)[:700])
drill.form("/backup/offbox/reconstitute", {"app": app, "confirm": "1"})
say(json.dumps(drill.wait_restore(), ensure_ascii=False)[:900])
elif step == "restore-onbox":
r = w.restore(args[0])
say("onbox restore: " + json.dumps({k: r.get(k) for k in ("ok", "snapshot_id", "http", "location", "seconds")}, ensure_ascii=False))
c, d = w.ctl("GET", "/api/backup/restore-status")
say(" restore-status: " + json.dumps((d or {}).get("data"), ensure_ascii=False)[:900])
else:
sys.exit("unknown step " + step)
if __name__ == "__main__":
main()