99 lines
4.2 KiB
Python
Executable File
99 lines
4.2 KiB
Python
Executable File
#!/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:]))
|