"""/backup: the backup settings, the last run and the local archives, and a run now.""" import configparser import shutil import vpnrouter import vpnrouter_backup from vpnrouter_status import run def backup_settings(settings): """The backup section as /backup shows it: its settings but pbs_password, with pbs_password_set.""" return {**{option: value for option, value in settings.items() if option != 'pbs_password'}, 'pbs_password_set': bool(settings['pbs_password'])} def rclone_remotes(): """The names of the remotes of vpnrouter_backup.RCLONE_CONFIG; [] when it cannot be read, as when it is encrypted.""" parser = configparser.ConfigParser(interpolation=None) try: parser.read(vpnrouter_backup.RCLONE_CONFIG, encoding='utf-8') except (OSError, configparser.Error): return [] return parser.sections() def backup(method, path, body): """Answer a /backup request: the HTTP status and the JSON document. GET /backup {"settings", "state", "archives", "next", "client", "rclone"}: the backup section without pbs_password, with pbs_password_set; the last run (vpnrouter_backup.state), null before the first; the local archives of this host, newest first, [{name, time, size}], [] with the pbs target; the next run of the timer as systemd shows it, null while backups are disabled; whether proxmox-backup-client is installed; and {"installed", "config", "remotes"} of rclone PUT /backup change any of the settings of the backup section; an empty or absent pbs_password keeps the current secret POST /backup/run start a backup now, without waiting for it; 409 while one runs A change is made as /site-to-site makes one, and applied by restarting vpn-router-setup without waiting for it. """ if body == 'invalid' or (body is not None and not isinstance(body, dict)): return 400, {'error': 'the body must be a JSON object'} body = body or {} if (method, path) == ('POST', '/backup/run'): if vpnrouter_backup.running(): return 409, {'error': 'a backup is running'} result = run('systemctl', 'start', '--no-block', vpnrouter_backup.UNIT) if result is None or result.returncode != 0: return 500, {'error': f'the backup cannot be started: {result.stderr.strip() if result else "no systemctl"}'} return 202, {'started': True} if (method, path) not in (('GET', '/backup'), ('PUT', '/backup')): return 404, {'error': f'no route {method} {path}'} try: if method == 'GET': settings = vpnrouter.load_config()['backup'] timer = run('systemctl', 'show', '-p', 'NextElapseUSecRealtime', '--value', vpnrouter_backup.TIMER) next_run = timer.stdout.strip() if timer and timer.returncode == 0 else '' return 200, { 'settings': backup_settings(settings), 'state': vpnrouter_backup.state() or None, 'archives': vpnrouter_backup.archives(settings['path']) if settings['target'] == 'local' else [], 'next': next_run if settings['enabled'] and next_run not in ('', 'n/a') else None, 'client': vpnrouter_backup.PBS_CLIENT.exists(), 'rclone': {'installed': shutil.which('rclone') is not None, 'config': vpnrouter_backup.RCLONE_CONFIG.is_file(), 'remotes': rclone_remotes()}, } settings = {option: body[option] for option in vpnrouter.SCHEMA['backup'] if option in body} if not settings.get('pbs_password'): settings.pop('pbs_password', None) with vpnrouter.config_lock(): data = vpnrouter.read_settings() current = data.get('backup') data['backup'] = {**(current if isinstance(current, dict) else {}), **settings} cfg = vpnrouter.with_defaults(data) vpnrouter.validate(cfg) vpnrouter.save_config(data) except vpnrouter.ConfigError as exc: return 400, {'error': str(exc)} except OSError as exc: return 500, {'error': f'the configuration cannot be written: {exc}'} except SystemExit as exc: return 500, {'error': str(exc.code)} run('systemctl', 'restart', '--no-block', 'vpn-router-setup.service') return 200, {'settings': backup_settings(cfg['backup'])}