#!/usr/bin/env python3 # -*- coding: utf-8 -*- # This file is part of dnmgmt, a number resource management system # Licensed under GNU General Public License v3 or later # Written by Sebastian Lohff (seba@someserver.de) import argparse import configparser import os import sys import json import requests import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnmgmt.settings") BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) django.setup() from django.db.models import Count from domains.models import Domain, ReverseZone __VERSION__ = '0.1' def _parser(): parser = argparse.ArgumentParser() parser.add_argument("--pdns-host", default="127.0.0.1", help="PDNS host") parser.add_argument("--pdns-port", default=8081, help="PDNS port") parser.add_argument("-c", "--config", default=None, type=argparse.FileType("r"), help="Path to config file (default path: ./dns-sync.conf, /etc/dns-sync.conf)") parser.add_argument("--api-key", default=None, help="PDNS API key") parser.add_argument("--version", action="version", version="%(prog)s " + __VERSION__) return parser def mergeDomains(zoneData, pdnsData): rrAdd = [] rrData = pdnsData['rrsets'] for domain, rrType, records in zoneData: found = False pdnsDom = list(filter(lambda _x: _x['name'] == domain and _x['type'] == rrType, rrData)) if len(pdnsDom) > 0: rrSet = set(_x['content'] for _x in pdnsDom[0]['records']) if rrSet == set(records): found = True if not found: # new domain! rrAdd.append({ "name": domain, "type": rrType, "ttl": 60*60, "changetype": "REPLACE", "records": [{"content": record, "disabled": False} for record in records], }) return rrAdd def removeOldDomains(zoneData, pdnsData): rrDel = [] #print("zone data", zoneData) #print("pdnsData", pdnsData) for entry in pdnsData['rrsets']: # search for name/type in domain dict. if non-existtant mark for deletion # this could be much more efficient with a dict! name: [rrset...] if not any(entry['name'] == _x[0] and entry['type'] == _x[1] for _x in zoneData): rrDel.append({ "changetype": "DELETE", "name": entry["name"], "type": entry["type"], }) return rrDel def handleNameserver(ns, servers, usedNameservers, domains): servers.append(ns.name) if ns.name not in usedNameservers and (ns.glueIPv4 or ns.glueIPv6): usedNameservers.append(ns.name) if ns.glueIPv4: domains.append((ns.name, "A", [ns.glueIPv4])) if ns.glueIPv6: domains.append((ns.name, "AAAA", [ns.glueIPv6])) def getDomainsFromQueryset(qs, zone, glueRecords, usedNameservers, v4reverse=False): for domain in qs: servers = [] for ns in domain.nameservers.all(): servers.append(ns.name) if ns.name not in usedNameservers and (ns.glueIPv4 or ns.glueIPv6): usedNameservers.append(ns.name) if ns.glueIPv4: glueRecords.append((ns.name, "A", [ns.glueIPv4])) if ns.glueIPv6: glueRecords.append((ns.name, "AAAA", [ns.glueIPv6])) zone.append((domain.getZone(), "NS", servers)) if domain.ds_records: zone.append((domain.getZone(), "DS", domain.ds_records.split("\n"))) if v4reverse: # for ipv4 reverse we have to do some extra work in case of classless delegations # see RFC2317 net = domain.parentNet.getNetwork() if net.prefixlen % 8 != 0: revZone = domain.getZone() parts = str(net.network_address).split(".") baseZone = ".".join(reversed(parts[:net.prefixlen // 8])) + ".in-addr.arpa." startNo = int(parts[net.prefixlen // 8]) lenExp = 8 - (net.prefixlen % 8) for i in range(2 ** lenExp): no = startNo + i zone.append(("%d.%s" % (no, baseZone), "CNAME", ["%d.%s" % (no, revZone)])) def mergeDomainsWithPdns(s, args, zone, zoneData, protectedRecords=[]): url = "http://%s:%s/api/v1/servers/localhost/zones/%s" % (args.pdns_host, args.pdns_port, zone,) pdnsData = s.get(url).json() baseProtectedRecords = [ (zone, "NS", []), (zone, "SOA", []), ] # add dn. (NS + glue Nameservers) newDomains = mergeDomains(zoneData, pdnsData) print("Add/replace", newDomains) if len(newDomains) > 0: r = s.patch(url, data=json.dumps({'rrsets': newDomains})) if r.status_code != 204: raise RuntimeError("Could not update records in powerdns, API returned %d" % r.status_code) delDomains = removeOldDomains(zoneData + protectedRecords + baseProtectedRecords, pdnsData) print("Del", delDomains) r = s.patch(url, data=json.dumps({'rrsets': delDomains})) if r.status_code != 204: raise RuntimeError("Could not update records in powerdns, API returned %d" % r.status_code) def main(): parser = _parser() args = parser.parse_args() config = configparser.ConfigParser() if args.config: config.read_file(args.config) else: config.read([os.path.join(os.path.abspath(__file__), "dns-sync.conf"), "/etc/dns-sync.conf"]) #print(config) #print(config.get("DEFAULT", "api-key")) #print(config.has_section("DEFAULT"), config.has_option("DEFAULT", "api-key")) if args.api_key: config["DEFAULT"]["api-key"] = args.api_key if not config.has_option("DEFAULT", "api-key"): print("Error: Could not find api-key (not present in config under [DEFAULT]; not given via command line)", file=sys.stderr) sys.exit(2) s = requests.Session() s.headers = { 'X-API-Key': config.get("DEFAULT", "api-key"), 'Accept': 'application/json' } domains = [] rzone4 = [] rzone6 = [] usedNameservers = [] # assenble domain data # dn. qs = Domain.objects.annotate(nsCount=Count('nameservers')).filter(nsCount__gt=0).order_by("name") getDomainsFromQueryset(qs, domains, domains, usedNameservers) # reverse zones qs = ReverseZone.objects.annotate(nsCount=Count('nameservers')).filter(nsCount__gt=0).order_by("parentNet__address") qs4 = filter(lambda _revz: _revz.getNetwork().version == 4, qs) qs6 = filter(lambda _revz: _revz.getNetwork().version == 6, qs) getDomainsFromQueryset(qs4, rzone4, domains, usedNameservers, v4reverse=True) getDomainsFromQueryset(qs6, rzone6, domains, usedNameservers) #print("dn.", domains) #print("v4", rzone4) #print("v6", rzone6) mergeDomainsWithPdns(s, args, "dn.", domains) mergeDomainsWithPdns(s, args, "10.in-addr.arpa.", rzone4) mergeDomainsWithPdns(s, args, "3.2.7.c.3.a.d.f.ip6.arpa.", rzone6) if __name__ == '__main__': main()