railiance-apps/tools/vergabe_pilot_backup_node.py
tegwick bf77e378a8
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Record native recovery success and prepare attended pilot backups
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790
2026-11-06 10:55:12 +01:00

76 lines
3.7 KiB
Python

"""Attended capture/seal entry point; frozen capture and export implementations."""
import argparse
import fcntl
import json
import os
import pwd
import signal
import vergabe_pilot_export as export
import vergabe_pilot_point as point
import vergabe_pilot_snapshot as snapshot
from vergabe_pilot_recovery_store import Refused, private_directory, require
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run", required=True)
parser.add_argument("mode", choices=("check", "capture", "seal", "resume"))
args = parser.parse_args()
point.run_time(args.run)
owner = pwd.getpwnam("tegwick")
require(os.geteuid() == 0 and os.environ.get("SUDO_USER") == owner.pw_name
and os.environ.get("SUDO_UID") == str(owner.pw_uid), "founder_node_sudo_required")
if args.mode == "check":
_, fixture, _ = snapshot.preflight(args.run)
print(json.dumps({"status": "ready_for_attended_capture", "runtime_mutations": False,
"fixture_present": fixture["fixture_present"], "helm_revision": snapshot.REVISION}))
return 0
if not snapshot.BASE.exists():
snapshot.BASE.mkdir(mode=0o700)
private_directory(snapshot.BASE)
fd = os.open(snapshot.BASE / "operation.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if args.mode == "resume":
private_directory(snapshot.BASE / args.run)
require(point.read_private(snapshot.BASE / "active.json")["run_id"] == args.run,
"resume_must_match_active_run")
journal = point.read_private(snapshot.BASE / args.run / "journal.json")
require(journal["run_id"] == args.run, "journal_run_mismatch")
result = snapshot.resume(journal)
if result["helper_cleanup_confirmed"]:
snapshot.checkpoint(snapshot.BASE / "active.json", {"run_id": args.run, "status": "resumed_after_failure"})
print(json.dumps({"status": "application_resumed", "run_id": args.run,
"helper_cleanup_confirmed": result["helper_cleanup_confirmed"],
"backup_acceptance": False}))
return 0 if result["helper_cleanup_confirmed"] else 1
if args.mode == "capture":
require(args.run != point.BASELINE and not (snapshot.BASE / args.run).exists(), "fresh_run_required")
# Refuse a forgotten older interrupted pause before creating any new one.
active = snapshot.BASE / "active.json"
if active.exists() or active.is_symlink():
old = point.read_private(active)
require(old["status"] in {"captured_and_resumed", "resumed_after_failure"}, "previous_capture_requires_review_or_resume")
baseline, fixture, status = snapshot.preflight(args.run)
code = snapshot.execute(args.run, baseline, fixture, status)
if code:
return code
point.select_node(args.run)
export.main()
return 0
if __name__ == "__main__":
os.umask(0o077)
def interrupted(signum, frame):
raise Refused("attended_backup_interrupted")
signal.signal(signal.SIGHUP, interrupted)
signal.signal(signal.SIGTERM, interrupted)
try:
raise SystemExit(main())
except Exception as error: # noqa: BLE001 — never expose private subprocess/parser output
print(json.dumps({"status": "attended_backup_failed", "failure": str(error) if isinstance(error, Refused)
else "attended_backup_node_error", "details_suppressed": True}))
raise SystemExit(1)