generate a db.paivola.fi file
authorTero Marttila <terom@fixme.fi>
Thu, 02 Apr 2009 23:59:31 +0300
changeset 5 86b05c0ab5cd
parent 4 8b633782f02d
child 6 57e8168ba8c4
generate a db.paivola.fi file
bind.py
bind_conf.py
host.py
main.py
settings/hosts.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bind.py	Thu Apr 02 23:59:31 2009 +0300
@@ -0,0 +1,187 @@
+"""
+    High-level BIND stuff
+"""
+
+from __future__ import with_statement
+
+import bind_conf as bindc
+
+import os.path, datetime
+
+DEFAULT_TTL = bindc.Interval(3600)
+
+class Settings (object) :
+    """
+        A set of basic settings for a zone, mostly default TTL/refresh/retry/expire/minimum settings
+    """
+
+    def __init__ (self, ttl, hostmaster, refresh, retry, expire, minimum) :
+        self.ttl = ttl
+        self.hostmaster = hostmaster
+        self.refresh = refresh
+        self.retry = retry
+        self.expire = expire
+        self.minimum = minimum
+
+class AutoSerial (object) :
+    """
+        Automatically generate the next serial to use by loading it from a file.
+
+        The generated serials are in YYYYMMDDXX format.
+    """
+
+    def __init__ (self, path) :
+        """
+            Load the current serial 
+
+            @param path the path to the serial file
+        """
+        
+        # store
+        self.path = path
+            
+        # load it
+        # XXX: locking
+        serial = self.read()
+        
+        # current date
+        today = datetime.date.today()
+
+        # parse it
+        if serial :
+            date, code = self.parse(serial)
+
+        else :
+            date, code = today, 0
+        
+        # increment it
+        date, code = self.next(date, code)
+
+        # format it
+        self._serial = self.build(date, code)
+
+        # write it out
+        self.write(self._serial)
+
+    def parse (self, serial) :
+        """
+            Parse the given serial into a (datetime.date, code) format
+        """
+        
+        # build it into a date
+        date = datetime.date(
+                year    = int(serial[0:4]),
+                month   = int(serial[4:6]),
+                day     = int(serial[6:8])
+            )
+
+        code = int(serial[8:])
+
+        return date, code
+   
+    def next (self, date, code) :
+        """
+            Return the next valid serial following the given one
+        """
+        
+        # current date
+        today = datetime.date.today()
+
+        # now to increment?
+        if date < today :
+            # jump to today's first serial
+            date = today
+            code = 0
+        
+        else :
+            # today or overflowed into the future, just increment the code
+            code += 1
+        
+        # code overflowed into next day?
+        if code > 99 :
+            date += datetime.timedelta(days=1)
+            code = 0
+
+        # ok
+        return date, code
+    
+    def build (self, date, code) :
+        """
+            Build a serial code the given date/code
+        """
+
+        assert 0 <= code <= 99
+
+        return "%s%02d" % (date.strftime("%Y%m%d"), code)
+
+    def read (self) :
+        """
+            Read the current serial, returning it, or None, if not found...
+        """
+        
+        # if it doesn't exist, default
+        if not os.path.exists(self.path) :
+            return None
+        
+        # read it
+        with open(self.path) as fh :
+            return fh.read().strip()
+        
+    def write (self, serial) :
+        """
+            Write a new serial
+        """
+
+        with open(self.path, 'w') as fh :
+            fh.write("%s\n" % (serial, ))
+    
+    def serial (self) :
+        """
+            Return a new, unused serial code (before __init__)
+        """
+
+        return self._serial
+
+class Domain (bindc.ZoneFile) :
+    """
+        A domain has a skeleton of stuff defined, but the rest is $INCLUDE'd from elsewhere, which is useful for
+        multi-domain setups where the domains are mostly similar
+    """
+
+    def __init__ (self, domain, path, nameservers, mailservers, serial, settings, include=None, objs=None) :
+        """
+            @param domain the domain name
+            @param path the path to the zone file
+            @param nameservers list of nameservers as labels
+            @param mailservers list of (pref, label) tuples for MX records
+            @param serial the serial code to use
+            @param settings the TTL/SOA settings to use
+            @param include the optional zonefile to include
+            @param objs the optional other objects to add to the zonefile
+        """
+
+        super(Domain, self).__init__(domain, path)
+        
+        # the default TTL
+        self.add_directive(bindc.TTLDirective(settings.ttl))
+
+        # the SOA record
+        self.add_record(bindc.SOA(None, nameservers[0], 
+                settings.hostmaster, serial, settings.refresh, settings.retry, settings.expire, settings.minimum
+            ))
+
+        # the NS records
+        for label in nameservers :
+            self.add_record(bindc.NS(None, label))
+
+        # the MX records
+        for pref, label in mailservers :
+            self.add_record(bindc.MX(None, pref, label))
+        
+        # include?
+        if include :
+            self.add_directive(bindc.IncludeDirective(include))
+
+        if objs :
+            for obj in objs :
+                self.add_obj(obj)
--- a/bind_conf.py	Thu Apr 02 22:52:26 2009 +0300
+++ b/bind_conf.py	Thu Apr 02 23:59:31 2009 +0300
@@ -32,8 +32,8 @@
             @param comment optional comments
         """
         
-        Object.__init__(comment)
-        conf.File.__init__(name, path)
+        Object.__init__(self, comment)
+        conf.File.__init__(self, name, path)
 
         # init
         self.objects = []
@@ -51,6 +51,21 @@
     # various aliases...
     add_comment = add_record = add_directive = add_obj
 
+    def fmt_lines (self) :
+        """
+            Just format all our objects
+        """
+        
+        # prefix comments
+        for line in self._fmt_comments() :
+            yield line
+        
+        # and then all objects
+        for obj in self.objects :
+            # ...and their lines
+            for line in obj.fmt_lines() :
+                yield line
+    
 class Comment (Object) :
     """
         A comment, is, well, a comment :)
@@ -176,7 +191,7 @@
 
         # then format the line
         # XXX: TTL?
-        yield "%30s %4s%4s %8s %s" % (self.label if self.label is not None else '', str(self.ttl) if self.ttl else '', self.cls, self.type, self.rdata)
+        yield "%-30s %-4s%-4s %-8s %s" % (self.label if self.label is not None else '', str(self.ttl) if self.ttl else '', self.cls, self.type, self.rdata)
 
 class SOA (ResourceRecord) :
     """
--- a/host.py	Thu Apr 02 22:52:26 2009 +0300
+++ b/host.py	Thu Apr 02 23:59:31 2009 +0300
@@ -3,6 +3,7 @@
 """
 
 import dhcp
+import bind_conf as bindc
 
 class Interface (object) :
     """
@@ -50,3 +51,10 @@
             # build it
             yield dhcp.Host(name, iface.addr, self.address)
 
+    def build_bind_domain_records (self, origin) :
+        """
+            Build and yield one or more forward records (A/AAAA) for the host, with the given domain as the origin
+        """
+
+        yield bindc.A(self.hostname, self.address)
+
--- a/main.py	Thu Apr 02 22:52:26 2009 +0300
+++ b/main.py	Thu Apr 02 23:59:31 2009 +0300
@@ -1,6 +1,6 @@
 #!/usr/bin/env python2.5
 
-import data, dhcp_conf, dhcp
+import data, dhcp, bind_conf, bind
 
 import optparse, itertools
 
@@ -14,7 +14,9 @@
 
     # define our options
     parser = optparse.OptionParser(usage=usage)
-    parser.add_option('--dhcpd-conf',    dest='dhcpd_conf', metavar='PATH', help="path to dhcpd.conf", default='/etc/dhcp3/dhcpd.conf')
+    parser.add_option('--dhcpd-conf',   dest='dhcpd_conf',      metavar="PATH", help="path to dhcpd.conf", default='/etc/dhcp3/dhcpd.conf')
+    parser.add_option('--bind-zone',    dest='bind_zone',       metavar="PATH", help="path to bind zone file", default=None)
+    parser.add_option('--autoserial',   dest='autoserial_path', metavar="PATH", help="path to autoserial file", default='autoserial')
     
     # parse them
     options, args = parser.parse_args(args=argv[1:])
@@ -30,7 +32,7 @@
         Write the DHCP config module using the data loaded from the given module
     """
     
-    # build the config f file
+    # build the config file
     config = dhcp.Config(path=options.dhcpd_conf,
         settings        = settings.dhcp_settings,
         options         = settings.dhcp_options,
@@ -42,6 +44,28 @@
     # write it out
     config.write()
 
+def write_bind (options, settings) :
+    """
+        Write a BIND config for a forward zone
+    """
+
+    assert options.bind_zone
+
+    # load the serial
+    autoserial = bind.AutoSerial(options.autoserial_path)
+
+    # build the zone file
+    zone = bind.Domain(domain=settings.domain, path=options.bind_zone,
+            nameservers     = settings.nameservers,
+            mailservers     = [((i+1)*10, label) for i, label in enumerate(settings.mailservers)],
+            serial          = autoserial.serial(),
+            settings        = settings.bind_settings,
+            objs            = itertools.chain(*[host.build_bind_domain_records(settings.domain) for host in settings.hosts]),
+    )
+
+    # write it out
+    zone.write()
+
 def main (argv) :
     """
         Our app entry point, parse args, load data, write out the config files
@@ -55,6 +79,7 @@
     
     # write out the config files
     write_dhcp(options, data_module)
+    write_bind(options, data_module)
 
 if __name__ == '__main__' :
     from sys import argv
--- a/settings/hosts.py	Thu Apr 02 22:52:26 2009 +0300
+++ b/settings/hosts.py	Thu Apr 02 23:59:31 2009 +0300
@@ -1,14 +1,40 @@
 from addr import IP, Network
 from host import Interface, Host
 from dhcp import Subnet
+from bind import Settings as BindSettings
+from bind_conf import Interval
 
-dhcp_settings = {
+# BIND stuff
+domain          = "paivola.fi"
+
+nameservers     = [
+        "ranssi.paivola.fi",
+        "misc1.idler.fi",
+        "misc2.idler.fi",
+        "srv.marttila.de",
+    ]
+
+mailservers     = [
+        "mail.paivola.fi",
+    ]
+
+bind_settings   = BindSettings(
+        ttl         = 3601,
+        hostmaster  = "hostmaster",
+        refresh     = Interval(h=1),
+        retry       = Interval(m=3),
+        expire      = Interval(d=28),
+        minimum     = Interval(60)
+    )
+
+# DHCP stuff
+dhcp_settings   = {
     'default-lease-time':   43200,
     'max-lease-time':       86400,
     'authorative':          None,
 }
 
-dhcp_options = {
+dhcp_options    = {
     'domain-name-servers':  IP('194.197.235.145'),
 }
 
@@ -18,6 +44,7 @@
     Subnet(Network('192.168.0.0/23'),   router_idx=1, unknown_clients='deny', comment="Internal network"),
 ]
 
+# general stuff
 hosts           = [
     Host('jumpgate',    IP('194.197.235.1'),    [ ]),
     Host('mikk4',       IP('194.197.235.72'),   [