#!/usr/bin/python3 """Show and change the router's configuration, /etc/vpn-router/vpn-router.json. show prints the file as indented JSON; the file is not changed. set takes a setting as section.option, such as web.port, or as site_to_site.NAME.option for a site-to-site connection, which it creates with the defaults (vpnrouter.CONNECTION) when it does not exist, and a value, which it converts to the type the package's schema (vpnrouter.SCHEMA) gives the setting: a whole number, true or false, a list of strings separated by commas (an empty value is an empty list), or a string. remove takes site_to_site.NAME and removes that connection. The configuration with the change is checked as vpn-router-setup checks it, and only then written with vpnrouter.save_config, holding vpnrouter.config_lock; vpn-router-setup applies it at its next run, which set and remove remind of. 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-config-edit show vpn-config-edit set SECTION.OPTION VALUE vpn-config-edit set site_to_site.NAME.OPTION VALUE vpn-config-edit remove site_to_site.NAME """ import argparse import copy import json import sys sys.path.insert(0, '/usr/lib/vpn-router') import vpnrouter class Failure(Exception): """A request that cannot be done; status is 2 invalid, 1 other.""" def __init__(self, message, status=1): super().__init__(message) self.status = status class Parser(argparse.ArgumentParser): """An argument parser whose errors are Failures with status 2.""" def error(self, message): raise Failure(f'{message}; see {self.prog} --help', 2) def read(): """The settings in the configuration file, without the defaults; {} when the file does not exist.""" try: return vpnrouter.read_settings() except vpnrouter.ConfigError as exc: raise Failure(str(exc)) def converted(key, text): """The path of the setting key, section.option or site_to_site.NAME.option, and text as the type the schema gives it.""" path = key.split('.') if path[0] == 'site_to_site' and len(path) == 3 and path[2] in vpnrouter.CONNECTION: kind = type(vpnrouter.CONNECTION[path[2]]) elif path[0] != 'site_to_site' and len(path) == 2 and path[0] in vpnrouter.SCHEMA and path[1] in vpnrouter.SCHEMA[path[0]]: kind = type(vpnrouter.SCHEMA[path[0]][path[1]]) else: raise Failure(f'{key}: unknown setting; settings are section.option, such as web.port, ' 'or site_to_site.NAME.option', 2) if kind is bool: if text not in ('true', 'false'): raise Failure(f'{key} takes true or false', 2) return path, text == 'true' if kind is int: try: return path, int(text) except ValueError: raise Failure(f'{key} takes a whole number', 2) if kind is list: return path, [item.strip() for item in text.split(',') if item.strip()] return path, text def main(argv): try: parser = Parser(prog='vpn-config-edit', description='Show and change the router configuration.') commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser) commands.add_parser('show', help='print the configuration as indented JSON') setter = commands.add_parser('set', help='set a setting and write the configuration') setter.add_argument('key', metavar='SECTION.OPTION', help='the setting, such as web.port or site_to_site.NAME.option') setter.add_argument('value', metavar='VALUE', help='its value; a list separated by commas') remover = commands.add_parser('remove', help='remove a site-to-site connection and write the configuration') remover.add_argument('key', metavar='site_to_site.NAME', help='the connection') options = parser.parse_args(argv) if options.command == 'show': print(json.dumps(read(), indent=2)) return 0 with vpnrouter.config_lock(): data = read() if options.command == 'set': path, value = converted(options.key, options.value) if path[0] == 'site_to_site': connections = data.setdefault('site_to_site', {}) connection = connections.setdefault(path[1], copy.deepcopy(vpnrouter.CONNECTION)) if not isinstance(connection, dict): raise Failure(f'site_to_site.{path[1]} is not an object') connection[path[2]] = value else: data.setdefault(path[0], {})[path[1]] = value else: section, _, name = options.key.partition('.') if section != 'site_to_site' or not name or name not in data.get('site_to_site', {}): raise Failure(f'{options.key}: no such site-to-site connection', 2) del data['site_to_site'][name] try: vpnrouter.validate(vpnrouter.with_defaults(data)) except vpnrouter.ConfigError as exc: raise Failure(str(exc), 2) vpnrouter.save_config(data) except Failure as failure: print(f'vpn-config-edit: {failure}', file=sys.stderr) return failure.status except OSError as exc: print(f'vpn-config-edit: {exc}', file=sys.stderr) return 1 if options.command == 'set': print(f'{options.key} set to {json.dumps(value)} in {vpnrouter.CONFIG_FILE}.') else: print(f'{options.key} removed from {vpnrouter.CONFIG_FILE}.') print('Apply it with: systemctl restart vpn-router-setup') return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))