206 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
#!/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'].lower() for _x in pdnsDom[0]['records'])
 | 
						|
            if rrSet == set(record.lower() for record in 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()
 |