From e5df80a3f54da23858c382a8e491ff9901317e1e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 20 Aug 2019 18:53:35 +0200 Subject: T1598: initial implementation of the hosts keeper daemon. --- src/services/vyos-hostsd | 254 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100755 src/services/vyos-hostsd (limited to 'src/services/vyos-hostsd') diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..972c2e3dc --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 . +# +# + +import os +import sys +import time +import json +import traceback + +import zmq + +import jinja2 + +debug = True + +DATA_DIR = "/var/lib/vyos/" +STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +hosts_tmpl_source = """ +### Autogenerated by VyOS ### +127.0.0.1 localhost +127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_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 %} +{% 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 ### +{% for ns in name_servers -%} +nameserver {{ns}} +{% 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": }} +# +# Hosts entries are similar: +# {"host": {"tag": , "address": }} +# +# The tag is either "static" or "dhcp-" +# 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 +STATE = { + "name_servers": {}, + "hosts": {}, + "host_name": "vyos", + "domain_name": "", + "search_domains": []} + + +def make_resolv_conf(data): + resolv_conf = resolv_tmpl.render(data) + print("Writing /etc/resolv.conf") + with open(RESOLV_CONF_FILE, 'w') as f: + f.write(resolv_conf) + +def make_hosts_file(state): + print("Writing /etc/hosts") + hosts = hosts_tmpl.render(state) + with open(HOSTS_FILE, 'w') as f: + f.write(hosts) + +def add_hosts(data, entries, tag): + hosts = data['hosts'] + + if not entries: + return + + for e in entries: + host = e['host'] + hosts[host] = {} + hosts[host]['tag'] = tag + hosts[host]['address'] = e['address'] + hosts[host]['aliases'] = e['aliases'] + +def delete_hosts(data, tag): + hosts = data['hosts'] + keys_for_deletion = [] + + # 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) + + for k in keys_for_deletion: + del hosts[k] + +def add_name_servers(data, entries, tag): + name_servers = data['name_servers'] + + if not entries: + return + + for e in entries: + name_servers[e] = {} + name_servers[e]['tag'] = tag + +def delete_name_servers(data, tag): + name_servers = data['name_servers'] + keys_for_deletion = [] + + for ns in name_servers: + if name_servers[ns]['tag'] == tag: + keys_for_deletion.append(ns) + + for k in keys_for_deletion: + del name_servers[k] + +def set_host_name(state, data): + if data['host_name']: + state['host_name'] = data['host_name'] + if data['domain_name']: + state['domain_name'] = data['domain_name'] + if data['search_domains']: + state['search_domains'] = data['search_domains'] + +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_json): + msg = json.loads(msg_json) + + op = get_option(msg, 'op') + _type = get_option(msg, 'type') + + if op == 'delete': + tag = get_option(msg, 'tag') + + if _type == 'name_servers': + delete_name_servers(STATE, tag) + elif _type == 'hosts': + delete_hosts(STATE, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'add': + tag = get_option(msg, 'tag') + entries = get_option(msg, 'data') + if _type == 'name_servers': + add_name_servers(STATE, entries, tag) + elif _type == 'hosts': + add_hosts(STATE, entries, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'set': + # Host name/domain name/search domain are set without a tag, + # there can be only one anyway + data = get_option(msg, 'data') + if _type == 'host_name': + set_host_name(STATE, data) + else: + raise ValueError("Unknown message type {0}".format(_type)) + else: + raise ValueError("Unknown operation {0}".format(op)) + + make_resolv_conf(STATE) + make_hosts_file(STATE) + + print("Saving state to {0}".format(STATE_FILE)) + with open(STATE_FILE, 'w') as f: + json.dump(STATE, f) + + +if __name__ == '__main__': + # Create a directory for state checkpoints + os.makedirs(DATA_DIR, exist_ok=True) + if os.path.exists(STATE_FILE): + with open(STATE_FILE, 'r') as f: + try: + data = json.load(f) + STATE = data + except: + print(traceback.format_exc()) + print("Failed to load the state file, using default") + + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind(SOCKET_PATH) + + while True: + # Wait for next request from client + message = socket.recv().decode() + print("Received a configuration change request") + if debug: + print("Request data: {0}".format(message)) + + resp = {} + + try: + handle_message(message) + except ValueError as e: + resp['error'] = str(e) + except: + print(traceback.format_exc()) + resp['error'] = "Internal error" + + if debug: + print("Sent response: {0}".format(resp)) + + # Send reply back to client + socket.send(json.dumps(resp).encode()) -- cgit v1.2.3 From 3cbebc67f970f1add6548c5e386b40d5632f8452 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 08:46:56 +0200 Subject: T1598: improve autogenerated file comments. --- src/services/vyos-hostsd | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'src/services/vyos-hostsd') diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 972c2e3dc..aec06c72c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -38,6 +38,9 @@ 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 }}{% endif %} @@ -48,7 +51,7 @@ 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 %} @@ -60,6 +63,8 @@ hosts_tmpl = jinja2.Template(hosts_tmpl_source) resolv_tmpl_source = """ ### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + {% for ns in name_servers -%} nameserver {{ns}} {% endfor -%} @@ -83,7 +88,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source) # {"server": {"tag": }} # # Hosts entries are similar: -# {"host": {"tag": , "address": }} +# {"host": {"tag": , "address": , "aliases": }} # # The tag is either "static" or "dhcp-" # It's used to distinguish entries created -- cgit v1.2.3 From a923efc0062119189d876e37514bd24b7773509e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 09:08:27 +0200 Subject: T1598: clean up vyos-hostsd state dump on clean shutdown. --- src/services/vyos-hostsd | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src/services/vyos-hostsd') diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index aec06c72c..1bcb2e1f5 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -20,6 +20,7 @@ import os import sys import time import json +import signal import traceback import zmq @@ -218,8 +219,16 @@ def handle_message(msg_json): with open(STATE_FILE, 'w') as f: json.dump(STATE, f) +def exit_handler(sig, frame): + """ Clean up the state when shutdown correctly """ + print("Cleaning up state") + os.unlink(STATE_FILE) + sys.exit(0) + if __name__ == '__main__': + signal.signal(signal.SIGTERM, exit_handler) + # Create a directory for state checkpoints os.makedirs(DATA_DIR, exist_ok=True) if os.path.exists(STATE_FILE): -- cgit v1.2.3 From b812bae9e317f7bcc91371316af28d07345682ba Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 26 Aug 2019 19:58:04 +0200 Subject: T1598: add a vyos-hostsd operation for retrieving name servers by tag. --- python/vyos/hostsd_client.py | 7 +++++++ src/services/vyos-hostsd | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'src/services/vyos-hostsd') diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index e2f05071b..f009aba98 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -30,6 +30,8 @@ class Client(object): reply = json.loads(reply_msg) if 'error' in reply: raise VyOSHostsdError(reply['error']) + else: + return reply["data"] except zmq.error.Again: raise VyOSHostsdError("Could not connect to vyos-hostsd") @@ -60,3 +62,8 @@ class Client(object): def delete_name_servers(self, tag): msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} self._communicate(msg) + + def get_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} + return self._communicate(msg) + diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 1bcb2e1f5..8f70eb4e9 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -171,6 +171,14 @@ def set_host_name(state, data): if data['search_domains']: state['search_domains'] = data['search_domains'] +def get_name_servers(state, tag): + ns = [] + data = state['name_servers'] + for n in data: + if data[n]['tag'] == tag: + ns.append(n) + return ns + def get_option(msg, key): if key in msg: return msg[key] @@ -209,6 +217,13 @@ def handle_message(msg_json): set_host_name(STATE, data) else: raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'get': + tag = get_option(msg, 'tag') + if _type == 'name_servers': + result = get_name_servers(STATE, tag) + else: + raise ValueError("Unimplemented") + return result else: raise ValueError("Unknown operation {0}".format(op)) @@ -254,7 +269,8 @@ if __name__ == '__main__': resp = {} try: - handle_message(message) + result = handle_message(message) + resp['data'] = result except ValueError as e: resp['error'] = str(e) except: -- cgit v1.2.3