#!/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()