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