summaryrefslogtreecommitdiff
path: root/src/services/vyos-hostsd
diff options
context:
space:
mode:
authorJernej Jakob <jernej.jakob@gmail.com>2020-06-11 07:58:49 +0200
committerJernej Jakob <jernej.jakob@gmail.com>2020-06-11 22:10:47 +0200
commit8797a010fcad067fbe604ed3e75c0605e9b5d1f7 (patch)
tree37a18356fbaedbf2e80495c1428e313b706424db /src/services/vyos-hostsd
parent62461c73fd6e6616a48d6319d461d89239e4ee2f (diff)
downloadvyos-1x-8797a010fcad067fbe604ed3e75c0605e9b5d1f7.tar.gz
vyos-1x-8797a010fcad067fbe604ed3e75c0605e9b5d1f7.zip
vyos-hostsd: T2583: partial rewrite
- update copyright date - validate incoming JSON data against a schema with voluptuous - add usage help describing internal messages syntax at top of vyos-hostsd - move socket and state file to directory /run/vyos-hostsd - replace jinja2 rendering with vyos.template - move all templates out of the executable into dedicated data/templates dirs - move recursor.conf forward-zones-recurse to forward-zones-file - generate lua-config-file for pdns-recursor with addNTA - support adding custom forward zones for pdns-recursor with optional added NTA and/or recursion-desired - move search_domains from set_host_name to separate add/delete/get commands - unify functions to support abstracting them in the future - track number of internal changes in "changes" variable saved in state file (informational in apply function) - do not apply changes immediately, add apply function that applies all changes (to not reload pdns-recursor excessively for a large set of changes, users must call the apply function once at the end) - add pdns_rec_control function that supports sending arbitrary commands to rec_control (fix pdns-recursor process name that caused the old function to think pdns-recursor was never running) - create /run/powerdns if it doesn't exist (on boot vyos-hostsd starts before pdns-recursor but we need to put our generated conf files there) - abstract specific command functions (add_*/del_*) into general functions to manipulate various types of data in the state variable - add command types: - forward_zones (generate custom forward zones for pdns-recursor) - search_domains (move from set_host_name as dhcp client needs to change them too) - name_server_tags_recursor (to set tags whose nameservers are added to pdns-recursor) - name_server_tags_system (to set tags whose nameservers and search domains are added to /etc/resolv.conf) - change hosts data format to make more sense (move tag from within each host dict to the key for a list of host dicts) - do not remove state file when shut down cleanly, to not lose state when restarting vyos-hostsd service that's then impossible to restore without restarting the whole router - a reboot will remove the state file as it lives in a tmpfs (/run) - remove too verbose info log on every received message - set mode of socket to 770 to secure it against processes not in hostsd group
Diffstat (limited to 'src/services/vyos-hostsd')
-rwxr-xr-xsrc/services/vyos-hostsd703
1 files changed, 500 insertions, 203 deletions
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index bf5d67cfa..0079f7e5c 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -14,7 +14,201 @@
# 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 will be added via lua-config-file.
+#
+# { 'type': 'forward_zones',
+# 'op': 'add',
+# 'data': {
+# '<str zone>': {
+# 'nslist': ['<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
@@ -25,10 +219,10 @@ import traceback
import re
import logging
import zmq
-import collections
-
-import jinja2
-from vyos.util import popen, process_named_running
+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
@@ -43,163 +237,271 @@ if debug:
else:
logger.setLevel(logging.INFO)
-
-DATA_DIR = "/var/lib/vyos/"
-STATE_FILE = os.path.join(DATA_DIR, "hostsd.state")
-
-SOCKET_PATH = "ipc:///run/vyos-hostsd.sock"
+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'
-hosts_tmpl_source = """
-### 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
-
-# From DHCP and "system static host-mapping"
-{%- if hosts %}
-{% for h in hosts -%}
-{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %}
-{% endfor %}
-{%- endif %}
-"""
-
-hosts_tmpl = jinja2.Template(hosts_tmpl_source)
-
-resolv_tmpl_source = """
-### Autogenerated by VyOS ###
-### Do not edit, your changes will get overwritten ###
-
-# name server from static configuration
-{% for ns in name_servers -%}
-{%- if name_servers[ns]['tag'] == "static" %}
-nameserver {{ns}}
-{%- endif %}
-{% endfor -%}
-
-{% for ns in name_servers -%}
-{%- if name_servers[ns]['tag'] != "static" %}
-# name server from {{name_servers[ns]['tag']}}
-nameserver {{ns}}
-{%- endif %}
-{% endfor -%}
-
-{%- if domain_name %}
-domain {{ domain_name }}
-{%- endif %}
-
-{%- if search_domains %}
-search {{ search_domains | join(" ") }}
-{%- endif %}
-
-"""
-
-resolv_tmpl = jinja2.Template(resolv_tmpl_source)
-
-# The state data includes a list of name servers
-# and a list of hosts entries.
-#
-# Name servers have the following structure:
-# {"server": {"tag": <str>}}
-#
-# Hosts entries are similar:
-# {"host": {"tag": <str>, "address": <str>, "aliases": <str list>}}
-#
-# The tag is either "static" or "dhcp-<intf>"
-# It's used to distinguish entries created
-# by different scripts so that they can be removed
-# and re-created without having to track what needs
-# to be changed
+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": collections.OrderedDict({}),
+ "name_servers": {},
+ "name_server_tags_recursor": [],
+ "name_server_tags_system": [],
+ "forward_zones": {},
"hosts": {},
"host_name": "vyos",
"domain_name": "",
- "search_domains": []}
+ "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: {
+ 'nslist': [str],
+ 'addNTA': bool,
+ 'recursion-desired': bool
+ }
+ }
+ }, required=True)
+
+hosts_add_schema = op_type_schema.extend({
+ 'data': {
+ str: {
+ 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(data):
- resolv_conf = resolv_tmpl.render(data)
- logger.info("Writing /etc/resolv.conf")
- with open(RESOLV_CONF_FILE, 'w') as f:
- f.write(resolv_conf)
+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_file(state):
- logger.info("Writing /etc/hosts")
- hosts = hosts_tmpl.render(state)
- with open(HOSTS_FILE, 'w') as f:
- f.write(hosts)
+def make_hosts(state):
+ logger.info(f"Writing {HOSTS_FILE}")
+ render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state,
+ user='root', group='root')
-def add_hosts(data, entries, tag):
- hosts = data['hosts']
+def make_pdns_rec_conf(state):
+ logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}")
- if not entries:
- return
+ # 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)
- for e in entries:
- host = e['host']
- hosts[host] = {}
- hosts[host]['tag'] = tag
- hosts[host]['address'] = e['address']
- hosts[host]['aliases'] = e['aliases']
+ render(PDNS_REC_LUA_CONF_FILE,
+ 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl',
+ state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
-def delete_hosts(data, tag):
- hosts = data['hosts']
- keys_for_deletion = []
+ 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)
- # You can't delete items from a dict while iterating over it,
- # so we build a list of doomed items first
- for h in hosts:
- if hosts[h]['tag'] == tag:
- keys_for_deletion.append(h)
+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
- for k in keys_for_deletion:
- del hosts[k]
+ _dict.update(items)
-def add_name_servers(data, entries, tag):
- name_servers = data['name_servers']
+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 entries:
+ if not items:
return
- for e in entries:
- name_servers[e] = {}
- name_servers[e]['tag'] = tag
+ for item, item_val in items.items():
+ if item not in _dict:
+ _dict[item] = OrderedDict({})
+ _dict[item].update(OrderedDict.fromkeys(item_val))
-def delete_name_servers(data, tag):
- name_servers = data['name_servers']
- regex_filter = re.compile(tag)
- for ns in list(name_servers.keys()):
- if regex_filter.match(name_servers[ns]['tag']):
- del name_servers[ns]
+def add_items_to_list(_list, items):
+ """
+ Dedupes and preserves sort order.
+ """
+ assert isinstance(_list, list)
+ assert isinstance(items, list)
-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']
- if 'search_domains' in data:
- state['search_domains'] = data['search_domains']
-
-def get_name_servers(state, tag):
- ns = []
- data = state['name_servers']
- regex_filter = re.compile(tag)
- for n in data:
- if regex_filter.match(data[n]['tag']):
- ns.append(n)
- return ns
+ 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:
@@ -207,85 +509,77 @@ def get_option(msg, key):
else:
raise ValueError("Missing required option \"{0}\"".format(key))
-def handle_message(msg_json):
- msg = json.loads(msg_json)
-
+def handle_message(msg):
+ result = None
op = get_option(msg, 'op')
- _type = get_option(msg, 'type')
- changes = 0
+ if op in ['add', 'delete', 'set']:
+ STATE['changes'] += 1
if op == 'delete':
- tag = get_option(msg, 'tag')
-
- if _type == 'name_servers':
- delete_name_servers(STATE, tag)
- changes += 1
- elif _type == 'hosts':
- delete_hosts(STATE, tag)
- changes += 1
+ _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("Unknown message type {0}".format(_type))
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
elif op == 'add':
- tag = get_option(msg, 'tag')
- entries = get_option(msg, 'data')
- if _type == 'name_servers':
- add_name_servers(STATE, entries, tag)
- changes += 1
- elif _type == 'hosts':
- add_hosts(STATE, entries, tag)
- changes += 1
+ _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("Unknown message type {0}".format(_type))
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
elif op == 'set':
- # Host name/domain name/search domain are set without a tag,
- # there can be only one anyway
+ _type = get_option(msg, 'type')
data = get_option(msg, 'data')
if _type == 'host_name':
set_host_name(STATE, data)
- changes += 1
else:
- raise ValueError("Unknown message type {0}".format(_type))
+ raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
elif op == 'get':
- tag = get_option(msg, 'tag')
- if _type == 'name_servers':
- result = get_name_servers(STATE, tag)
+ _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("Unimplemented")
- return result
- else:
- raise ValueError("Unknown operation {0}".format(op))
+ 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
- make_resolv_conf(STATE)
- make_hosts_file(STATE)
+ else:
+ raise ValueError(f"Unknown operation {op}")
- logger.info("Saving state to {0}".format(STATE_FILE))
+ logger.debug(f"Saving state to {STATE_FILE}")
with open(STATE_FILE, 'w') as f:
json.dump(STATE, f)
- if changes > 0:
- if process_named_running("pdns_recursor"):
- (ret,return_code) = popen("sudo rec_control --socket-dir=/run/powerdns reload-zones")
- if return_code > 0:
- logger.exception("PowerDNS rec_control failed to reload")
-
-def exit_handler(sig, frame):
- """ Clean up the state when shutdown correctly """
- logger.info("Cleaning up state")
- os.unlink(STATE_FILE)
- sys.exit(0)
-
+ return result
if __name__ == '__main__':
- signal.signal(signal.SIGTERM, exit_handler)
-
# Create a directory for state checkpoints
- os.makedirs(DATA_DIR, exist_ok=True)
+ os.makedirs(RUN_DIR, exist_ok=True)
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
try:
- data = json.load(f)
- STATE = data
+ STATE = json.load(f)
except:
logger.exception(traceback.format_exc())
logger.exception("Failed to load the state file, using default")
@@ -294,28 +588,31 @@ if __name__ == '__main__':
socket = context.socket(zmq.REP)
# Set the right permissions on the socket, then change it back
- o_mask = os.umask(0)
+ o_mask = os.umask(0o007)
socket.bind(SOCKET_PATH)
os.umask(o_mask)
while True:
# Wait for next request from client
- message = socket.recv().decode()
- logger.info("Received a configuration change request")
- logger.debug("Request data: {0}".format(message))
-
- resp = {}
+ msg_json = socket.recv().decode()
+ logger.debug(f"Request data: {msg_json}")
try:
- result = handle_message(message)
- resp['data'] = result
+ 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"
- logger.debug("Sent response: {0}".format(resp))
-
# Send reply back to client
socket.send(json.dumps(resp).encode())
+ logger.debug(f"Sent response: {resp}")