]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
added script for importing zone data from PowerDNS-like SQL
authorLibor Peltan <libor.peltan@nic.cz>
Wed, 14 Nov 2018 11:16:56 +0000 (12:16 +0100)
committerDaniel Salzman <daniel.salzman@nic.cz>
Tue, 22 Jan 2019 14:22:19 +0000 (15:22 +0100)
scripts/dns_sql2zf.py [new file with mode: 0755]

diff --git a/scripts/dns_sql2zf.py b/scripts/dns_sql2zf.py
new file mode 100755 (executable)
index 0000000..3c408d1
--- /dev/null
@@ -0,0 +1,243 @@
+#!/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()
+