#!/usr/bin/python3 """Write a candidate vpn-router.json to the path given on the command line. Called from postinst, which hands the result to ucf(1). ucf compares it with /etc/vpn-router/vpn-router.json and decides what to do about local changes, so this script never writes to /etc and never has to worry about destroying an administrator's edits. The candidate is the current /etc/vpn-router/vpn-router.json as it is, or the shipped defaults when there is none, with every debconf answer that was given written over it. Without an answer that changes it, the candidate is the current file byte for byte, so an upgrade or a reconfiguration that changes nothing gives ucf nothing to ask; defaults the file leaves out are applied by vpnrouter.load_config() when the file is read. An empty answer means "leave alone", which is why clearing the pre-shared key from the debconf database after installation does not blank it on the next dpkg-reconfigure. Answers arrive as VPN_ROUTER_* environment variables and are converted to the type of the setting they fill: comma-separated lists become arrays, "true" and "false" become booleans and digits become integers. """ import json import os import sys sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) import vpnrouter # environment variable -> (section, option) ANSWERS = { 'VPN_ROUTER_PLATFORM': ('general', 'platform'), 'VPN_ROUTER_MODE': ('general', 'mode'), 'VPN_ROUTER_INT_ADDR': ('local', 'int_addr'), 'VPN_ROUTER_EXTERNAL_INTERFACE': ('interfaces', 'external'), 'VPN_ROUTER_INTERNAL_INTERFACE': ('interfaces', 'internal'), 'VPN_ROUTER_LOCAL_FQDN': ('wan', 'local_fqdn'), 'VPN_ROUTER_LOCAL_ID_MODE': ('wan', 'local_id_mode'), 'VPN_ROUTER_LOCAL_CIDRS': ('local', 'cidrs'), 'VPN_ROUTER_INT_GATEWAY_IP': ('local', 'int_gateway_ip'), 'VPN_ROUTER_REMOTE_ADDRS': ('remote', 'addrs'), 'VPN_ROUTER_REMOTE_ID': ('remote', 'id'), 'VPN_ROUTER_REMOTE_CIDRS': ('remote', 'cidrs'), 'VPN_ROUTER_PSK': ('remote', 'psk'), 'VPN_ROUTER_P2S_ENABLED': ('p2s', 'enabled'), 'VPN_ROUTER_P2S_ADDRESS_POOL': ('p2s', 'address_pool'), 'VPN_ROUTER_P2S_CA_NAME': ('p2s', 'ca_name'), 'VPN_ROUTER_WG_ENABLED': ('wireguard', 'enabled'), 'VPN_ROUTER_WG_ADDRESS': ('wireguard', 'address'), 'VPN_ROUTER_WG_LISTEN_PORT': ('wireguard', 'listen_port'), } def main(): if len(sys.argv) != 2: print('usage: generate-config OUTPUT', file=sys.stderr) return 2 output = sys.argv[1] try: current = vpnrouter.CONFIG_FILE.read_text(encoding='utf-8') except FileNotFoundError: current = None if current is None: cfg = vpnrouter.load_config() else: try: cfg = json.loads(current) except ValueError as exc: vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}') if not isinstance(cfg, dict): vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: the top level is not an object') answered = False for variable, (section, option) in ANSWERS.items(): value = os.environ.get(variable) if not value: continue # A value that does not convert is kept as a string for validate() to report. default = vpnrouter.SCHEMA[section][option] if isinstance(default, bool): value = value == 'true' elif isinstance(default, int): value = int(value) if value.isdigit() else value elif isinstance(default, list): value = [item.strip() for item in value.split(',') if item.strip()] if not isinstance(cfg.get(section, {}), dict): vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {section} is not an object') if cfg.setdefault(section, {}).get(option) != value: cfg[section][option] = value answered = True # Without a change the candidate is the current file byte for byte, as ucf compares checksums. with open(output, 'w', encoding='utf-8') as handle: if current is not None and not answered: handle.write(current) else: json.dump(cfg, handle, indent=2) handle.write('\n') os.chmod(output, 0o600) return 0 if __name__ == '__main__': sys.exit(main())