#!/usr/bin/python3 """Make, list and restore backups of the router (vpnrouter_backup). run makes a backup as backup in /etc/vpn-router/vpn-router.json says, whether or not backups are enabled, and prints its result; vpn-router-backup.service runs it on the schedule of its timer. list prints the local archives of this host in backup.path, newest first, or with the pbs target the snapshots of this host on the Proxmox Backup Server. restore puts back a local archive, or with --pbs a snapshot, such as host/router/2026-10-25T03:00:00Z, after showing what it holds and asking for yes, which --yes gives; what it replaces is kept in /var/lib/vpn-router/backup/before-restore. A failure is a message on standard error; the exit status is 2 for an invalid request and 1 for any other failure. -h or --help shows the options. vpn-router-backup run vpn-router-backup list vpn-router-backup restore [--yes] ARCHIVE vpn-router-backup restore [--yes] --pbs SNAPSHOT """ import argparse import os import sys sys.path.insert(0, '/usr/lib/vpn-router') import vpnrouter import vpnrouter_backup class Parser(argparse.ArgumentParser): """An argument parser whose errors end with status 2.""" def error(self, message): print(f'{self.prog}: {message}; see {self.prog} --help', file=sys.stderr) sys.exit(2) def confirmed(manifest, yes): """Whether to restore the backup of the manifest: yes, or the answer yes to the question.""" print(f'Backup of {manifest.get("host")} made {manifest.get("time")} by vpn-router {manifest.get("version")}:') for item in manifest.get('items', []): print(f' {item}') if yes: return True return input('Replace the router state with it? Type yes: ').strip() == 'yes' def main(argv): parser = Parser(prog='vpn-router-backup', description='Make, list and restore backups of the router.') commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser) commands.add_parser('run', help='make a backup now') commands.add_parser('list', help='list the backups of this host') restorer = commands.add_parser('restore', help='put a backup back') restorer.add_argument('source', metavar='ARCHIVE|SNAPSHOT', help='a local archive, or with --pbs a snapshot') restorer.add_argument('--pbs', action='store_true', help='restore a snapshot of the Proxmox Backup Server') restorer.add_argument('--yes', action='store_true', help='do not ask before replacing the router state') options = parser.parse_args(argv) os.umask(0o077) try: if options.command == 'run': record = vpnrouter_backup.run() if record['result'] == 'failed': print(f'vpn-router-backup: {record["error"]}', file=sys.stderr) return 1 print(f'Backup {record["archive"] or record["snapshot"]} made, {record["size"]} bytes.') for name in record['removed']: print(f'Removed {name}.') if record['cloud']: print(f'Cloud copy to {record["cloud"]["remote"]}: {record["cloud"]["error"] or "ok"}.') return 0 cfg = vpnrouter.load_config() if options.command == 'list': if cfg['backup']['target'] == 'pbs': print(vpnrouter_backup.snapshots(cfg), end='') else: for item in vpnrouter_backup.archives(cfg['backup']['path']): print(f'{item["name"]} {item["size"]}') return 0 if options.pbs and options.source.startswith('/'): print('vpn-router-backup: a snapshot is type/id/time, such as host/router/2026-10-25T03:00:00Z', file=sys.stderr) return 2 saved = vpnrouter_backup.restore(options.source, options.pbs, lambda manifest: confirmed(manifest, options.yes)) except vpnrouter_backup.Failure as exc: print(f'vpn-router-backup: {exc}', file=sys.stderr) return 1 except OSError as exc: print(f'vpn-router-backup: {exc}', file=sys.stderr) return 1 if saved is None: print('Nothing restored.') return 1 print(f'Restored; what it replaced is in {saved}.') return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))