Add DNS aggregator via PowerDNS and sqlite3
This commit is contained in:
parent
a2a7a1d790
commit
c13a4e84af
|
@ -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')
|
|
@ -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')
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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);
|
Loading…
Reference in New Issue