From c13a4e84afd832e971054f2972ffb9364ef17aeb Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Mon, 15 Oct 2018 21:09:40 -0400 Subject: [PATCH] Add DNS aggregator via PowerDNS and sqlite3 --- node-daemon/pvcd/DNSAggregatorInstance.py | 183 ++++++++++++++++++ node-daemon/pvcd/Daemon.py | 15 +- node-daemon/pvcd/NodeInstance.py | 13 +- node-daemon/pvcd/VXNetworkInstance.py | 8 +- .../pvcd/powerdns-aggregator-schema.sql | 92 +++++++++ 5 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 node-daemon/pvcd/DNSAggregatorInstance.py create mode 100644 node-daemon/pvcd/powerdns-aggregator-schema.sql diff --git a/node-daemon/pvcd/DNSAggregatorInstance.py b/node-daemon/pvcd/DNSAggregatorInstance.py new file mode 100644 index 00000000..ab2fcc1a --- /dev/null +++ b/node-daemon/pvcd/DNSAggregatorInstance.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +# DNSAggregatorInstance.py - Class implementing a DNS aggregator and run by pvcd +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 sqlite3 + +import pvcd.log as log +import pvcd.zkhandler as zkhandler +import pvcd.common as common + +# A barebones PowerDNS sqlite schema (relative to the active dir) +sql_schema_file = 'pvcd/powerdns-aggregator-schema.sql' + +class DNSAggregatorInstance(object): + # Initialization function + def __init__(self, zk_conn, config, logger, d_network): + self.zk_conn = zk_conn + self.config = config + self.logger = logger + self.d_network = d_network + + self.active = False + self.database_file = self.config['pdns_dynamic_directory'] + '/pdns-aggregator.sqlite3' + + self.dns_server_daemon = None + + self.prepare_db() + + # Preparation function + def prepare_db(self): + # Connect to the database + sql_conn = sqlite3.connect(self.database_file) + sql_curs = sql_conn.cursor() + + # Try to access the domains table + try: + sql_curs.execute( + 'select * from domains' + ) + write_schema = False + # The table doesn't exist, so we should create it + except sqlite3.OperationalError: + write_schema = True + + if write_schema: + with open('{}/{}'.format(os.getcwd(), sql_schema_file), 'r') as schema_file: + schema = ''.join(schema_file.readlines()) + sql_curs.executescript(schema) + + sql_conn.close() + + # Add a new network to the aggregator database + def add_client_network(self, network): + network_domain = self.d_network[network].domain + network_gateway = self.d_network[network].ip_gateway + + self.logger.out( + 'Adding entry for client domain {}'.format( + network_domain + ), + prefix='DNS aggregator', + state='o' + ) + # Connect to the database + sql_conn = sqlite3.connect(self.database_file) + sql_curs = sql_conn.cursor() + # Try to access the domains entry + sql_curs.execute( + 'select * from domains where name=?', + (network_domain,) + ) + results = sql_curs.fetchall() + if results: + write_domain = False + else: + write_domain = True + + if write_domain: + sql_curs.execute( + 'insert into domains (name, master, type, account) values (?, ?, "SLAVE", "internal")', + (network_domain, network_gateway) + ) + sql_conn.commit() + + sql_conn.close() + + # Remove a deleted network from the aggregator database + def remove_client_network(self, network): + network_domain = self.d_network[network].domain + + self.logger.out( + 'Removing entry for client domain {}'.format( + network_domain + ), + prefix='DNS aggregator', + state='o' + ) + # Connect to the database + sql_conn = sqlite3.connect(self.database_file) + sql_curs = sql_conn.cursor() + print(network_domain) + sql_curs.execute( + 'delete from domains where name=?', + (network_domain,) + ) + sql_conn.commit() + sql_conn.close() + + # Force AXFR + def get_axfr(self, network): + self.logger.out( + 'Perform AXFR for {}'.format( + self.d_network[network].domain + ), + prefix='DNS aggregator', + state='o' + ) + common.run_os_command('/usr/bin/pdns_control --socket-dir={} retrieve {}'.format(self.config['pdns_dynamic_directory'], self.d_network[network].domain)) + + # Start up the PowerDNS instance + def start_aggregator(self): + self.logger.out( + 'Starting PowerDNS zone aggregator', + state='o' + ) + # Define the PowerDNS config + dns_configuration = [ + '--no-config', + '--daemon=no', + '--disable-syslog=yes', + '--disable-axfr=no', + '--guardian=yes', + '--local-address=0.0.0.0', + '--local-port=10053', + '--log-dns-details=on', + '--loglevel=3', + '--master=no', + '--slave=yes', + '--slave-cycle-interval=5', + '--version-string=powerdns', + '--socket-dir={}'.format(self.config['pdns_dynamic_directory']), + '--launch=gsqlite3', + '--gsqlite3-database={}'.format(self.database_file), + '--gsqlite3-dnssec=no' + ] + # Start the pdns process in a thread + self.dns_server_daemon = common.run_os_daemon( + '/usr/sbin/pdns_server {}'.format( + ' '.join(dns_configuration) + ), + environment=None, + logfile='{}/pdns-aggregator.log'.format(self.config['pdns_log_directory']) + ) + + # Stop the PowerDNS instance + def stop_aggregator(self): + if self.dns_server_daemon: + self.logger.out( + 'Stopping PowerDNS zone aggregator', + state='o' + ) + self.dns_server_daemon.signal('term') diff --git a/node-daemon/pvcd/Daemon.py b/node-daemon/pvcd/Daemon.py index cee9fc41..b3806ad0 100644 --- a/node-daemon/pvcd/Daemon.py +++ b/node-daemon/pvcd/Daemon.py @@ -44,6 +44,7 @@ import pvcd.common as common import pvcd.DomainInstance as DomainInstance import pvcd.NodeInstance as NodeInstance import pvcd.VXNetworkInstance as VXNetworkInstance +import pvcd.DNSAggregatorInstance as DNSAggregatorInstance ############################################################################### # PVCD - node daemon startup program @@ -506,6 +507,12 @@ node_list = [] network_list = [] domain_list = [] +# Create an instance of the DNS Aggregator if we're a coordinator +if config['daemon_mode'] == 'coordinator': + dns_aggregator = DNSAggregatorInstance.DNSAggregatorInstance(zk_conn, config, logger, d_network) +else: + dns_aggregator = None + # Node objects @zk_conn.ChildrenWatch('/nodes') def update_nodes(new_node_list): @@ -514,7 +521,7 @@ def update_nodes(new_node_list): # Add any missing nodes to the list for node in new_node_list: if not node in node_list: - d_node[node] = NodeInstance.NodeInstance(node, myhostname, zk_conn, config, logger, d_node, d_network, d_domain) + d_node[node] = NodeInstance.NodeInstance(node, myhostname, zk_conn, config, logger, d_node, d_network, d_domain, dns_aggregator) # Remove any deleted nodes from the list for node in node_list: @@ -542,6 +549,7 @@ def update_networks(new_network_list): for network in new_network_list: if not network in network_list: d_network[network] = VXNetworkInstance.VXNetworkInstance(network, zk_conn, config, logger, this_node) + dns_aggregator.add_client_network(network) # Start primary functionality if this_node.router_state == 'primary': d_network[network].createGatewayAddress() @@ -557,9 +565,14 @@ def update_networks(new_network_list): # Stop general functionality d_network[network].removeFirewall() d_network[network].removeNetwork() + dns_aggregator.remove_client_network(network) # Delete the object del(d_network[network]) +# if config['daemon_mode'] == 'coordinator': +# # Update the DNS aggregator +# dns_aggregator.update_network_list(d_network) + # Update and print new list network_list = new_network_list logger.out('{}Network list:{} {}'.format(logger.fmt_blue, logger.fmt_end, ' '.join(network_list)), state='i') diff --git a/node-daemon/pvcd/NodeInstance.py b/node-daemon/pvcd/NodeInstance.py index 4325b8ca..ec5deecc 100644 --- a/node-daemon/pvcd/NodeInstance.py +++ b/node-daemon/pvcd/NodeInstance.py @@ -35,7 +35,7 @@ import pvcd.common as common class NodeInstance(object): # Initialization function - def __init__(self, name, this_node, zk_conn, config, logger, d_node, d_network, d_domain): + def __init__(self, name, this_node, zk_conn, config, logger, d_node, d_network, d_domain, dns_aggregator): # Passed-in variables on creation self.name = name self.this_node = this_node @@ -55,6 +55,7 @@ class NodeInstance(object): self.d_node = d_node self.d_network = d_network self.d_domain = d_domain + self.dns_aggregator = dns_aggregator # Printable lists self.active_node_list = [] self.flushed_node_list = [] @@ -289,17 +290,25 @@ class NodeInstance(object): def become_secondary(self): self.logger.out('Setting router {} to secondary state'.format(self.name), state='i') self.logger.out('Network list: {}'.format(', '.join(self.network_list))) - time.sleep(0.5) + time.sleep(1) for network in self.d_network: self.d_network[network].stopDHCPServer() self.d_network[network].removeGatewayAddress() + self.dns_aggregator.stop_aggregator() def become_primary(self): self.logger.out('Setting router {} to primary state.'.format(self.name), state='i') self.logger.out('Network list: {}'.format(', '.join(self.network_list))) + self.dns_aggregator.start_aggregator() + time.sleep(0.5) + # Start up the gateways and DHCP servers for network in self.d_network: self.d_network[network].createGatewayAddress() self.d_network[network].startDHCPServer() + time.sleep(0.5) + # Handle AXFRs after to avoid slowdowns + for network in self.d_network: + self.dns_aggregator.get_axfr(network) # Flush all VMs on the host def flush(self): diff --git a/node-daemon/pvcd/VXNetworkInstance.py b/node-daemon/pvcd/VXNetworkInstance.py index 45592c0e..385b3ae7 100644 --- a/node-daemon/pvcd/VXNetworkInstance.py +++ b/node-daemon/pvcd/VXNetworkInstance.py @@ -324,7 +324,6 @@ add rule inet filter input meta iifname {bridgenic} counter drop dhcp_configuration = [ '--domain-needed', '--bogus-priv', -# '--no-resolv', '--no-hosts', '--filterwin2k', '--expand-hosts', @@ -338,10 +337,10 @@ add rule inet filter input meta iifname {bridgenic} counter drop '--bind-interfaces', '--leasefile-ro', '--dhcp-script=/usr/share/pvc/pvcd/dnsmasq-zookeeper-leases.py', - '--dhcp-range={},{},4h'.format(self.dhcp_start, self.dhcp_end), + '--dhcp-range={},{},48h'.format(self.dhcp_start, self.dhcp_end), '--dhcp-hostsdir={}'.format(self.dnsmasq_hostsdir), + '--log-facility=-', '--log-queries=extra', - '--log-facility={}/dnsmasq.log'.format(self.config['dnsmasq_log_directory']), '--keep-in-foreground' ] # Start the dnsmasq process in a thread @@ -349,7 +348,8 @@ add rule inet filter input meta iifname {bridgenic} counter drop '/usr/sbin/dnsmasq {}'.format( ' '.join(dhcp_configuration) ), - environment=dhcp_environment + environment=dhcp_environment, + logfile='{}/dnsmasq-{}.log'.format(self.config['dnsmasq_log_directory'], self.vni) ) def removeNetwork(self): diff --git a/node-daemon/pvcd/powerdns-aggregator-schema.sql b/node-daemon/pvcd/powerdns-aggregator-schema.sql new file mode 100644 index 00000000..4748a8dd --- /dev/null +++ b/node-daemon/pvcd/powerdns-aggregator-schema.sql @@ -0,0 +1,92 @@ +PRAGMA foreign_keys = 1; + +CREATE TABLE domains ( + id INTEGER PRIMARY KEY, + name VARCHAR(255) NOT NULL COLLATE NOCASE, + master VARCHAR(128) DEFAULT NULL, + last_check INTEGER DEFAULT NULL, + type VARCHAR(6) NOT NULL, + notified_serial INTEGER DEFAULT NULL, + account VARCHAR(40) DEFAULT NULL +); + +CREATE UNIQUE INDEX name_index ON domains(name); + + +CREATE TABLE records ( + id INTEGER PRIMARY KEY, + domain_id INTEGER DEFAULT NULL, + name VARCHAR(255) DEFAULT NULL, + type VARCHAR(10) DEFAULT NULL, + content VARCHAR(65535) DEFAULT NULL, + ttl INTEGER DEFAULT NULL, + prio INTEGER DEFAULT NULL, + change_date INTEGER DEFAULT NULL, + disabled BOOLEAN DEFAULT 0, + ordername VARCHAR(255), + auth BOOL DEFAULT 1, + FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX rec_name_index ON records(name); +CREATE INDEX nametype_index ON records(name,type); +CREATE INDEX domain_id ON records(domain_id); +CREATE INDEX orderindex ON records(ordername); + + +CREATE TABLE supermasters ( + ip VARCHAR(64) NOT NULL, + nameserver VARCHAR(255) NOT NULL COLLATE NOCASE, + account VARCHAR(40) NOT NULL +); + +CREATE UNIQUE INDEX ip_nameserver_pk ON supermasters(ip, nameserver); + + +CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + domain_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL, + FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX comments_domain_id_index ON comments (domain_id); +CREATE INDEX comments_nametype_index ON comments (name, type); +CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); + + +CREATE TABLE domainmetadata ( + id INTEGER PRIMARY KEY, + domain_id INT NOT NULL, + kind VARCHAR(32) COLLATE NOCASE, + content TEXT, + FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX domainmetaidindex ON domainmetadata(domain_id); + + +CREATE TABLE cryptokeys ( + id INTEGER PRIMARY KEY, + domain_id INT NOT NULL, + flags INT NOT NULL, + active BOOL, + content TEXT, + FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX domainidindex ON cryptokeys(domain_id); + + +CREATE TABLE tsigkeys ( + id INTEGER PRIMARY KEY, + name VARCHAR(255) COLLATE NOCASE, + algorithm VARCHAR(50) COLLATE NOCASE, + secret VARCHAR(255) +); + +CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);