Add DNS aggregator via PowerDNS and sqlite3

This commit is contained in:
Joshua Boniface 2018-10-15 21:09:40 -04:00
parent a2a7a1d790
commit c13a4e84af
5 changed files with 304 additions and 7 deletions

View File

@ -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 <joshua@boniface.me>
#
# 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 <https://www.gnu.org/licenses/>.
#
###############################################################################
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')

View File

@ -44,6 +44,7 @@ import pvcd.common as common
import pvcd.DomainInstance as DomainInstance import pvcd.DomainInstance as DomainInstance
import pvcd.NodeInstance as NodeInstance import pvcd.NodeInstance as NodeInstance
import pvcd.VXNetworkInstance as VXNetworkInstance import pvcd.VXNetworkInstance as VXNetworkInstance
import pvcd.DNSAggregatorInstance as DNSAggregatorInstance
############################################################################### ###############################################################################
# PVCD - node daemon startup program # PVCD - node daemon startup program
@ -506,6 +507,12 @@ node_list = []
network_list = [] network_list = []
domain_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 # Node objects
@zk_conn.ChildrenWatch('/nodes') @zk_conn.ChildrenWatch('/nodes')
def update_nodes(new_node_list): def update_nodes(new_node_list):
@ -514,7 +521,7 @@ def update_nodes(new_node_list):
# Add any missing nodes to the list # Add any missing nodes to the list
for node in new_node_list: for node in new_node_list:
if not node in 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 # Remove any deleted nodes from the list
for node in node_list: for node in node_list:
@ -542,6 +549,7 @@ def update_networks(new_network_list):
for network in new_network_list: for network in new_network_list:
if not network in network_list: if not network in network_list:
d_network[network] = VXNetworkInstance.VXNetworkInstance(network, zk_conn, config, logger, this_node) d_network[network] = VXNetworkInstance.VXNetworkInstance(network, zk_conn, config, logger, this_node)
dns_aggregator.add_client_network(network)
# Start primary functionality # Start primary functionality
if this_node.router_state == 'primary': if this_node.router_state == 'primary':
d_network[network].createGatewayAddress() d_network[network].createGatewayAddress()
@ -557,9 +565,14 @@ def update_networks(new_network_list):
# Stop general functionality # Stop general functionality
d_network[network].removeFirewall() d_network[network].removeFirewall()
d_network[network].removeNetwork() d_network[network].removeNetwork()
dns_aggregator.remove_client_network(network)
# Delete the object # Delete the object
del(d_network[network]) 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 # Update and print new list
network_list = new_network_list network_list = new_network_list
logger.out('{}Network list:{} {}'.format(logger.fmt_blue, logger.fmt_end, ' '.join(network_list)), state='i') logger.out('{}Network list:{} {}'.format(logger.fmt_blue, logger.fmt_end, ' '.join(network_list)), state='i')

View File

@ -35,7 +35,7 @@ import pvcd.common as common
class NodeInstance(object): class NodeInstance(object):
# Initialization function # 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 # Passed-in variables on creation
self.name = name self.name = name
self.this_node = this_node self.this_node = this_node
@ -55,6 +55,7 @@ class NodeInstance(object):
self.d_node = d_node self.d_node = d_node
self.d_network = d_network self.d_network = d_network
self.d_domain = d_domain self.d_domain = d_domain
self.dns_aggregator = dns_aggregator
# Printable lists # Printable lists
self.active_node_list = [] self.active_node_list = []
self.flushed_node_list = [] self.flushed_node_list = []
@ -289,17 +290,25 @@ class NodeInstance(object):
def become_secondary(self): def become_secondary(self):
self.logger.out('Setting router {} to secondary state'.format(self.name), state='i') self.logger.out('Setting router {} to secondary state'.format(self.name), state='i')
self.logger.out('Network list: {}'.format(', '.join(self.network_list))) self.logger.out('Network list: {}'.format(', '.join(self.network_list)))
time.sleep(0.5) time.sleep(1)
for network in self.d_network: for network in self.d_network:
self.d_network[network].stopDHCPServer() self.d_network[network].stopDHCPServer()
self.d_network[network].removeGatewayAddress() self.d_network[network].removeGatewayAddress()
self.dns_aggregator.stop_aggregator()
def become_primary(self): def become_primary(self):
self.logger.out('Setting router {} to primary state.'.format(self.name), state='i') self.logger.out('Setting router {} to primary state.'.format(self.name), state='i')
self.logger.out('Network list: {}'.format(', '.join(self.network_list))) 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: for network in self.d_network:
self.d_network[network].createGatewayAddress() self.d_network[network].createGatewayAddress()
self.d_network[network].startDHCPServer() 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 # Flush all VMs on the host
def flush(self): def flush(self):

View File

@ -324,7 +324,6 @@ add rule inet filter input meta iifname {bridgenic} counter drop
dhcp_configuration = [ dhcp_configuration = [
'--domain-needed', '--domain-needed',
'--bogus-priv', '--bogus-priv',
# '--no-resolv',
'--no-hosts', '--no-hosts',
'--filterwin2k', '--filterwin2k',
'--expand-hosts', '--expand-hosts',
@ -338,10 +337,10 @@ add rule inet filter input meta iifname {bridgenic} counter drop
'--bind-interfaces', '--bind-interfaces',
'--leasefile-ro', '--leasefile-ro',
'--dhcp-script=/usr/share/pvc/pvcd/dnsmasq-zookeeper-leases.py', '--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), '--dhcp-hostsdir={}'.format(self.dnsmasq_hostsdir),
'--log-facility=-',
'--log-queries=extra', '--log-queries=extra',
'--log-facility={}/dnsmasq.log'.format(self.config['dnsmasq_log_directory']),
'--keep-in-foreground' '--keep-in-foreground'
] ]
# Start the dnsmasq process in a thread # 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( '/usr/sbin/dnsmasq {}'.format(
' '.join(dhcp_configuration) ' '.join(dhcp_configuration)
), ),
environment=dhcp_environment environment=dhcp_environment,
logfile='{}/dnsmasq-{}.log'.format(self.config['dnsmasq_log_directory'], self.vni)
) )
def removeNetwork(self): def removeNetwork(self):

View File

@ -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);