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.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')

View File

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

View File

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

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