Commit e26f6fc7 authored by Libor Peltan's avatar Libor Peltan Committed by Daniel Salzman

added script for importing zone data from PowerDNS-like SQL

parent 0af4d4ca
#!/usr/bin/python3
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
# 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/>.
# requirements:
# sudo apt install libmysqlclient-dev
# pip3 install sqlobject configparser mysqlclient argparse
from sqlobject import *
import configparser
import argparse
import os
import string
import sys
import time
import re
from subprocess import DEVNULL, PIPE, Popen
# globals
soa_serial = int(time.time())
config = configparser.ConfigParser()
fix_absolute = False
storage = os.getcwd()
knotc_binary = "knotc"
knotc_socket = None
class Domains(SQLObject):
# id = IntCol() # implicitly there
name = StringCol()
master = StringCol()
last_check = StringCol()
type = StringCol()
notified_serial = StringCol()
account = StringCol()
class Records(SQLObject):
# id = IntCol() # implicitly there
domain = ForeignKey('Domains')
name = StringCol()
type = StringCol()
content = StringCol()
ttl = IntCol()
prio = IntCol()
change_date = StringCol()
ordername = StringCol()
auth = StringCol()
class Changes(SQLObject):
# id = IntCol() # implicitly there
domain = ForeignKey('Domains')
type = IntCol() # -1 .. zone removed; 0 .. zone modified; 1 .. zone added
def remove_dot(s):
return s[:-1] if s[-1] == '.' else s
def fix_abs(name):
return remove_dot(name) + '.' if fix_absolute else name
def domain_get_records(domain):
if str(domain).isdigit():
return Records.select(Records.q.domain == domain)
else:
dn = remove_dot(domain)
return Records.select(AND(Domains.q.id == Records.q.domain, Domains.q.name == dn))
def domain_id2name(domain):
return Domains.select(Domains.q.id == domain)[0].name
def get_config(key, default_val):
global config
return int(config['DEFAULT'][key]) if key in config['DEFAULT'] and config['DEFAULT'][key] is not None else default_val
def get_soa_params():
refresh = get_config("soa-refresh-default", 10800)
retry = get_config("soa-retry-default", 3600)
expire = get_config("soa-expire-default", 604800)
minttl = get_config("soa-minimum-ttl", 3600)
return (refresh, retry, expire, minttl)
def soa_content(db_content):
global soa_serial
(nameserver, contact, fake_serial) = db_content.split()
ns = fix_abs(nameserver)
co = fix_abs(contact.replace("@", "."))
(refresh, retry, expire, minttl) = get_soa_params()
return ("%s %s %d %d %d %d %d" % (ns, co, soa_serial, refresh, retry, expire, minttl))
def zone_storage(zone):
global storage
return os.path.join(storage, "%s.zone" % remove_dot(zone))
def knotc_single(*args):
global knotc_binary
global knotc_socket
cmd = [ knotc_binary, "-s", knotc_socket ] + list(args)
p = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
(stdout, stderr) = p.communicate()
if p.returncode != 0:
raise Exception("error: knotc %s failed: '%s'" % (str(args), stderr))
def knotc_send(type, zone):
if type == 0:
knotc_single("zone-reload", zone)
else:
try:
knotc_single("conf-begin")
if type > 0:
knotc_single("conf-set", "zone[%s]" % zone)
knotc_single("conf-set", "zone[%s].file" % zone, zone_storage(zone))
else:
knotc_single("conf-unset", "zone[%s]" % zone)
knotc_single("conf-commit")
except:
knotc_single("conf-abort")
raise
def print_record(record, outfile):
t = record.type.upper()
if t == 'SOA':
content = soa_content(record.content)
elif t == 'MX' or t == 'SRV':
content = "%d %s" % (record.prio, record.content)
else:
content = record.content
if t in ('NS', 'MX', 'CNAME', 'DNAME', 'SRV', 'PTR'):
content = fix_abs(content)
record = ("%s. %d %s %s\n" % (record.name, record.ttl, t, content))
outfile.write(record)
def print_domain(domain, change_type = 0):
global knotc_socket
dn = domain_id2name(domain) if str(domain).isdigit() else domain
f = open(zone_storage(dn), "w")
for r in domain_get_records(domain):
print_record(r, f)
f.close()
if knotc_socket is not None:
knotc_send(change_type, dn)
print("Updated zone %s" % dn, file=sys.stderr)
def domain_from_change(change):
global knotc_socket
if change.type >= 0:
print_domain(change.domain.name, change.type)
else:
dn = change.domain.name
try:
os.remove(zone_storage(dn))
except:
print("Warning: failed to delete zonefile for %s" % dn)
if knotc_socket is not None:
knotc_send(change.type, dn)
else:
print("Warning: removed zone '%s', but unspecified knotc socket." % dn, file=sys.stderr)
def process_changes(startwith):
processed = []
try:
for ch in Changes.select(Changes.q.id > startwith):
domain_from_change(ch)
processed.append(ch.id)
finally:
if len(processed) > 0:
print("Processed up to change_id %d" % processed[-1], file=sys.stderr)
# TODO delete processed ?
def read_config_file(filename):
global config
with open(filename, 'r') as f:
fcontent = '[DEFAULT]\n' + f.read()
config.read_string(fcontent)
def main():
global storage
global knotc_socket
global soa_serial
global fix_absolute
argp = argparse.ArgumentParser(prog='dns_sql2zf', description="Export DNS records from Mysql or Postgres DB into zonefile.", epilog="(C) CZ.NIC, GPLv3") # TODO better epilog
argp.add_argument(dest='domains', metavar='zone', nargs='*', help='Zone to be exported.')
argp.add_argument('--db', dest='dburi', metavar='DB_URI', nargs=1, required=True, help='URI of database to export from (example: mysql://user:password@127.0.0.1/powerdns_db).')
argp.add_argument('--storage', dest='storage', metavar='path', nargs=1, help='Storage for the generated zonefile (otherwise current dir).')
argp.add_argument('--all', dest='all', action='store_true', help="Export all zones.")
argp.add_argument('--confile', dest='confile', metavar='file', nargs=1, help='PowerDNS configfile to obtain SOA parameters (otherwise defaults).')
argp.add_argument('--serial', dest='soa_serial', type=int, metavar='uint32', nargs=1, help='SOA serial number (otherwise UNIX timestamp).')
argp.add_argument('--absolute-names', dest='fix_absolute', action='store_true', help="Interpret names in records' contents (e.g. CNAME, NS...) as absolute even if w/o trailing dot.")
argp.add_argument('--from-changes', dest='from_changes', metavar="from_id", nargs='?', const=[0], help="Export zones listed in extra 'changes' table.")
argp.add_argument('--knotc', dest='knotc_socket', metavar='knot_socket', nargs=1, help="Notify Knot DNS about changes (requires: $PATH/knotc).")
argp.add_argument('--version', action='version', version='dns_sql2zf 0.1')
args = argp.parse_args()
if args.soa_serial is not None:
soa_serial = args.soa_serial[0]
if args.confile is not None:
read_config_file(args.confile[0])
if args.storage is not None:
storage = args.storage[0]
if args.fix_absolute:
fix_absolute = True
if args.knotc_socket is not None:
knotc_socket = args.knotc_socket[0]
connection = connectionForURI(args.dburi[0])
sqlhub.processConnection = connection
for domain in args.domains:
print_domain(domain)
if args.all:
for d in Domains.select():
print_domain(d.id)
if args.from_changes is not None:
process_changes(args.from_changes[0])
if __name__ == "__main__":
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment