108 lines
4.3 KiB
Python
Executable File
108 lines
4.3 KiB
Python
Executable File
#!/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())
|