Files
werkjournal/scripts/operator-restore.py
2026-09-09 14:26:09 +02:00

68 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Verify a private snapshot, require explicit confirmation, then restore via one SSH call."""
import argparse
import base64
import datetime
import json
import os
from pathlib import Path
import runpy
import shlex
import shutil
import subprocess
import sys
import tempfile
ROOT = Path(__file__).parent
verify = runpy.run_path(str(ROOT / 'backup.py'))['verify']
def remote_command(target, recover=False):
program = "import base64; scope={'__name__':'restore_helpers'}; "
for name in ('deploy-update-remote.py', 'backup.py', 'restore-remote.py'):
encoded = base64.b64encode((ROOT / name).read_bytes()).decode('ascii')
if name == 'restore-remote.py':
program += "scope['__name__']='__main__'; "
program += "exec(compile(base64.b64decode('" + encoded + "'),'" + name + "','exec'),scope); "
arguments = ['python3', '-c', program]
if recover:
arguments.append('--recover')
return ['ssh', '-o', 'BatchMode=yes', target, shlex.join(arguments)]
def restore(target, source, confirm=input):
# Freeze the verified bytes before asking for confirmation; the original
# file can change while an operator is reading the prompt.
with tempfile.NamedTemporaryFile() as snapshot:
with Path(source).open('rb') as stream:
shutil.copyfileobj(stream, snapshot)
snapshot.flush()
snapshot.seek(0)
metadata = verify(snapshot.name)
commit = metadata['release']['commit']
stamp = datetime.datetime.fromtimestamp(metadata['createdAt'], datetime.timezone.utc).isoformat()
print('Target: ' + target)
print('Backup created: ' + stamp + '; application commit: ' + commit)
print('This replaces all current account and tracking data with this backup, including its access credentials. Newer data will leave the active database; the prior state is retained privately on the server.')
required = 'RESTORE ' + commit
if confirm('Type ' + required + ' to confirm: ') != required:
raise RuntimeError('Restore cancelled; no SSH connection opened')
snapshot.seek(0)
return subprocess.call(remote_command(target), stdin=snapshot)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('target')
choice = parser.add_mutually_exclusive_group(required=True)
choice.add_argument('--backup', type=Path)
choice.add_argument('--recover', action='store_true')
args = parser.parse_args()
try:
if args.recover:
raise SystemExit(subprocess.call(remote_command(args.target, recover=True)))
raise SystemExit(restore(args.target, args.backup))
except (OSError, RuntimeError, ValueError, EOFError) as error:
print('Restore failed: ' + str(error), file=sys.stderr)
raise SystemExit(1)