dnsrev.py
#!/usr/bin/python
#
# dnsrev - Simple DNS PTR generator. Works with IPv4 and IPv6 addresses
# with different zonefile layouts.
#
# Copyright 2011-2014 Wilmer van der Gaast <wilmer@gaast.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
############################### DEPENDENCIES ###############################
# If it doesn't run properly, make sure you have the dnspython and ipaddr
# Python modules installed, and named-compilezones. On Debian systems, just
# apt-get install python-ipaddr python-dnspython bind9utils
import dns.reversename
import getopt
import ipaddr
import os
import re
import subprocess
import sys
import tempfile
import time
AUTO_SEP = ";; ---- dnsrev.py ---- automatically generated, do not edit ---- dnsrev.py ----"
def subnet_rev(full_addr):
"""Like dns.reversename.from_address but for subnets."""
addr, mask = full_addr.split("/")
full_label = str(dns.reversename.from_address(addr))
if ':' in addr:
rest = (128 - int(mask)) / 4
else:
rest = (32 - int(mask)) / 8
return full_label.split(".", rest)[-1]
def parse_zone(fn, zone):
"""Feed a zonefile through named-compilezone."""
p = subprocess.Popen(["/usr/sbin/named-compilezone", "-o", "-", zone, fn],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
zone, errors = p.communicate()
if p.returncode > 0:
print "While parsing %s:\n" % fn
print errors
sys.exit(1)
return zone.splitlines()
def dns_re(types):
"""Simple DNS zonefile line matcher."""
return re.compile(r"^([^\s]*\.)\s+(?:\d+\s+)?IN\s+(%s)\s+(.*)$" % "|".join(types))
def get_flag(flag, default=None):
"""Ugly getopt wrapper."""
flag = "-%s" % flag
flags = getopt.getopt(sys.argv[1:], "dhnsc:")[0]
res = [y for x, y in flags if x == flag]
if len(res) == 0:
if default is not None:
return default
else:
return False
elif res[0] == "":
return True
else:
return res[0]
def new_soa(old):
"""Create new SOA for today's date (or increment the old one if
otherwise the one would would be lower."""
tm = time.localtime()
new = (tm.tm_year * 1000000 +
tm.tm_mon * 10000 +
tm.tm_mday * 100)
if new > old:
return new
else:
# Just +1 if necessary.
return old + 1
# Using this more as a struct.
class ZoneFile(object):
def __init__(self, fn):
self.fn = fn
def mktemp(self):
dir, fn = os.path.split(self.fn)
return tempfile.NamedTemporaryFile(dir=dir, prefix=(fn + ".")).name
cfg = {}
try:
execfile(get_flag("c", "dnsrev.conf"), cfg)
except IOError:
pass
if not cfg or get_flag("h"):
print """\
dnsrev - Autogen/refresh reverse DNS zonefiles.
Set your forward and reverse zones. All zonefiles have to exist already,
this script does not (yet) create reverse zonefiles from scratch, it only
updates them.
-c [file] Configuration file location (default: ./dnsrev.conf).
-h This help info.
-n Dry run.
-d Show diffs of changes.
-s Do not update SOA serial number.
The configuration file should define two lists of tuples like this:
FWD_ZONES = [("db.example.net", "example.net"),
...]
REV_ZONES = [("db.example.net.rev4", "192.0.32.0/24"),
("db.example.net.rev6", "2620:0:2d0:200::/64"),
...]
The first column is the name of the zonefile. The second column is the
domain name in FWD_ZONES, and the ASCII-formatted subnet (including
netmask) in REV_ZONES.
You can list as many forward and reverse zones as you want. There doesn't
have to be any kind of 1:1 relationship between any of them."""
sys.exit(1)
# Convert all config data into zonefile "objects".
rev_files = []
for zone in cfg["REV_ZONES"]:
fn, sn = zone[0:2]
o = ZoneFile(fn)
o.sn = sn
o.sno = ipaddr.IPNetwork(sn)
if len(zone) > 2:
o.zone = zone[2]
else:
o.zone = subnet_rev(sn)
o.manual = {}
o.auto = {}
rev_files.append(o)
fwd_files = []
for fn, zone in cfg["FWD_ZONES"]:
o = ZoneFile(fn)
o.zone = zone
fwd_files.append(o)
# Get all manually-set reverse info (and don't autogen that part).
revre = dns_re(["PTR", "SOA"])
for f in rev_files:
cont = open(f.fn).read()
parts = cont.split(AUTO_SEP)
f.head = parts[0]
f.oldauto = None
if len(parts) > 1: # Better not be > 2 actually!
f.oldauto = parts[1].strip().splitlines()
fn_tmp = f.mktemp()
open(fn_tmp, "w").write(f.head)
for line in parse_zone(fn_tmp, f.zone):
m = revre.match(line)
if not m:
continue
mg = m.groups()
if mg[1] == "PTR":
label, _, name = m.groups()
f.manual[label] = name
else:
soa = mg[2].split(" ")
f.serial = int(soa[2])
os.unlink(fn_tmp)
# Get all forward zone info.
fwd = []
for f in fwd_files:
fwd += parse_zone(f.fn, f.zone)
addrs = []
addrre = dns_re(["A", "AAAA"])
for line in fwd:
m = addrre.match(line)
if not m:
continue
name, _, address = m.groups()
for f in rev_files:
if ipaddr.IPNetwork(address) in f.sno:
if f.sno.ip.version == 4 and f.sno.prefixlen > 24:
label = "%s.%s" % (address.split(".")[3], f.zone)
else:
label = str(dns.reversename.from_address(address))
if label in f.manual:
#print "Already manually created: %s" % address
pass # fuck you python
elif label not in f.auto:
f.auto[label] = name
else:
print "Duplicate entry, two names for %s" % address
# Generate the reverse files.
for f in rev_files:
if f.auto:
recs = []
for ad in sorted(f.auto.keys()):
recs.append("%-50s IN PTR %s" % (ad, f.auto[ad]))
if recs == f.oldauto:
print "No changes for %s" % f.fn
else:
serial = new_soa(f.serial)
print "Updating %s, new serial %d" % (f.fn, serial)
head = f.head.rstrip()
if not get_flag("s"):
serre = re.compile(r"\b(SOA\b.*?)\b%d\b" % f.serial, re.S)
head = serre.sub(r"\g<1>%d" % serial, head)
fn_tmp = f.mktemp()
o = file(fn_tmp, "w")
o.write(head)
o.write("\n\n%s\n\n%s\n" % (AUTO_SEP, "\n".join(recs)))
o.close()
if get_flag("d"):
p = subprocess.Popen(["/usr/bin/diff", "-u", f.fn, fn_tmp])
p.communicate()
if not get_flag("n"):
os.rename(fn_tmp, f.fn)
else:
os.unlink(fn_tmp)
else:
# Bug: If the file had some autogen data we won't delete it. Oh well.
print "No data for %s" % f.fn
pass
Generated by GNU Enscript 1.6.5.90.