railiance-apps/tools/vergabe_pilot_backup.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

268 lines
14 KiB
Python

#!/usr/bin/env python3
"""Founder workstation attended backup for the admitted demo-company release.
Default reviews only. --execute captures a fresh point (brief app pause), seals
on-node, fetches ciphertext, then opens contained owner login to publish/readback/
decrypt. --fetch RUN or --publish RUN retries without another capture/pause.
--retention previews owner-verified provider retention; --prune executes it.
"""
import argparse
import datetime as dt
import hashlib
import json
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
import vergabe_pilot_export as export
import vergabe_pilot_export_remote as transfer
import vergabe_pilot_point as point
import vergabe_pilot_publish as publisher
from vergabe_pilot_recovery_store import Refused, private_directory, require
PLAN = Path(__file__).resolve().parents[1] / "docs/proposals/2026-11-06-vergabe-attended-backups/plan.json"
NODE_MODULES = ("vergabe_pilot_recovery_store", "vergabe_pilot_export", "vergabe_pilot_point",
"vergabe_pilot_snapshot", "vergabe_pilot_backup_node")
def source_pins():
plan = json.loads(PLAN.read_text())
expected = {*NODE_MODULES, "vergabe_pilot_export_remote", "vergabe_pilot_publish",
"vergabe_pilot_backup", "vergabe_pilot_retention", "vergabe_pilot_operating_restore",
"vergabe_pilot_restore", "vergabe_pilot_restore_remote"}
require(set(plan["sources"]) == expected, "attended_source_set_changed")
sources = {}
for name, checksum in plan["sources"].items():
raw = Path(__file__).with_name(name + ".py").read_bytes()
require(hashlib.sha256(raw).hexdigest() == checksum, "attended_source_pin_changed")
sources[name] = raw.decode()
publisher.pins()
return sources
def remote_command(run, mode):
point.run_time(run)
require(mode in {"check", "capture", "seal", "resume"}, "invalid_backup_mode")
sources = source_pins()
bootstrap = "import sys,types;"
for name in NODE_MODULES[:-1]:
bootstrap += ("m=types.ModuleType(" + repr(name) + ");sys.modules[m.__name__]=m;"
"exec(compile(" + repr(sources[name]) + "," + repr(name + ".py") + ",'exec'),m.__dict__);")
bootstrap += ("sys.argv=['vergabe_pilot_backup_node.py',*sys.argv[1:]];exec(compile(" +
repr(sources[NODE_MODULES[-1]]) + ",'vergabe_pilot_backup_node.py','exec'),{'__name__':'__main__'})")
command = shlex.join(["sudo", "/usr/bin/env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml",
"/usr/bin/python3", "-c", bootstrap, "--run", run, mode])
require(len(command.encode()) < 120000, "attended_transport_size_bound")
return ["ssh", "-t", "railiance01", command]
def kit_root():
return Path.home() / ".local/state/vergabe-pilot-recovery"
def configure(directory, run):
require(transfer.outside_git(directory.resolve()), "private_kit_outside_git_required")
sealed = point.select_transfer(directory, run)
verified = transfer.verify(directory)
publisher.OBJECT = export.NAME + ".zip.age"
publisher.CIPHER_SHA256 = sealed["ciphertext_sha256"]
publisher.CIPHER_BYTES = sealed["ciphertext_bytes"]
require(verified["ciphertext_sha256"] == sealed["ciphertext_sha256"], "sealed_point_changed")
return sealed
def fetch(run, already_sealed=False):
point.run_time(run)
root = kit_root()
require(transfer.outside_git(root.resolve()), "private_kit_outside_git_required")
root.mkdir(parents=True, exist_ok=True, mode=0o700)
private_directory(root)
directory = root / run
if directory.exists() or directory.is_symlink():
configure(directory, run)
return directory
if not already_sealed:
result = subprocess.run(remote_command(run, "seal"), check=False)
require(result.returncode == 0, "node_seal_failed_use_same_run_for_retry")
with tempfile.TemporaryDirectory(prefix="fetch-", dir=root) as temporary:
temporary = Path(temporary)
for suffix in (".transfer.json", ".zip.age"):
name = "vergabe-pilot-" + run + suffix
copied = subprocess.run(["scp", "-p", "railiance01:" + name, str(temporary / name)],
capture_output=True, timeout=120, check=False)
require(copied.returncode == 0, "encrypted_fetch_failed_use_same_run_for_retry")
configure(temporary, run)
temporary.rename(directory)
return directory
def inventory(root):
"""Only private local kits with ciphertext and complete owner receipts qualify."""
private_directory(root)
children = list(root.iterdir())
require(len(children) <= 2000, "local_inventory_bound")
points = []
for directory in sorted(children):
if not directory.is_dir() or directory.is_symlink():
continue
try:
point.run_time(directory.name)
sealed = configure(directory, directory.name)
tombstone = directory / "provider-removed.json"
if tombstone.exists() or tombstone.is_symlink():
removed = point.read_private(tombstone)
require(removed["run_id"] == directory.name and removed["ciphertext_sha256"] == sealed["ciphertext_sha256"]
and removed["status"] == "provider_removed_and_readback_absent", "retention_tombstone_invalid")
continue
receipts = list(directory.glob("provider-receipt-*.json"))
require(len(receipts) <= 100, "provider_receipt_count_bound")
if any(point.successful_provider(point.read_private(p), directory.name, sealed) for p in receipts):
points.append({"run_id": directory.name, "ciphertext_sha256": sealed["ciphertext_sha256"],
"ciphertext_bytes": sealed["ciphertext_bytes"]})
except (OSError, ValueError, KeyError, TypeError, Refused):
continue # Incomplete/unknown kits never qualify for provider deletion.
return points
def write_receipt(path, body):
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(body, stream, indent=2)
stream.flush()
os.fsync(stream.fileno())
def owner_child(args):
# Child stays silent under Warden, including setup and parser failures.
try:
source_pins()
require(args.directory and args.receipt and args.directory.is_absolute()
and args.receipt.parent == args.directory, "private_owner_paths_required")
configure(args.directory, args.run)
if args.action == "publish":
return publisher.child(args.directory, args.receipt)
require(args.action == "prune", "invalid_owner_action")
import vergabe_pilot_retention as retention
return retention.child(args.directory.parent, args.receipt, inventory)
except BaseException: # noqa: BLE001 — contained child must never print private output
return 1
def owner_action(directory, run, action="publish"):
configure(directory, run)
require(all(shutil.which(t) for t in ("warden", "bao", "age", "age-keygen")), "owner_tools_required")
output = directory / (("provider-receipt-" if action == "publish" else "retention-receipt-") + uuid.uuid4().hex[:12] + ".json")
command = [sys.executable, str(publisher.PLATFORM / "scripts/openbao-attended-exec.py"), "--",
sys.executable, str(Path(__file__).resolve()), "--contained", "--run", run,
"--directory", str(directory), "--receipt", str(output), "--action", action]
result = subprocess.run(command, check=False, env=publisher.owner_login_environment())
if output.exists() or output.is_symlink():
body = point.read_private(output)
else:
body = {"status": "owner_login_or_child_not_completed", "provider_outcome": "unconfirmed"}
body.update(warden_exit_code=result.returncode, receipt_file=str(output), **publisher.session_outcome(result.returncode))
if output.exists():
export.regular(output, os.geteuid())
fd = os.open(output, os.O_WRONLY | os.O_TRUNC | os.O_NOFOLLOW)
with os.fdopen(fd, "w") as stream:
json.dump(body, stream, indent=2)
stream.flush()
os.fsync(stream.fileno())
else:
write_receipt(output, body)
print(json.dumps(body, indent=2))
expected = "published_read_back_and_decrypted" if action == "publish" else "provider_retention_complete"
require(result.returncode == 0 and body["status"] == expected, "owner_operation_incomplete")
return body
def main():
parser = argparse.ArgumentParser(description=__doc__)
modes = parser.add_mutually_exclusive_group()
modes.add_argument("--check", action="store_true")
modes.add_argument("--execute", action="store_true")
modes.add_argument("--fetch")
modes.add_argument("--publish")
modes.add_argument("--resume")
modes.add_argument("--retention", action="store_true")
modes.add_argument("--prune", action="store_true")
modes.add_argument("--restore")
modes.add_argument("--restore-cleanup", nargs=2, metavar=("RUN", "NONCE"))
modes.add_argument("--contained", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--run", help=argparse.SUPPRESS)
parser.add_argument("--directory", type=Path, help=argparse.SUPPRESS)
parser.add_argument("--receipt", type=Path, help=argparse.SUPPRESS)
parser.add_argument("--action", choices=("publish", "prune"), default="publish", help=argparse.SUPPRESS)
parser.add_argument("--verify-restore", action="store_true", help="also rehearse this point in fresh isolated claims")
args = parser.parse_args()
if args.contained:
return owner_child(args)
require(not args.verify_restore or args.execute or args.publish, "verify_restore_requires_capture_or_publish")
sources = source_pins()
if not any((args.check, args.execute, args.fetch, args.publish, args.resume, args.retention, args.prune,
args.restore, args.restore_cleanup)):
print(json.dumps({"status": "prepared", "target": "railiance01", "helm_revision": 4,
"next": "--check then --execute", "runtime_mutations": False,
"capture_briefly_pauses_pilot": True, "scheduled_service": False}))
return 0
require(sys.stdin.isatty(), "founder_interactive_terminal_required")
if args.restore or args.restore_cleanup:
import vergabe_pilot_operating_restore as recovery
if args.restore_cleanup:
return recovery.cleanup(sources, *args.restore_cleanup)
directory = kit_root() / args.restore
sealed = configure(directory, args.restore)
return recovery.execute(directory, args.restore, sealed, sources)
if args.retention or args.prune:
plan = point.retention_plan(inventory(kit_root()))
print(json.dumps({"status": "retention_preview", **plan}, indent=2), flush=True)
if args.prune and plan["candidates"]:
require(any(p["run_id"] == point.BASELINE for p in plan["keep"]), "baseline_receipt_required")
newest = plan["keep"][0]["run_id"]
owner_action(kit_root() / newest, newest, "prune")
return 0
run = args.fetch or args.publish or args.resume or dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ-") + uuid.uuid4().hex[:12]
point.run_time(run)
if args.check or args.resume:
return subprocess.run(remote_command(run, "resume" if args.resume else "check"), check=False).returncode
print(json.dumps({"status": "attended_backup_started", "run_id": run,
"capture_requested": args.execute}), flush=True)
if args.execute:
result = subprocess.run(remote_command(run, "capture"), check=False)
require(result.returncode == 0, "capture_or_seal_failed_keep_run_id_and_review_resume")
directory = kit_root() / run if args.publish else fetch(run, already_sealed=args.execute)
configure(directory, run)
if args.fetch:
print(json.dumps({"status": "encrypted_workstation_copy_verified", "run_id": run,
"provider_publication_verified": False, "local_directory": str(directory)}))
return 0
body = owner_action(directory, run)
if args.verify_restore:
import vergabe_pilot_operating_restore as recovery
sealed = configure(directory, run)
require(recovery.execute(directory, run, sealed, sources) == 0, "backup_published_but_restore_requires_review")
summary = {"schema": "railiance.vergabe-pilot-attended-backup.v1", "status": "complete", "run_id": run,
"provider_publication_verified": True, "independent_readback_verified": True,
"decryption_verified": True, "session_cleanup_confirmed": True, "restore_verified": args.verify_restore,
"provider_receipt": body["receipt_file"], "capture_requested": args.execute,
"retention_execution_pending": bool(point.retention_plan(inventory(kit_root()))["candidates"])}
write_receipt(directory / ("attended-backup-" + uuid.uuid4().hex[:12] + ".json"), summary)
print(json.dumps(summary, indent=2))
return 0
if __name__ == "__main__":
os.umask(0o077)
try:
raise SystemExit(main())
except Exception as error: # noqa: BLE001 — keep private transport/parser output suppressed
if "--contained" not in sys.argv:
print(json.dumps({"status": "attended_backup_failed", "failure": str(error) if isinstance(error, Refused)
else "attended_backup_error", "details_suppressed": True}))
raise SystemExit(1)