diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl index 8b73c6e51..03662d562 100644 --- a/data/templates/vyos-hostsd/hosts.tmpl +++ b/data/templates/vyos-hostsd/hosts.tmpl @@ -1,24 +1,25 @@ ### Autogenerated by VyOS ### ### Do not edit, your changes will get overwritten ### # Local host 127.0.0.1 localhost 127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters {% if hosts is defined and hosts is not none %} # From 'system static-host-mapping' and DHCP server {% for tag, taghosts in hosts.items() %} # {{ tag }} {% for host, hostprops in taghosts.items() if hostprops.address is defined %} -{{ "%-15s" | format(hostprops.address) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% for addr in hostprops.address %} +{{ "%-15s" | format(addr) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% endfor %} {% endfor %} {% endfor %} {% endif %} - diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index 2b1644609..005a55ab3 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -1,114 +1,114 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="system"> <children> <leafNode name="name-server" owner="${vyos_conf_scripts_dir}/host_name.py"> <properties> <help>System Domain Name Servers (DNS)</help> <priority>400</priority> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <valueHelp> <format>ipv4</format> <description>Domain Name Server IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>Domain Name Server IPv6 address</description> </valueHelp> <valueHelp> <format>txt</format> <description>Use Domain Name Server from DHCP interface</description> </valueHelp> <multi/> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> <validator name="interface-name"/> </constraint> </properties> </leafNode> <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py"> <properties> <help>System host name (default: vyos)</help> <constraint> <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> </constraint> </properties> </leafNode> <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py"> <properties> <help>System domain name</help> <constraint> <validator name="fqdn"/> </constraint> </properties> </leafNode> <node name="domain-search" owner="${vyos_conf_scripts_dir}/host_name.py"> <properties> <help>Domain Name Server (DNS) domain completion order</help> <priority>400</priority> </properties> <children> <leafNode name="domain"> <properties> <help>DNS domain completion order</help> <constraint> <regex>[-a-zA-Z0-9.]+$</regex> </constraint> <constraintErrorMessage>Invalid domain name</constraintErrorMessage> <multi/> </properties> </leafNode> </children> </node> <node name="static-host-mapping" owner="${vyos_conf_scripts_dir}/host_name.py"> <properties> <help>Map host names to addresses</help> <priority>400</priority> </properties> <children> <tagNode name="host-name"> <properties> <help>Host name for static address mapping</help> <constraint> <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$</regex> </constraint> <constraintErrorMessage>invalid hostname</constraintErrorMessage> </properties> <children> <leafNode name="alias"> <properties> <help>Alias for this address</help> <constraint> <regex>.{1,63}$</regex> </constraint> <constraintErrorMessage>invalid alias hostname, needs to be between 1 and 63 charactes</constraintErrorMessage> <multi /> </properties> </leafNode> <leafNode name="inet"> <properties> <help>IP Address [REQUIRED]</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address</description> </valueHelp> <constraint> <validator name="ip-address"/> </constraint> + <multi/> </properties> </leafNode> </children> </tagNode> - </children> </node> </children> </node> </interfaceDefinition> diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index a7135911d..87bad0dc6 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -1,187 +1,187 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import re import sys import copy import vyos.util import vyos.hostsd_client from vyos import ConfigError from vyos.config import Config from vyos.ifconfig import Section from vyos.template import is_ip from vyos.util import cmd from vyos.util import call from vyos.util import process_named_running from vyos import airbag airbag.enable() default_config_data = { 'hostname': 'vyos', 'domain_name': '', 'domain_search': [], 'nameserver': [], 'nameservers_dhcp_interfaces': {}, 'static_host_mapping': {} } hostsd_tag = 'system' def get_config(config=None): if config: conf = config else: conf = Config() hosts = copy.deepcopy(default_config_data) hosts['hostname'] = conf.return_value(['system', 'host-name']) # This may happen if the config is not loaded yet, # e.g. if run by cloud-init if not hosts['hostname']: hosts['hostname'] = default_config_data['hostname'] if conf.exists(['system', 'domain-name']): hosts['domain_name'] = conf.return_value(['system', 'domain-name']) hosts['domain_search'].append(hosts['domain_name']) for search in conf.return_values(['system', 'domain-search', 'domain']): hosts['domain_search'].append(search) if conf.exists(['system', 'name-server']): for ns in conf.return_values(['system', 'name-server']): if is_ip(ns): hosts['nameserver'].append(ns) else: tmp = '' if_type = Section.section(ns) if conf.exists(['interfaces', if_type, ns, 'address']): tmp = conf.return_values(['interfaces', if_type, ns, 'address']) hosts['nameservers_dhcp_interfaces'].update({ ns : tmp }) # system static-host-mapping for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']): hosts['static_host_mapping'][hn] = {} - hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet']) + hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet']) hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias']) return hosts def verify(hosts): if hosts is None: return None # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") if not hostname_regex.match(hosts['hostname']): raise ConfigError('Invalid host name ' + hosts["hostname"]) # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" length = len(hosts['hostname']) if length < 1 or length > 63: raise ConfigError( 'Invalid host-name length, must be less than 63 characters') all_static_host_mapping_addresses = [] # static mappings alias hostname for host, hostprops in hosts['static_host_mapping'].items(): if not hostprops['address']: raise ConfigError(f'IP address required for static-host-mapping "{host}"') all_static_host_mapping_addresses.append(hostprops['address']) for a in hostprops['aliases']: if not hostname_regex.match(a) and len(a) != 0: raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"') for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items(): # Warnin user if interface does not have DHCP or DHCPv6 configured if not set(interface_config).intersection(['dhcp', 'dhcpv6']): print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!') return None def generate(config): pass def apply(config): if config is None: return None ## Send the updated data to vyos-hostsd try: hc = vyos.hostsd_client.Client() hc.set_host_name(config['hostname'], config['domain_name']) hc.delete_search_domains([hostsd_tag]) if config['domain_search']: hc.add_search_domains({hostsd_tag: config['domain_search']}) hc.delete_name_servers([hostsd_tag]) if config['nameserver']: hc.add_name_servers({hostsd_tag: config['nameserver']}) # add our own tag's (system) nameservers and search to resolv.conf hc.delete_name_server_tags_system(hc.get_name_server_tags_system()) hc.add_name_server_tags_system([hostsd_tag]) # this will add the dhcp client nameservers to resolv.conf for intf in config['nameservers_dhcp_interfaces']: hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}']) hc.delete_hosts([hostsd_tag]) if config['static_host_mapping']: hc.add_hosts({hostsd_tag: config['static_host_mapping']}) hc.apply() except vyos.hostsd_client.VyOSHostsdError as e: raise ConfigError(str(e)) ## Actually update the hostname -- vyos-hostsd doesn't do that # No domain name -- the Debian way. hostname_new = config['hostname'] # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. hostname_old = cmd('hostnamectl --static') call(f'hostnamectl set-hostname --static {hostname_new}') # Restart services that use the hostname if hostname_new != hostname_old: call("systemctl restart rsyslog.service") # If SNMP is running, restart it too if process_named_running('snmpd'): call('systemctl restart snmpd.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) sys.exit(1) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 4c4bb036e..f4b1d0fc2 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -1,619 +1,619 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # ######### # USAGE # ######### # This daemon listens on its socket for JSON messages. # The received message format is: # # { 'type': '<message type>', # 'op': '<message operation>', # 'data': <data list or dict> # } # # For supported message types, see below. # 'op' can be 'add', delete', 'get', 'set' or 'apply'. # Different message types support different sets of operations and different # data formats. # # Changes to configuration made via add or delete don't take effect immediately, # they are remembered in a state variable and saved to disk to a state file. # State is remembered across daemon restarts but not across system reboots # as it's saved in a temporary filesystem (/run). # # 'apply' is a special operation that applies the configuration from the cached # state, rendering all config files and reloading relevant daemons (currently # just pdns-recursor via rec-control). # # note: 'add' operation also acts as 'update' as it uses dict.update, if the # 'data' dict item value is a dict. If it is a list, it uses list.append. # ### tags # Tags can be arbitrary, but they are generally in this format: # 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>' # They are used to distinguish entries created by different scripts so they can # be removed and recreated without having to track what needs to be changed. # They are also used as a way to control which tags settings (e.g. nameservers) # get added to various config files via name_server_tags_(recursor|system) # ### name_server_tags_(recursor|system) # A list of tags whose nameservers and search domains is used to generate # /etc/resolv.conf and pdns-recursor config. # system list is used to generate resolv.conf. # recursor list is used to generate pdns-rec forward-zones. # When generating each file, the order of nameservers is as per the order of # name_server_tags (the order in which tags were added), then the order in # which the name servers for each tag were added. # #### Message types # ### name_servers # # { 'type': 'name_servers', # 'op': 'add', # 'data': { # '<str tag>': ['<str nameserver>', ...], # ... # } # } # # { 'type': 'name_servers', # 'op': 'delete', # 'data': ['<str tag>', ...] # } # # { 'type': 'name_servers', # 'op': 'get', # 'tag_regex': '<str regex>' # } # response: # { 'data': { # '<str tag>': ['<str nameserver>', ...], # ... # } # } # ### name_server_tags # # { 'type': 'name_server_tags', # 'op': 'add', # 'data': ['<str tag>', ...] # } # # { 'type': 'name_server_tags', # 'op': 'delete', # 'data': ['<str tag>', ...] # } # # { 'type': 'name_server_tags', # 'op': 'get', # } # response: # { 'data': ['<str tag>', ...] } # ### forward_zones ## Additional zones added to pdns-recursor forward-zones-file. ## If recursion_desired is true, '+' will be prepended to the zone line. ## If addnta is true, a NTA (Negative Trust Anchor) will be added via ## lua-config-file. # # { 'type': 'forward_zones', # 'op': 'add', # 'data': { # '<str zone>': { # 'server': ['<str nameserver>', ...], # 'addnta': <bool>, # 'recursion_desired': <bool> # } # ... # } # } # # { 'type': 'forward_zones', # 'op': 'delete', # 'data': ['<str zone>', ...] # } # # { 'type': 'forward_zones', # 'op': 'get', # } # response: # { 'data': { # '<str zone>': { ... }, # ... # } # } # # ### search_domains # # { 'type': 'search_domains', # 'op': 'add', # 'data': { # '<str tag>': ['<str domain>', ...], # ... # } # } # # { 'type': 'search_domains', # 'op': 'delete', # 'data': ['<str tag>', ...] # } # # { 'type': 'search_domains', # 'op': 'get', # } # response: # { 'data': { # '<str tag>': ['<str domain>', ...], # ... # } # } # ### hosts # # { 'type': 'hosts', # 'op': 'add', # 'data': { # '<str tag>': { # '<str host>': { # 'address': '<str address>', # 'aliases': ['<str alias>, ...] # }, # ... # }, # ... # } # } # # { 'type': 'hosts', # 'op': 'delete', # 'data': ['<str tag>', ...] # } # # { 'type': 'hosts', # 'op': 'get' # 'tag_regex': '<str regex>' # } # response: # { 'data': { # '<str tag>': { # '<str host>': { # 'address': '<str address>', # 'aliases': ['<str alias>, ...] # }, # ... # }, # ... # } # } ### host_name # # { 'type': 'host_name', # 'op': 'set', # 'data': { # 'host_name': '<str hostname>' # 'domain_name': '<str domainname>' # } # } import os import sys import time import json import signal import traceback import re import logging import zmq from voluptuous import Schema, MultipleInvalid, Required, Any from collections import OrderedDict from vyos.util import popen, chown, chmod_755, makedir, process_named_running from vyos.template import render debug = True # Configure logging logger = logging.getLogger(__name__) # set stream as output logs_handler = logging.StreamHandler() logger.addHandler(logs_handler) if debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) RUN_DIR = "/run/vyos-hostsd" STATE_FILE = os.path.join(RUN_DIR, "vyos-hostsd.state") SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock') RESOLV_CONF_FILE = '/etc/resolv.conf' HOSTS_FILE = '/etc/hosts' PDNS_REC_USER = PDNS_REC_GROUP = 'pdns' PDNS_REC_RUN_DIR = '/run/powerdns' PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' STATE = { "name_servers": {}, "name_server_tags_recursor": [], "name_server_tags_system": [], "forward_zones": {}, "hosts": {}, "host_name": "vyos", "domain_name": "", "search_domains": {}, "changes": 0 } # the base schema that every received message must be in base_schema = Schema({ Required('op'): Any('add', 'delete', 'set', 'get', 'apply'), 'type': Any('name_servers', 'name_server_tags_recursor', 'name_server_tags_system', 'forward_zones', 'search_domains', 'hosts', 'host_name'), 'data': Any(list, dict), 'tag': str, 'tag_regex': str }) # more specific schemas op_schema = Schema({ 'op': str, }, required=True) op_type_schema = op_schema.extend({ 'type': str, }, required=True) host_name_add_schema = op_type_schema.extend({ 'data': { 'host_name': str, 'domain_name': Any(str, None) } }, required=True) data_dict_list_schema = op_type_schema.extend({ 'data': { str: [str] } }, required=True) data_list_schema = op_type_schema.extend({ 'data': [str] }, required=True) tag_regex_schema = op_type_schema.extend({ 'tag_regex': str }, required=True) forward_zone_add_schema = op_type_schema.extend({ 'data': { str: { 'server': [str], 'addnta': Any({}, None), 'recursion_desired': Any({}, None), } } }, required=False) hosts_add_schema = op_type_schema.extend({ 'data': { str: { str: { - 'address': str, + 'address': [str], 'aliases': [str] } } } }, required=True) # op and type to schema mapping msg_schema_map = { 'name_servers': { 'add': data_dict_list_schema, 'delete': data_list_schema, 'get': tag_regex_schema }, 'name_server_tags_recursor': { 'add': data_list_schema, 'delete': data_list_schema, 'get': op_type_schema }, 'name_server_tags_system': { 'add': data_list_schema, 'delete': data_list_schema, 'get': op_type_schema }, 'forward_zones': { 'add': forward_zone_add_schema, 'delete': data_list_schema, 'get': op_type_schema }, 'search_domains': { 'add': data_dict_list_schema, 'delete': data_list_schema, 'get': tag_regex_schema }, 'hosts': { 'add': hosts_add_schema, 'delete': data_list_schema, 'get': tag_regex_schema }, 'host_name': { 'set': host_name_add_schema }, None: { 'apply': op_schema } } def validate_schema(data): base_schema(data) try: schema = msg_schema_map[data['type'] if 'type' in data else None][data['op']] schema(data) except KeyError: raise ValueError(( 'Invalid or unknown combination: ' f'op: "{data["op"]}", type: "{data["type"]}"')) def pdns_rec_control(command): # pdns-r process name is NOT equal to the name shown in ps if not process_named_running('pdns-r/worker'): logger.info(f'pdns_recursor not running, not sending "{command}"') return logger.info(f'Running "rec_control {command}"') (ret,ret_code) = popen(( f"rec_control --socket-dir={PDNS_REC_RUN_DIR} {command}")) if ret_code > 0: logger.exception(( f'"rec_control {command}" failed with exit status {ret_code}, ' f'output: "{ret}"')) def make_resolv_conf(state): logger.info(f"Writing {RESOLV_CONF_FILE}") render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state, user='root', group='root') def make_hosts(state): logger.info(f"Writing {HOSTS_FILE}") render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state, user='root', group='root') def make_pdns_rec_conf(state): logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}") # on boot, /run/powerdns does not exist, so create it makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP) chmod_755(PDNS_REC_RUN_DIR) render(PDNS_REC_LUA_CONF_FILE, 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl', state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) logger.info(f"Writing {PDNS_REC_ZONES_FILE}") render(PDNS_REC_ZONES_FILE, 'dns-forwarding/recursor.forward-zones.conf.tmpl', state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) def set_host_name(state, data): if data['host_name']: state['host_name'] = data['host_name'] if 'domain_name' in data: state['domain_name'] = data['domain_name'] def add_items_to_dict(_dict, items): """ Dedupes and preserves sort order. """ assert isinstance(_dict, dict) assert isinstance(items, dict) if not items: return _dict.update(items) def add_items_to_dict_as_keys(_dict, items): """ Added item values are converted to OrderedDict with the value as keys and null values. This is to emulate a list but with inherent deduplication. Dedupes and preserves sort order. """ assert isinstance(_dict, dict) assert isinstance(items, dict) if not items: return for item, item_val in items.items(): if item not in _dict: _dict[item] = OrderedDict({}) _dict[item].update(OrderedDict.fromkeys(item_val)) def add_items_to_list(_list, items): """ Dedupes and preserves sort order. """ assert isinstance(_list, list) assert isinstance(items, list) if not items: return for item in items: if item not in _list: _list.append(item) def delete_items_from_dict(_dict, items): """ items is a list of keys to delete. Doesn't error if the key doesn't exist. """ assert isinstance(_dict, dict) assert isinstance(items, list) for item in items: if item in _dict: del _dict[item] def delete_items_from_list(_list, items): """ items is a list of items to remove. Doesn't error if the key doesn't exist. """ assert isinstance(_list, list) assert isinstance(items, list) for item in items: if item in _list: _list.remove(item) def get_items_from_dict_regex(_dict, item_regex_string): """ Returns the items whose keys match item_regex_string. """ assert isinstance(_dict, dict) assert isinstance(item_regex_string, str) tmp = {} regex = re.compile(item_regex_string) for item in _dict: if regex.match(item): tmp[item] = _dict[item] return tmp def get_option(msg, key): if key in msg: return msg[key] else: raise ValueError("Missing required option \"{0}\"".format(key)) def handle_message(msg): result = None op = get_option(msg, 'op') if op in ['add', 'delete', 'set']: STATE['changes'] += 1 if op == 'delete': _type = get_option(msg, 'type') data = get_option(msg, 'data') if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']: delete_items_from_dict(STATE[_type], data) elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: delete_items_from_list(STATE[_type], data) else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') elif op == 'add': _type = get_option(msg, 'type') data = get_option(msg, 'data') if _type in ['name_servers', 'search_domains']: add_items_to_dict_as_keys(STATE[_type], data) elif _type in ['forward_zones', 'hosts']: add_items_to_dict(STATE[_type], data) # maybe we need to rec_control clear-nta each domain that was removed here? elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: add_items_to_list(STATE[_type], data) else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') elif op == 'set': _type = get_option(msg, 'type') data = get_option(msg, 'data') if _type == 'host_name': set_host_name(STATE, data) else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') elif op == 'get': _type = get_option(msg, 'type') if _type in ['name_servers', 'search_domains', 'hosts']: tag_regex = get_option(msg, 'tag_regex') result = get_items_from_dict_regex(STATE[_type], tag_regex) elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']: result = STATE[_type] else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') elif op == 'apply': logger.info(f"Applying {STATE['changes']} changes") make_resolv_conf(STATE) make_hosts(STATE) make_pdns_rec_conf(STATE) pdns_rec_control('reload-lua-config') pdns_rec_control('reload-zones') logger.info("Success") result = {'message': f'Applied {STATE["changes"]} changes'} STATE['changes'] = 0 else: raise ValueError(f"Unknown operation {op}") logger.debug(f"Saving state to {STATE_FILE}") with open(STATE_FILE, 'w') as f: json.dump(STATE, f) return result if __name__ == '__main__': # Create a directory for state checkpoints os.makedirs(RUN_DIR, exist_ok=True) if os.path.exists(STATE_FILE): with open(STATE_FILE, 'r') as f: try: STATE = json.load(f) except: logger.exception(traceback.format_exc()) logger.exception("Failed to load the state file, using default") context = zmq.Context() socket = context.socket(zmq.REP) # Set the right permissions on the socket, then change it back o_mask = os.umask(0o000) socket.bind(SOCKET_PATH) os.umask(o_mask) while True: # Wait for next request from client msg_json = socket.recv().decode() logger.debug(f"Request data: {msg_json}") try: msg = json.loads(msg_json) validate_schema(msg) resp = {} resp['data'] = handle_message(msg) except ValueError as e: resp['error'] = str(e) except MultipleInvalid as e: # raised by schema resp['error'] = f'Invalid message: {str(e)}' logger.exception(resp['error']) except: logger.exception(traceback.format_exc()) resp['error'] = "Internal error" # Send reply back to client socket.send(json.dumps(resp).encode()) logger.debug(f"Sent response: {resp}")