Files
linux-cloud-router/debian-package/src/usr/sbin/vpn-config-edit
T

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:]))