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