restructure, break
authorTero Marttila <terom@fixme.fi>
Sun, 12 Jul 2009 00:51:08 +0300
changeset 7 0f9cae2d7147
parent 6 57e8168ba8c4
child 8 46d36bc33086
restructure, break
__init__.py
addr.py
bind.py
bind_conf.py
conf.py
data.py
dhcp.py
dhcp_conf.py
host.py
in/__init__.py
in/hosts.py
main.py
pvl-config
pvl/__init__.py
pvl/config/__init__.py
pvl/config/addr.py
pvl/config/bind.py
pvl/config/bind_conf.py
pvl/config/conf.py
pvl/config/data.py
pvl/config/dhcp.py
pvl/config/dhcp_conf.py
pvl/config/host.py
settings/__init__.py
settings/hosts.py
test/bind.py
test/conf.py
test/dhcp.py
test_bind.py
test_conf.py
test_dhcp.py
--- a/addr.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-"""
-    Used to define IP-address/subnet stuff
-"""
-
-import IPy
-
-class IP (IPy.IP, object) :
-    """
-        A literal IPv4 address
-    """
-    
-    def __init__ (self, address) :
-        """
-            Parse the given literal IP address in "a.b.c.d" form
-        """
-    
-        super(IP, self).__init__(address)
-    
-    def is_v4 (self) :
-        """
-            Returns True if the address is an IPv4 address
-        """
-
-        return self.version() == 4
-
-    def is_v6 (self) :
-        """
-            Returns True if the address is an IPv6 address
-        """
-
-        return self.version() == 6
-
-class Network (IPy.IP, object) :
-    """
-        An IPv4 network (subnet)
-    """
-
-    def __init__ (self, prefix) :
-        """
-            Parse the given prefix in "a.b.c.d/l" form
-        """
-
-        super(Network, self).__init__(prefix)
-
-class MAC (object) :
-    """
-        A mac address
-    """
-
-    def __init__ (self, mac) :
-        """
-            Parse the given MAC address in "aa:bb:cc:dd:ee:ff" form
-        """
-
-        # XXX: validate
-        self.mac = mac
-
-    def __str__ (self) :
-        return self.mac
-
--- a/bind.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,187 +0,0 @@
-"""
-    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	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,395 +0,0 @@
-"""
-    Configuration file output for the ISC DNS server: BIND
-"""
-
-import conf
-
-class Object (conf.ConfObject) :
-    """
-        Our own version of ConfObject that knows how to format comments
-    """
-
-    def _fmt_comments (self) :
-        """
-            Format comments using the standard format:
-                ";" <comment>
-        """
-
-        for comment in self.comments :
-            if comment is not None :
-                yield "; %s" % (comment, )
-
-class ZoneFile (Object, conf.File) :
-    """
-        A zone file containing a bunch of directives, resource records and comments
-    """
-
-    def __init__ (self, name, path, ttl=None, comment=None) :
-        """
-            @param name the name of the zonefile, for status stuff
-            @param path the path to the zonefile
-            @param ttl default TTL to use
-            @param comment optional comments
-        """
-        
-        Object.__init__(self, comment)
-        conf.File.__init__(self, name, path)
-
-        # init
-        self.objects = []
-
-        if ttl :
-            self.add_directive(TTLDirective(ttl))
-
-    def add_obj (self, obj) :
-        """
-            Add an Object onto the list of things to include in this zone file
-        """
-
-        self.objects.append(obj)
-
-    # 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 :)
-    """
-
-    def __init__ (self, comment) :
-        """
-            @param comment the comment string
-        """
-
-        Object.__init__(self, [comment])
-    
-    def fmt_lines (self) :
-        """
-            Yield a single line with the comment
-        """
-
-        return self._fmt_comments()
-
-class Label (object) :
-    """
-        A label, as used in a ResourceRecord, either as the label, or the rdata for various resource types
-
-        You can also use strs, this just implements a __str__ method
-    """
-
-    def __init__ (self, label) :
-        """
-            @param label the literal label to use
-        """
-
-        self.label = label
-
-    def __str__ (self) :
-        return self.label
-
-class Origin (Label) :
-    """
-        A label that represents the zone's origin
-    """
-
-    def __init__ (self) :
-        pass
-
-    def __str__ (self) :
-        return '@'
-
-class FQDN (Label) :
-    """
-        A label that represents the given external domain (i.e. this adds the . to the end that people always forget).
-    """
-
-    def __init__ (self, fqdn) :
-        self.fqdn = fqdn
-
-    def __str__ (self) :
-        return "%s." % (self.fqdn, )
-
-class Interval (object) :
-    """
-        A time interval suitable for use in SOA records
-    """
-
-    def __init__ (self, s=None, m=None, h=None, d=None) :
-        """
-            @param s seconds
-            @param m minutes
-            @param h hours
-            @param d days
-        """
-
-        self.s = s
-        self.m = m
-        self.h = h
-        self.d = d
-
-    def __str__ (self) :
-        """
-            If only seconds were given, just return those directly, otherwise, apply units
-        """
-
-        if self.s and not self.m and not self.h and not self.d :
-            return str(self.s)
-
-        else :
-            return "%s%s%s%s" % (
-                    "%ds" % self.s if self.s else '',
-                    "%dm" % self.m if self.m else '',
-                    "%dh" % self.h if self.h else '',
-                    "%dd" % self.d if self.d else ''
-                )
-
-class ResourceRecord (Object) :
-    """
-        A generic resource record for a BIND zone file
-    """
-
-    def __init__ (self, label, type, rdata, cls='IN', ttl=None, **kwargs) :
-        """
-            @param label the "name" of this record, or None to referr to the previous record's label
-            @param type the type as a string ('A', 'TXT', etc.)
-            @param rdata the rdata, as a raw string
-            @param cls the class, e.g. 'IN'
-            @param ttl the time-to-live value in seconds, or None to omit it
-        """
-
-        super(ResourceRecord, self).__init__(**kwargs)
-
-        self.label = label
-        self.type = type
-        self.rdata = rdata
-        self.cls = cls
-        self.ttl = ttl
-
-    def fmt_lines (self) :
-        """
-            Just format the lines, eh
-        """
-        
-        # prefix comments
-        for line in self._fmt_comments() :
-            yield line
-
-        # 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)
-
-class SOA (ResourceRecord) :
-    """
-        "Identifies the start of a zone of authority", must be the first record
-    """
-
-    def __init__ (self, label, primary_ns, hostmaster, serial, refresh, retry, expire, minimum, **kwargs) :
-        """
-            @param label the "name" of the zone, usually ORIGIN
-            @param primary_ns the address of the primary NS server
-            @param hostmaster the mailbox of the zone's hostmaster
-            @param serial the serial number of the zone as an integer
-            @param refresh time interval between zone refreshes in seconds
-            @param retry time interval between retrys for failed refreshes
-            @param expire time interval before zone data can no longer be considered authorative
-            @param minimum minimum TTL for RRs in this zone
-        """
-
-        super(SOA, self).__init__(label, 'SOA', "%s %s ( %s %s %s %s %s )" % (
-                primary_ns, hostmaster, serial, refresh, retry, expire, minimum
-            ), **kwargs)
-
-class A (ResourceRecord) :
-    """
-        An IPv4 forward address
-    """
-
-    def __init__ (self, label, addr, **kwargs) :
-        """
-            @param label the "name" of the address
-            @param addr the IPv4 target address
-        """
-
-        assert(addr.is_v4())
-
-        super(A, self).__init__(label, 'A', addr, **kwargs)
-
-class AAAA (ResourceRecord) :
-    """
-        An IPv6 forward address
-    """
-
-    def __init__ (self, label, addr, **kwargs) :
-        """
-            @param label the "name" of the address
-            @param addr the IPv6 target address
-        """
-        
-        assert(addr.is_v6())
-
-        super(AAAA, self).__init__(label, 'AAAA', addr.strCompressed(), **kwargs)
-
-class CNAME (ResourceRecord) :
-    """
-        A canonical-name alias
-    """
-
-    def __init__ (self, label, target, **kwargs) :
-        """
-            @param label the "name" of the alias
-            @param target the alias target
-        """
-
-        super(CNAME, self).__init__(label, 'CNAME', target, **kwargs)
-
-class TXT (ResourceRecord) :
-    """
-        A human-readable information record
-    """
-
-    def __init__ (self, label, text, **kwargs) :
-        """
-            @param label the "name" of the text record
-            @param text the text data, shouldn't contain any quotes...
-        """
-
-        super(TXT, self).__init__(label, 'TXT', '"%s"' % text, **kwargs)
-
-class MX (ResourceRecord) :
-    """
-        A mail-exchange definition
-    """
-
-    def __init__ (self, label, pref, exchange, **kwargs) :
-        """
-            @param label the "name" of the domain to handle mail for
-            @param pref the numerical preference for this exchange
-            @param exchange the domain name of the mail exchange (SMTP server)
-        """
-
-        super(MX, self).__init__(label, 'MX', "%d %s" % (pref, exchange), **kwargs)
-
-class NS (ResourceRecord) :
-    """
-        An authorative name server
-    """
-
-    def __init__ (self, label, nsname, **kwargs) :
-        """
-            @param label the "name" of the domain to have a nameserver for
-            @param nsname the name of the nameserver
-        """
-
-        super(NS, self).__init__(label, 'NS', nsname)
-
-class PTR (ResourceRecord) :
-    """
-        An IPv4/IPv6 reverse address
-    """
-
-    def __init__ (self, addr, name, **kwargs) :
-        """
-            @param addr the addr.IP to map via in-addr.arpa
-            @param name the name to map the address to
-        """
-
-        # XXX: quick hack, this gives an absolute name
-        label = addr.reverseName()
-        
-        super(PTR, self).__init__(label, 'PTR', name)
-
-class Directive (Object) :
-    """
-        Special directives that can be used in zone files to control behaviour
-    """
-
-    def __init__ (self, name, *args, **kwargs) :
-        """
-            @param name the $NAME bit
-            @param args optional list of space-seprated arguments, Nones are ignored
-        """
-
-        super(Directive, self).__init__(**kwargs)
-
-        self.name = name
-        self.args = [arg for arg in args if arg is not None]
-
-    def fmt_lines (self) :
-        # prefix comments
-        for line in self._fmt_comments() :
-            yield line
-
-        # then format it
-        yield "$%s%s" % (self.name, (' ' + ' '.join(str(arg) for arg in self.args)) if self.args else '')
-
-class OriginDirective (Directive) :
-    """
-        Set the origin used to resolve the zone's labels.
-
-        Note that the origin label is not absolute by default - use FQDN for that
-    """
-
-    def __init__ (self, origin, **kwargs) :
-        """
-            @param origin the origin label
-        """
-
-        super(OriginDirective, self).__init__('ORIGIN', origin, **kwargs)
-
-class TTLDirective (Directive) :
-    """
-        Set the TTL used for records by default
-    """
-
-    def __init__ (self, ttl, **kwargs) :
-        """
-            @param ttl the new ttl to use
-        """
-
-        super(TTLDirective, self).__init__('TTL', ttl, **kwargs)
-
-class IncludeDirective (Directive) :
-    """
-        Include another zoen file, optionally with a different origin
-    """
-
-    def __init__ (self, filename, origin=None, **kwargs) :
-        """
-            @param filename the zone file to include
-            @param origin the optional origin to process the zonefile with
-        """
-
-        super(IncludeDirective, self).__init__('INCLUDE', filename, origin, **kwargs)
-
-class GenerateDirective (Directive) :
-    """
-        Generate a bunch of numbered records using an expression for the label and rdata.
-
-        At the simplest, any "$" in the expression is replaced with the value of the iterator.
-    """
-
-    def __init__ (self, range, lhs, type, rhs, ttl=None, cls=None, **kwargs) :
-        """
-            @param range (start, stop, step) tuple
-            @param lhs expression to generate the label
-            @param type the resource record type
-            @param rhs expression to generate the rdata
-        """
-
-        super(GenerateDirective, self).__init__('GENERATE', '%d-%d' % range, lhs, ttl, cls, type, rhs, **kwargs)
-
--- a/conf.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-"""
-    Generic configuration file output
-"""
-
-import os, os.path, tempfile, shutil
-
-class ConfObject (object) :
-    """
-        An object that can be written to a ConfFile, as multiple lines of text.
-    """
-
-    def __init__ (self, comments=None) :
-        """
-            Initialize with the given list of comments. Comments that are None should be ignore.
-        """
-        
-        # init the comments list
-        self.comments = comments or []
-
-    def add_comment (self, comment) :
-        """
-            Add a comment to be rendered in the output.
-        """
-        
-        # add it
-        self.comments.append(comment)
-
-    def fmt_lines (self) :
-        """
-            Yield a series of lines to be output in the file
-        """
-        
-        abstract
-
-class File (ConfObject) :
-    """
-        A single configuration file on the filesystem.
-
-        Configuration files are themselves ConfObject's, although this must be implemented in the inheriting class.
-    """
-
-    def __init__ (self, name, path, backup_suffix='.bak', mode=0644) :
-        """
-            Initialize the config file, but don't open it yet
-
-            @param name the human-readable friendly name for the config file
-            @param path the full filesystem path for the file
-            @param backup_suffix rename the old file by adding this suffix when replacing it
-            @param mode the permission bits for the new file
-        """
-  
-        self.name = name
-        self.path = path
-        self.backup_suffix = backup_suffix
-        self.mode = mode
-    
-    def write_file (self, file) :
-        """
-            Write out this config's stuff into the given file
-        """
-
-        writer = Writer(file)
-        writer.write_obj(self)
-
-    def write (self) :
-        """
-            Write out a new config file with this config's stuff using the following procedure:
-
-                * lock the real file
-                * open a new temporary file, and write out the objects into that, closing it
-                * move the real file out of the way
-                * move the temporary file into the real file's place
-                * unlock the real file
-        """
-
-        # XXX: how to aquire the lock?
-        # XXX: this needs checking over
-
-        # open the new temporary file
-        # XXX: ensure fd is closed
-        tmp_fd, tmp_path = tempfile.mkstemp(prefix=self.name)
-
-        # ...as a file
-        tmp_file = os.fdopen(tmp_fd, "w")
-        
-        # fix the permissions
-        os.chmod(tmp_path, self.mode)
-
-        # write it
-        self.write_file(tmp_file)
-
-        # close it
-        tmp_file.close()
-        
-        # move the old file out of the way
-        if os.path.exists(self.path) :
-            os.rename(self.path, self.path + self.backup_suffix)
-
-        # move the new file in
-        shutil.move(tmp_path, self.path)
-
-class Writer (object) :
-    """
-        A conf.Writer is used to write out a new conf.File (as a temporary file)
-    """
-
-    def __init__ (self, file) :
-        """
-            @param file the temporary file object
-        """
-
-        self.file = file
-    
-    def write_line (self, line) :
-        """
-            Write a single line to the file
-        """
-
-        self.file.write("%s\n" % (line, ))
-    
-    def write_lines (self, lines) :
-        """
-            Write a series of lines into the file
-        """
-
-        for line in lines :
-            self.write_line(line)
-
-    def write_obj (self, obj) :
-        """
-            Write a single object to the file
-        """
-        
-        # just write out all the lines
-        self.write_lines(obj.fmt_lines())
-
--- a/data.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-"""
-    Functions to load data from various sources
-"""
-
-import imp
-
-def load_py (name, path) :
-    """
-        Load a python file from the given filesystem path, returning the module itself.
-
-        The "name" of the module must be given, it should be something sane and unique...
-    """
-
-    # just load it and return
-    return imp.load_source(name, path)
-    
--- a/dhcp.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-"""
-    Higher-level DHCP config structure model
-"""
-
-import dhcp_conf as dhcpc
-
-class Config (dhcpc.ConfFile) :
-    """
-        A full configuration file
-    """
-
-    def __init__ (self, name=dhcpc.ConfFile.DEFAULT_NAME, path=dhcpc.ConfFile.DEFAULT_PATH, 
-            settings=None, options=None, shared_network=False, subnets=None, hosts=None, comment=None
-    ) :
-        """
-            Create a full configuration file for the given settings:
-            
-            settings:       a { name: value } mappping of general settings to set
-            options:        a { opt_name: opt_value } mapping of options to set
-            shared_network: define the subnets as a shared network of the given name
-            subnets:        an iterable of Subnet's to define
-            hosts:          an iterable of Host's to define
-
-        """
-
-        dhcpc.ConfFile.__init__(self, name, path, comment=comment)
-
-        # define global settings
-        if settings :
-            self.add_params(dhcpc.Parameter(setting, value) for setting, value in settings.iteritems())
-        
-        # define global options
-        if options :
-            self.add_params(dhcpc.Option(option, value) for option, value in options.iteritems())
-        
-        # the shared-network section, or a series of subnets
-        if shared_network :
-            self.add_decl(dhcpc.SharedNetwork(shared_network, decls=subnets))
-        
-        elif subnets :
-            self.add_decls(subnets)
-        
-        # hosts section
-        if hosts :
-            self.add_decls(hosts)
-
-class Subnet (dhcpc.Subnet) :
-    """
-        A subnet declaration with a router, and optionally a dynamic address pool, and allow/deny unknown clients
-    """
-
-    def __init__ (self, subnet, router_idx=1, range=None, unknown_clients=None, comment=None) :
-        """
-            @param subnet the addr.IP representing the subnet
-            @param router_idx the subnet[index] of the default gateway
-            @param range optional (from_idx, to_idx) to define a dhcp pool
-            @param unknown_clients optional 'allow'/'deny' to set a policy for unknown clients
-        """
-        
-        # validate
-        if unknown_clients :
-            assert unknown_clients in ('allow', 'deny')
-
-        super(Subnet, self).__init__(subnet, params=[
-            dhcpc.Option("routers", subnet[router_idx]),
-            dhcpc.Parameter("range", subnet[range[0]], subnet[range[1]]) if range else None,
-            dhcpc.Parameter(unknown_clients, "unknown-clients") if unknown_clients else None,
-        ], comment=comment)
-
-
-class Host (dhcpc.Host) :
-    """
-        A host declaration with a hardware address and a IP address
-    """
-
-    def __init__ (self, hostname, mac_addr, ip_addr, comment=None) :
-        super(Host, self).__init__(hostname, params=[
-            dhcpc.Parameter("hardware ethernet", mac_addr),
-            dhcpc.Parameter("fixed-address", ip_addr)
-        ], comment=comment)
-
--- a/dhcp_conf.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,341 +0,0 @@
-"""
-    Configuration file output for the ISC DHCP server
-"""
-
-import conf
-
-import itertools
-
-class Object (conf.ConfObject) :
-    """
-        Our version of ConfObject
-    """
-    
-    def _fmt_comments (self) :
-        """
-            Format our comment lines
-        """
-        
-        for comment in self.comments :
-            if comment is not None :
-                yield "# %s" % (comment, )
-
-
-class Comment (Object) :
-    """
-        A comment, is, well, a comment :)
-    """
-
-    def __init__ (self, comment) :
-        """
-            @param comment the comment string
-        """
-
-        Object.__init__(self, [comment])
-    
-    def fmt_lines (self) :
-        """
-            Yield a single line with the comment
-        """
-
-        return self._fmt_comments()
-
-class _Section (Object) :
-    """
-        Base implementation of Section, but doesn't format comments in output (inheriting class can define how that happens)
-    """
-
-    def __init__ (self, params=None, decls=None, comment=None) :
-        """
-            If params/decls are given, those are the used as the initial contents of this section
-
-            If a comment is given, then it will be formatted before the section's stuff
-        """
-        
-        Object.__init__(self, [comment])
-
-        self.params = params or []
-        self.decls = decls or []
-
-    def add_param (self, param) :
-        """
-            Add the given Parameter to the end of this section's params
-        """
-
-        self.params.append(param)
-
-    def add_params (self, params) :
-        for param in params :
-            self.add_param(param)
-
-    def add_decl (self, decl) :
-        """
-            Add the given Declaration to the end of this section's decls
-        """
-
-        self.decls.append(decl)
-
-    def add_decls (self, decls) :
-        for decl in decls :
-            self.add_decl(decl)
-
-    def fmt_lines (self) :
-        """
-            Format all of our params and decls, in that order
-        """
-
-        # then output each content line
-        for stmt in itertools.chain(self.params, self.decls) :
-            # skip Nones
-            if stmt is None :
-                continue
-
-            for line in stmt.fmt_lines() :
-                yield line
-
-class Section (_Section) :
-    """
-        A section holds a list of params and a list of decls, plus some comments at the beginning of the section
-    """
-    
-    def fmt_lines (self) :
-        """
-            Format all of our comments, and then super
-        """
-
-        # comments
-        for line in self._fmt_comments() :
-            yield line
-
-        # section stuff
-        for line in _Section.fmt_lines(self) :
-            yield line
-
-class ConfFile (Section, conf.File) :
-    DEFAULT_NAME = "dhcpd.conf"
-    DEFAULT_PATH = "/etc/dhcp3/dhcpd.conf"
-
-    def __init__ (self, name=DEFAULT_NAME, path=DEFAULT_PATH, params=None, decls=None, comment=None) :
-        """
-            Initialize the dhcpd config file, but don't open it yet.
-        """
-        
-        conf.File.__init__(self, name, path)
-        Section.__init__(self, params, decls, comment)
-
-class Statement (Object) :
-    """
-        A statement is a single line in the config file
-    """
-
-    def __init__ (self, name, *args, **kwargs) :
-        """
-            Arguments given as None will be ignored.
-
-            A comment can be given as a keyword argument
-        """
-
-        if kwargs : assert len(kwargs) == 1 and 'comment' in kwargs
-
-        Object.__init__(self, [kwargs.get('comment')])
-
-        self.name = name
-        self.args = [arg for arg in args if arg is not None]
-    
-    def _fmt_arg (self, arg) :
-        """
-            Formats a arg for use in output, the following types are supported:
-
-                list/tuple/iter:    results in a comma-and-space separated list of formatted values
-                unicode:            results in an encoded str
-                str:                results in the string itself, quoted if needed
-                other:              attempt to convert to a str, and then format that
-        """
-        
-        # format lists specially
-        # XXX: iterators?
-        if isinstance(arg, (list, tuple)) :
-            # recurse as a comma-and-space separated list
-            return ', '.join(self._fmt_arg(a) for a in arg)
-
-        elif isinstance(arg, Literal) :
-            # use what it specifies
-            return arg.fmt_arg()
-
-        elif isinstance(arg, unicode) :
-            # recurse with the str version
-            # XXX: what encoding to use?
-            return self._fmt_arg(arg.encode('utf8'))
-
-        elif isinstance(arg, str) :
-            # XXX: quoting
-            return arg
-        
-        else :
-            # try and use it as a string
-            return self._fmt_arg(str(arg))
-    
-    def _fmt_data (self) :
-        """
-            Formats the statement name/params as a single line, ignoring None
-        """
-
-        return "%s%s" % (self.name, (' ' + ' '.join(self._fmt_arg(a) for a in self.args)) if self.args else '')
-
-class Literal (Statement) :
-    """
-        A literal is something that goes into the config file as-is, with no formatting or escaping applied.
-    """
-
-    def __init__ (self, literal) :
-        self.literal = literal
-    
-    def fmt_arg (self) :
-        return self.literal
-
-    def fmt_lines (self) :
-        yield self.literal
-
-class Parameter (Statement) :
-    """
-        A parameter is a single statement that configures the behaviour of something.
-
-        Parameters have a name, and optionally, a number of arguments, and are formatted as statements terminated with
-        a semicolon. For convenience, params/decls that are None are ignored.
-            
-        The parameter will be formatted like this:
-            <name> [ <arg> [ ... ] ] ";"
-    """
-    
-    def fmt_lines (self) :
-        """
-            Yields a single ;-terminated line
-        """
-        
-        # comments
-        for line in self._fmt_comments() :
-            yield line
-        
-        # the line
-        yield "%s;" % self._fmt_data()
-
-class Declaration (_Section, Statement) :
-    """
-        A declaration begins like a statement (with name and args), but then contains a curly-braces-delimited block
-        that acts like a Section.
-
-        <name> [ <args> [ ... ] ] {
-            [ <Section> ]
-        }
-        
-    """
-
-    def __init__ (self, name, args=[], params=None, decls=None, comment=None) :
-        """
-            The name/args will be formatted as in Statement, but params should be an iterable of Parameters, and decls
-            an iterable of Declarations.
-        """
-        
-        # init the statement bit
-        _Section.__init__(self, params, decls)
-        Statement.__init__(self, name, *args, **dict(comment=comment))
-
-    def fmt_lines (self) :
-        """
-            Yields a header line, a series of indented body lines, and the footer line
-        """
-        
-        # comments
-        for line in self._fmt_comments() :
-            yield line
-        
-        # the header to open the block
-        yield "%s {" % self._fmt_data()
-        
-        # then output the section stuff, indented
-        for line in _Section.fmt_lines(self) :
-            yield "\t%s" % line
-
-        # and then close the block
-        yield "}"
-
-class SharedNetwork (Declaration) :
-    """
-        A shared-network declaration is used to define a set of subnets that share the same physical network,
-        optionally with some shared params.
-
-        shared-network <name> {
-            [ parameters ]
-            [ declarations ]
-        }
-    """
-
-    def __init__ (self, name, *args, **kwargs) :
-        """
-            @param name the name of the shared-subnet
-        """
-
-        super(SharedNetwork, self).__init__("shared-network", [name], *args, **kwargs)
-
-class Subnet (Declaration) :
-    """
-        A subnet is used to provide the information about a subnet required to identify whether or not an IP address is
-        on that subnet, and may also be used to specify parameters/declarations for that subnet.
-        
-        subnet <subnet-number> netmask <netmask> {
-            [ parameters ]
-            [ declarations ]
-        }
-    """
-
-    def __init__ (self, network, *args, **kwargs) :
-        """
-            @param network the addr.Network for the subnet
-        """
-
-        super(Subnet, self).__init__("subnet", [network.net(), "netmask", network.netmask()], *args, **kwargs)
-
-class Group (Declaration) :
-    """
-        A group is simply used to apply a set of parameters to a set of declarations.
-
-        group {
-            [ parameters ]
-            [ declarations ]
-        }
-    """
-
-    def __init__ (self, *args, **kwargs) :
-        super(Group, self).__init__("group", [], *args, **kwargs)
-
-
-class Host (Declaration) :
-    """
-        A host is used to match a request against specific host, and then apply settings for that host.
-
-        The "hostname" is the DHCP name to identify the host. 
-
-        If no dhcp-client-identifier option is specified in the parameters, then the host is matched using the
-        "hardware" parameter.
-
-        host <hostname> {
-            [ parameters ]
-            [ declarations ]
-        }
-    """
-
-    def __init__ (self, hostname, *args, **kwargs) :
-        super(Host, self).__init__("host", [hostname], *args, **kwargs)
-
-class Option (Parameter) :
-    """
-        A generic 'option' parameter for a dhcpd.conf file
-    """
-
-    def __init__ (self, name, *args, **kwargs) :
-        """
-            Formatted as a Satement with a name of "option <name>".
-        """
-
-        super(Option, self).__init__("option %s" % name, *args, **kwargs)
-
--- a/host.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-"""
-    Information about one physica host
-"""
-
-import dhcp
-import bind_conf as bindc
-
-class Interface (object) :
-    """
-        A physical interface for a host
-    """
-
-    def __init__ (self, mac_addr, name=None) :
-        """
-            @param name the short name of the interface (e.g. 'lan' or 'wlan'), or None for no suffix
-            @param mac the physical-layer addr.MAC address
-        """
-
-        self.addr = mac_addr
-        self.name = name
-
-class Host (object) :
-    """
-        A host has a single address/name, an owner, and multiple interfaces
-    """
-
-    def __init__ (self, hostname, address, interfaces) :
-        """
-            @param hostname the short hostname, without the domain name component
-            @param address the addr.IP address
-            @param interfaces a list of zero or more Interface objects
-        """
-
-        self.hostname = hostname
-        self.address = address
-        self.interfaces = interfaces
-    
-    def build_dhcp_hosts (self) :
-        """
-            Build and yield a series of dhcp_conf.Host objects for this host.
-
-            If the host does not have any interfaces defined, this doesn't yield anything
-        """
-        
-        # XXX: do we want to ensure that the host names are unique?
-        
-        for iface in self.interfaces :
-            # the DHCP hostname
-            name = "%s%s" % (self.hostname, ('-%s' % (iface.name)) if iface.name else '')
-            
-            # 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)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/in/hosts.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,55 @@
+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, FQDN
+
+# BIND stuff
+domain          = "paivola.fi"
+
+nameservers     = [
+        FQDN("ranssi.paivola.fi"),
+        FQDN("misc1.idler.fi"),
+        FQDN("misc2.idler.fi"),
+        FQDN("srv.marttila.de"),
+    ]
+
+mailservers     = [
+        FQDN("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    = {
+    'domain-name-servers':  IP('194.197.235.145'),
+}
+
+shared_network  = 'PVL'
+subnets         = [
+    Subnet(Network('194.197.235.0/24'), router_idx=1, range=(26, 70), unknown_clients='allow', comment="Public network"),
+    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'),   [ 
+        Interface('00:16:01:37:D1:D2'), 
+        Interface('00:0F:B0:0A:EF:58'),
+    ]),
+]
+
--- a/main.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-#!/usr/bin/env python2.5
-
-import data, dhcp, bind_conf, bind
-
-import optparse, itertools
-
-def parse_args (argv) :
-    """
-        Parse the command-line arguments from the given argv list, returning a (options_struct, args_list) tuple,
-        as per optparse.
-    """
-
-    usage = "Usage: %prog [options] data-file"
-
-    # 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('--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:])
-
-    # parse the positional arguments
-    data_file, = args
-
-    # ok
-    return options, (data_file, )
-
-def write_dhcp (options, settings) :
-    """
-        Write the DHCP config module using the data loaded from the given module
-    """
-    
-    # build the config file
-    config = dhcp.Config(path=options.dhcpd_conf,
-        settings        = settings.dhcp_settings,
-        options         = settings.dhcp_options,
-        shared_network  = settings.shared_network,
-        subnets         = settings.subnets,
-        hosts           = itertools.chain(*(host.build_dhcp_hosts() for host in settings.hosts)),
-    )
-
-    # 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
-    """
-    
-    # parse args
-    options, (data_file, ) = parse_args(argv)
-
-    # load the data
-    data_module = data.load_py('pvl_hosts_data', data_file)
-    
-    # write out the config files
-    write_dhcp(options, data_module)
-    write_bind(options, data_module)
-
-if __name__ == '__main__' :
-    from sys import argv
-
-    main(argv)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl-config	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,88 @@
+#!/usr/bin/env python2.5
+
+import data, dhcp, bind_conf, bind
+
+import optparse, itertools
+
+def parse_args (argv) :
+    """
+        Parse the command-line arguments from the given argv list, returning a (options_struct, args_list) tuple,
+        as per optparse.
+    """
+
+    usage = "Usage: %prog [options] data-file"
+
+    # 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('--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:])
+
+    # parse the positional arguments
+    data_file, = args
+
+    # ok
+    return options, (data_file, )
+
+def write_dhcp (options, settings) :
+    """
+        Write the DHCP config module using the data loaded from the given module
+    """
+    
+    # build the config file
+    config = dhcp.Config(path=options.dhcpd_conf,
+        settings        = settings.dhcp_settings,
+        options         = settings.dhcp_options,
+        shared_network  = settings.shared_network,
+        subnets         = settings.subnets,
+        hosts           = itertools.chain(*(host.build_dhcp_hosts() for host in settings.hosts)),
+    )
+
+    # 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
+    """
+    
+    # parse args
+    options, (data_file, ) = parse_args(argv)
+
+    # load the data
+    data_module = data.load_py('pvl_hosts_data', data_file)
+    
+    # write out the config files
+    write_dhcp(options, data_module)
+    write_bind(options, data_module)
+
+if __name__ == '__main__' :
+    from sys import argv
+
+    main(argv)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/addr.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,60 @@
+"""
+    Used to define IP-address/subnet stuff
+"""
+
+import IPy
+
+class IP (IPy.IP, object) :
+    """
+        A literal IPv4 address
+    """
+    
+    def __init__ (self, address) :
+        """
+            Parse the given literal IP address in "a.b.c.d" form
+        """
+    
+        super(IP, self).__init__(address)
+    
+    def is_v4 (self) :
+        """
+            Returns True if the address is an IPv4 address
+        """
+
+        return self.version() == 4
+
+    def is_v6 (self) :
+        """
+            Returns True if the address is an IPv6 address
+        """
+
+        return self.version() == 6
+
+class Network (IPy.IP, object) :
+    """
+        An IPv4 network (subnet)
+    """
+
+    def __init__ (self, prefix) :
+        """
+            Parse the given prefix in "a.b.c.d/l" form
+        """
+
+        super(Network, self).__init__(prefix)
+
+class MAC (object) :
+    """
+        A mac address
+    """
+
+    def __init__ (self, mac) :
+        """
+            Parse the given MAC address in "aa:bb:cc:dd:ee:ff" form
+        """
+
+        # XXX: validate
+        self.mac = mac
+
+    def __str__ (self) :
+        return self.mac
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/bind.py	Sun Jul 12 00:51:08 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/bind_conf.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,395 @@
+"""
+    Configuration file output for the ISC DNS server: BIND
+"""
+
+import conf
+
+class Object (conf.ConfObject) :
+    """
+        Our own version of ConfObject that knows how to format comments
+    """
+
+    def _fmt_comments (self) :
+        """
+            Format comments using the standard format:
+                ";" <comment>
+        """
+
+        for comment in self.comments :
+            if comment is not None :
+                yield "; %s" % (comment, )
+
+class ZoneFile (Object, conf.File) :
+    """
+        A zone file containing a bunch of directives, resource records and comments
+    """
+
+    def __init__ (self, name, path, ttl=None, comment=None) :
+        """
+            @param name the name of the zonefile, for status stuff
+            @param path the path to the zonefile
+            @param ttl default TTL to use
+            @param comment optional comments
+        """
+        
+        Object.__init__(self, comment)
+        conf.File.__init__(self, name, path)
+
+        # init
+        self.objects = []
+
+        if ttl :
+            self.add_directive(TTLDirective(ttl))
+
+    def add_obj (self, obj) :
+        """
+            Add an Object onto the list of things to include in this zone file
+        """
+
+        self.objects.append(obj)
+
+    # 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 :)
+    """
+
+    def __init__ (self, comment) :
+        """
+            @param comment the comment string
+        """
+
+        Object.__init__(self, [comment])
+    
+    def fmt_lines (self) :
+        """
+            Yield a single line with the comment
+        """
+
+        return self._fmt_comments()
+
+class Label (object) :
+    """
+        A label, as used in a ResourceRecord, either as the label, or the rdata for various resource types
+
+        You can also use strs, this just implements a __str__ method
+    """
+
+    def __init__ (self, label) :
+        """
+            @param label the literal label to use
+        """
+
+        self.label = label
+
+    def __str__ (self) :
+        return self.label
+
+class Origin (Label) :
+    """
+        A label that represents the zone's origin
+    """
+
+    def __init__ (self) :
+        pass
+
+    def __str__ (self) :
+        return '@'
+
+class FQDN (Label) :
+    """
+        A label that represents the given external domain (i.e. this adds the . to the end that people always forget).
+    """
+
+    def __init__ (self, fqdn) :
+        self.fqdn = fqdn
+
+    def __str__ (self) :
+        return "%s." % (self.fqdn, )
+
+class Interval (object) :
+    """
+        A time interval suitable for use in SOA records
+    """
+
+    def __init__ (self, s=None, m=None, h=None, d=None) :
+        """
+            @param s seconds
+            @param m minutes
+            @param h hours
+            @param d days
+        """
+
+        self.s = s
+        self.m = m
+        self.h = h
+        self.d = d
+
+    def __str__ (self) :
+        """
+            If only seconds were given, just return those directly, otherwise, apply units
+        """
+
+        if self.s and not self.m and not self.h and not self.d :
+            return str(self.s)
+
+        else :
+            return "%s%s%s%s" % (
+                    "%ds" % self.s if self.s else '',
+                    "%dm" % self.m if self.m else '',
+                    "%dh" % self.h if self.h else '',
+                    "%dd" % self.d if self.d else ''
+                )
+
+class ResourceRecord (Object) :
+    """
+        A generic resource record for a BIND zone file
+    """
+
+    def __init__ (self, label, type, rdata, cls='IN', ttl=None, **kwargs) :
+        """
+            @param label the "name" of this record, or None to referr to the previous record's label
+            @param type the type as a string ('A', 'TXT', etc.)
+            @param rdata the rdata, as a raw string
+            @param cls the class, e.g. 'IN'
+            @param ttl the time-to-live value in seconds, or None to omit it
+        """
+
+        super(ResourceRecord, self).__init__(**kwargs)
+
+        self.label = label
+        self.type = type
+        self.rdata = rdata
+        self.cls = cls
+        self.ttl = ttl
+
+    def fmt_lines (self) :
+        """
+            Just format the lines, eh
+        """
+        
+        # prefix comments
+        for line in self._fmt_comments() :
+            yield line
+
+        # 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)
+
+class SOA (ResourceRecord) :
+    """
+        "Identifies the start of a zone of authority", must be the first record
+    """
+
+    def __init__ (self, label, primary_ns, hostmaster, serial, refresh, retry, expire, minimum, **kwargs) :
+        """
+            @param label the "name" of the zone, usually ORIGIN
+            @param primary_ns the address of the primary NS server
+            @param hostmaster the mailbox of the zone's hostmaster
+            @param serial the serial number of the zone as an integer
+            @param refresh time interval between zone refreshes in seconds
+            @param retry time interval between retrys for failed refreshes
+            @param expire time interval before zone data can no longer be considered authorative
+            @param minimum minimum TTL for RRs in this zone
+        """
+
+        super(SOA, self).__init__(label, 'SOA', "%s %s ( %s %s %s %s %s )" % (
+                primary_ns, hostmaster, serial, refresh, retry, expire, minimum
+            ), **kwargs)
+
+class A (ResourceRecord) :
+    """
+        An IPv4 forward address
+    """
+
+    def __init__ (self, label, addr, **kwargs) :
+        """
+            @param label the "name" of the address
+            @param addr the IPv4 target address
+        """
+
+        assert(addr.is_v4())
+
+        super(A, self).__init__(label, 'A', addr, **kwargs)
+
+class AAAA (ResourceRecord) :
+    """
+        An IPv6 forward address
+    """
+
+    def __init__ (self, label, addr, **kwargs) :
+        """
+            @param label the "name" of the address
+            @param addr the IPv6 target address
+        """
+        
+        assert(addr.is_v6())
+
+        super(AAAA, self).__init__(label, 'AAAA', addr.strCompressed(), **kwargs)
+
+class CNAME (ResourceRecord) :
+    """
+        A canonical-name alias
+    """
+
+    def __init__ (self, label, target, **kwargs) :
+        """
+            @param label the "name" of the alias
+            @param target the alias target
+        """
+
+        super(CNAME, self).__init__(label, 'CNAME', target, **kwargs)
+
+class TXT (ResourceRecord) :
+    """
+        A human-readable information record
+    """
+
+    def __init__ (self, label, text, **kwargs) :
+        """
+            @param label the "name" of the text record
+            @param text the text data, shouldn't contain any quotes...
+        """
+
+        super(TXT, self).__init__(label, 'TXT', '"%s"' % text, **kwargs)
+
+class MX (ResourceRecord) :
+    """
+        A mail-exchange definition
+    """
+
+    def __init__ (self, label, pref, exchange, **kwargs) :
+        """
+            @param label the "name" of the domain to handle mail for
+            @param pref the numerical preference for this exchange
+            @param exchange the domain name of the mail exchange (SMTP server)
+        """
+
+        super(MX, self).__init__(label, 'MX', "%d %s" % (pref, exchange), **kwargs)
+
+class NS (ResourceRecord) :
+    """
+        An authorative name server
+    """
+
+    def __init__ (self, label, nsname, **kwargs) :
+        """
+            @param label the "name" of the domain to have a nameserver for
+            @param nsname the name of the nameserver
+        """
+
+        super(NS, self).__init__(label, 'NS', nsname)
+
+class PTR (ResourceRecord) :
+    """
+        An IPv4/IPv6 reverse address
+    """
+
+    def __init__ (self, addr, name, **kwargs) :
+        """
+            @param addr the addr.IP to map via in-addr.arpa
+            @param name the name to map the address to
+        """
+
+        # XXX: quick hack, this gives an absolute name
+        label = addr.reverseName()
+        
+        super(PTR, self).__init__(label, 'PTR', name)
+
+class Directive (Object) :
+    """
+        Special directives that can be used in zone files to control behaviour
+    """
+
+    def __init__ (self, name, *args, **kwargs) :
+        """
+            @param name the $NAME bit
+            @param args optional list of space-seprated arguments, Nones are ignored
+        """
+
+        super(Directive, self).__init__(**kwargs)
+
+        self.name = name
+        self.args = [arg for arg in args if arg is not None]
+
+    def fmt_lines (self) :
+        # prefix comments
+        for line in self._fmt_comments() :
+            yield line
+
+        # then format it
+        yield "$%s%s" % (self.name, (' ' + ' '.join(str(arg) for arg in self.args)) if self.args else '')
+
+class OriginDirective (Directive) :
+    """
+        Set the origin used to resolve the zone's labels.
+
+        Note that the origin label is not absolute by default - use FQDN for that
+    """
+
+    def __init__ (self, origin, **kwargs) :
+        """
+            @param origin the origin label
+        """
+
+        super(OriginDirective, self).__init__('ORIGIN', origin, **kwargs)
+
+class TTLDirective (Directive) :
+    """
+        Set the TTL used for records by default
+    """
+
+    def __init__ (self, ttl, **kwargs) :
+        """
+            @param ttl the new ttl to use
+        """
+
+        super(TTLDirective, self).__init__('TTL', ttl, **kwargs)
+
+class IncludeDirective (Directive) :
+    """
+        Include another zoen file, optionally with a different origin
+    """
+
+    def __init__ (self, filename, origin=None, **kwargs) :
+        """
+            @param filename the zone file to include
+            @param origin the optional origin to process the zonefile with
+        """
+
+        super(IncludeDirective, self).__init__('INCLUDE', filename, origin, **kwargs)
+
+class GenerateDirective (Directive) :
+    """
+        Generate a bunch of numbered records using an expression for the label and rdata.
+
+        At the simplest, any "$" in the expression is replaced with the value of the iterator.
+    """
+
+    def __init__ (self, range, lhs, type, rhs, ttl=None, cls=None, **kwargs) :
+        """
+            @param range (start, stop, step) tuple
+            @param lhs expression to generate the label
+            @param type the resource record type
+            @param rhs expression to generate the rdata
+        """
+
+        super(GenerateDirective, self).__init__('GENERATE', '%d-%d' % range, lhs, ttl, cls, type, rhs, **kwargs)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/conf.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,136 @@
+"""
+    Generic configuration file output
+"""
+
+import os, os.path, tempfile, shutil
+
+class ConfObject (object) :
+    """
+        An object that can be written to a ConfFile, as multiple lines of text.
+    """
+
+    def __init__ (self, comments=None) :
+        """
+            Initialize with the given list of comments. Comments that are None should be ignore.
+        """
+        
+        # init the comments list
+        self.comments = comments or []
+
+    def add_comment (self, comment) :
+        """
+            Add a comment to be rendered in the output.
+        """
+        
+        # add it
+        self.comments.append(comment)
+
+    def fmt_lines (self) :
+        """
+            Yield a series of lines to be output in the file
+        """
+        
+        abstract
+
+class File (ConfObject) :
+    """
+        A single configuration file on the filesystem.
+
+        Configuration files are themselves ConfObject's, although this must be implemented in the inheriting class.
+    """
+
+    def __init__ (self, name, path, backup_suffix='.bak', mode=0644) :
+        """
+            Initialize the config file, but don't open it yet
+
+            @param name the human-readable friendly name for the config file
+            @param path the full filesystem path for the file
+            @param backup_suffix rename the old file by adding this suffix when replacing it
+            @param mode the permission bits for the new file
+        """
+  
+        self.name = name
+        self.path = path
+        self.backup_suffix = backup_suffix
+        self.mode = mode
+    
+    def write_file (self, file) :
+        """
+            Write out this config's stuff into the given file
+        """
+
+        writer = Writer(file)
+        writer.write_obj(self)
+
+    def write (self) :
+        """
+            Write out a new config file with this config's stuff using the following procedure:
+
+                * lock the real file
+                * open a new temporary file, and write out the objects into that, closing it
+                * move the real file out of the way
+                * move the temporary file into the real file's place
+                * unlock the real file
+        """
+
+        # XXX: how to aquire the lock?
+        # XXX: this needs checking over
+
+        # open the new temporary file
+        # XXX: ensure fd is closed
+        tmp_fd, tmp_path = tempfile.mkstemp(prefix=self.name)
+
+        # ...as a file
+        tmp_file = os.fdopen(tmp_fd, "w")
+        
+        # fix the permissions
+        os.chmod(tmp_path, self.mode)
+
+        # write it
+        self.write_file(tmp_file)
+
+        # close it
+        tmp_file.close()
+        
+        # move the old file out of the way
+        if os.path.exists(self.path) :
+            os.rename(self.path, self.path + self.backup_suffix)
+
+        # move the new file in
+        shutil.move(tmp_path, self.path)
+
+class Writer (object) :
+    """
+        A conf.Writer is used to write out a new conf.File (as a temporary file)
+    """
+
+    def __init__ (self, file) :
+        """
+            @param file the temporary file object
+        """
+
+        self.file = file
+    
+    def write_line (self, line) :
+        """
+            Write a single line to the file
+        """
+
+        self.file.write("%s\n" % (line, ))
+    
+    def write_lines (self, lines) :
+        """
+            Write a series of lines into the file
+        """
+
+        for line in lines :
+            self.write_line(line)
+
+    def write_obj (self, obj) :
+        """
+            Write a single object to the file
+        """
+        
+        # just write out all the lines
+        self.write_lines(obj.fmt_lines())
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/data.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,16 @@
+"""
+    Functions to load data from various sources
+"""
+
+import imp
+
+def load_py (name, path) :
+    """
+        Load a python file from the given filesystem path, returning the module itself.
+
+        The "name" of the module must be given, it should be something sane and unique...
+    """
+
+    # just load it and return
+    return imp.load_source(name, path)
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/dhcp.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,81 @@
+"""
+    Higher-level DHCP config structure model
+"""
+
+import dhcp_conf as dhcpc
+
+class Config (dhcpc.ConfFile) :
+    """
+        A full configuration file
+    """
+
+    def __init__ (self, name=dhcpc.ConfFile.DEFAULT_NAME, path=dhcpc.ConfFile.DEFAULT_PATH, 
+            settings=None, options=None, shared_network=False, subnets=None, hosts=None, comment=None
+    ) :
+        """
+            Create a full configuration file for the given settings:
+            
+            settings:       a { name: value } mappping of general settings to set
+            options:        a { opt_name: opt_value } mapping of options to set
+            shared_network: define the subnets as a shared network of the given name
+            subnets:        an iterable of Subnet's to define
+            hosts:          an iterable of Host's to define
+
+        """
+
+        dhcpc.ConfFile.__init__(self, name, path, comment=comment)
+
+        # define global settings
+        if settings :
+            self.add_params(dhcpc.Parameter(setting, value) for setting, value in settings.iteritems())
+        
+        # define global options
+        if options :
+            self.add_params(dhcpc.Option(option, value) for option, value in options.iteritems())
+        
+        # the shared-network section, or a series of subnets
+        if shared_network :
+            self.add_decl(dhcpc.SharedNetwork(shared_network, decls=subnets))
+        
+        elif subnets :
+            self.add_decls(subnets)
+        
+        # hosts section
+        if hosts :
+            self.add_decls(hosts)
+
+class Subnet (dhcpc.Subnet) :
+    """
+        A subnet declaration with a router, and optionally a dynamic address pool, and allow/deny unknown clients
+    """
+
+    def __init__ (self, subnet, router_idx=1, range=None, unknown_clients=None, comment=None) :
+        """
+            @param subnet the addr.IP representing the subnet
+            @param router_idx the subnet[index] of the default gateway
+            @param range optional (from_idx, to_idx) to define a dhcp pool
+            @param unknown_clients optional 'allow'/'deny' to set a policy for unknown clients
+        """
+        
+        # validate
+        if unknown_clients :
+            assert unknown_clients in ('allow', 'deny')
+
+        super(Subnet, self).__init__(subnet, params=[
+            dhcpc.Option("routers", subnet[router_idx]),
+            dhcpc.Parameter("range", subnet[range[0]], subnet[range[1]]) if range else None,
+            dhcpc.Parameter(unknown_clients, "unknown-clients") if unknown_clients else None,
+        ], comment=comment)
+
+
+class Host (dhcpc.Host) :
+    """
+        A host declaration with a hardware address and a IP address
+    """
+
+    def __init__ (self, hostname, mac_addr, ip_addr, comment=None) :
+        super(Host, self).__init__(hostname, params=[
+            dhcpc.Parameter("hardware ethernet", mac_addr),
+            dhcpc.Parameter("fixed-address", ip_addr)
+        ], comment=comment)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/dhcp_conf.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,341 @@
+"""
+    Configuration file output for the ISC DHCP server
+"""
+
+import conf
+
+import itertools
+
+class Object (conf.ConfObject) :
+    """
+        Our version of ConfObject
+    """
+    
+    def _fmt_comments (self) :
+        """
+            Format our comment lines
+        """
+        
+        for comment in self.comments :
+            if comment is not None :
+                yield "# %s" % (comment, )
+
+
+class Comment (Object) :
+    """
+        A comment, is, well, a comment :)
+    """
+
+    def __init__ (self, comment) :
+        """
+            @param comment the comment string
+        """
+
+        Object.__init__(self, [comment])
+    
+    def fmt_lines (self) :
+        """
+            Yield a single line with the comment
+        """
+
+        return self._fmt_comments()
+
+class _Section (Object) :
+    """
+        Base implementation of Section, but doesn't format comments in output (inheriting class can define how that happens)
+    """
+
+    def __init__ (self, params=None, decls=None, comment=None) :
+        """
+            If params/decls are given, those are the used as the initial contents of this section
+
+            If a comment is given, then it will be formatted before the section's stuff
+        """
+        
+        Object.__init__(self, [comment])
+
+        self.params = params or []
+        self.decls = decls or []
+
+    def add_param (self, param) :
+        """
+            Add the given Parameter to the end of this section's params
+        """
+
+        self.params.append(param)
+
+    def add_params (self, params) :
+        for param in params :
+            self.add_param(param)
+
+    def add_decl (self, decl) :
+        """
+            Add the given Declaration to the end of this section's decls
+        """
+
+        self.decls.append(decl)
+
+    def add_decls (self, decls) :
+        for decl in decls :
+            self.add_decl(decl)
+
+    def fmt_lines (self) :
+        """
+            Format all of our params and decls, in that order
+        """
+
+        # then output each content line
+        for stmt in itertools.chain(self.params, self.decls) :
+            # skip Nones
+            if stmt is None :
+                continue
+
+            for line in stmt.fmt_lines() :
+                yield line
+
+class Section (_Section) :
+    """
+        A section holds a list of params and a list of decls, plus some comments at the beginning of the section
+    """
+    
+    def fmt_lines (self) :
+        """
+            Format all of our comments, and then super
+        """
+
+        # comments
+        for line in self._fmt_comments() :
+            yield line
+
+        # section stuff
+        for line in _Section.fmt_lines(self) :
+            yield line
+
+class ConfFile (Section, conf.File) :
+    DEFAULT_NAME = "dhcpd.conf"
+    DEFAULT_PATH = "/etc/dhcp3/dhcpd.conf"
+
+    def __init__ (self, name=DEFAULT_NAME, path=DEFAULT_PATH, params=None, decls=None, comment=None) :
+        """
+            Initialize the dhcpd config file, but don't open it yet.
+        """
+        
+        conf.File.__init__(self, name, path)
+        Section.__init__(self, params, decls, comment)
+
+class Statement (Object) :
+    """
+        A statement is a single line in the config file
+    """
+
+    def __init__ (self, name, *args, **kwargs) :
+        """
+            Arguments given as None will be ignored.
+
+            A comment can be given as a keyword argument
+        """
+
+        if kwargs : assert len(kwargs) == 1 and 'comment' in kwargs
+
+        Object.__init__(self, [kwargs.get('comment')])
+
+        self.name = name
+        self.args = [arg for arg in args if arg is not None]
+    
+    def _fmt_arg (self, arg) :
+        """
+            Formats a arg for use in output, the following types are supported:
+
+                list/tuple/iter:    results in a comma-and-space separated list of formatted values
+                unicode:            results in an encoded str
+                str:                results in the string itself, quoted if needed
+                other:              attempt to convert to a str, and then format that
+        """
+        
+        # format lists specially
+        # XXX: iterators?
+        if isinstance(arg, (list, tuple)) :
+            # recurse as a comma-and-space separated list
+            return ', '.join(self._fmt_arg(a) for a in arg)
+
+        elif isinstance(arg, Literal) :
+            # use what it specifies
+            return arg.fmt_arg()
+
+        elif isinstance(arg, unicode) :
+            # recurse with the str version
+            # XXX: what encoding to use?
+            return self._fmt_arg(arg.encode('utf8'))
+
+        elif isinstance(arg, str) :
+            # XXX: quoting
+            return arg
+        
+        else :
+            # try and use it as a string
+            return self._fmt_arg(str(arg))
+    
+    def _fmt_data (self) :
+        """
+            Formats the statement name/params as a single line, ignoring None
+        """
+
+        return "%s%s" % (self.name, (' ' + ' '.join(self._fmt_arg(a) for a in self.args)) if self.args else '')
+
+class Literal (Statement) :
+    """
+        A literal is something that goes into the config file as-is, with no formatting or escaping applied.
+    """
+
+    def __init__ (self, literal) :
+        self.literal = literal
+    
+    def fmt_arg (self) :
+        return self.literal
+
+    def fmt_lines (self) :
+        yield self.literal
+
+class Parameter (Statement) :
+    """
+        A parameter is a single statement that configures the behaviour of something.
+
+        Parameters have a name, and optionally, a number of arguments, and are formatted as statements terminated with
+        a semicolon. For convenience, params/decls that are None are ignored.
+            
+        The parameter will be formatted like this:
+            <name> [ <arg> [ ... ] ] ";"
+    """
+    
+    def fmt_lines (self) :
+        """
+            Yields a single ;-terminated line
+        """
+        
+        # comments
+        for line in self._fmt_comments() :
+            yield line
+        
+        # the line
+        yield "%s;" % self._fmt_data()
+
+class Declaration (_Section, Statement) :
+    """
+        A declaration begins like a statement (with name and args), but then contains a curly-braces-delimited block
+        that acts like a Section.
+
+        <name> [ <args> [ ... ] ] {
+            [ <Section> ]
+        }
+        
+    """
+
+    def __init__ (self, name, args=[], params=None, decls=None, comment=None) :
+        """
+            The name/args will be formatted as in Statement, but params should be an iterable of Parameters, and decls
+            an iterable of Declarations.
+        """
+        
+        # init the statement bit
+        _Section.__init__(self, params, decls)
+        Statement.__init__(self, name, *args, **dict(comment=comment))
+
+    def fmt_lines (self) :
+        """
+            Yields a header line, a series of indented body lines, and the footer line
+        """
+        
+        # comments
+        for line in self._fmt_comments() :
+            yield line
+        
+        # the header to open the block
+        yield "%s {" % self._fmt_data()
+        
+        # then output the section stuff, indented
+        for line in _Section.fmt_lines(self) :
+            yield "\t%s" % line
+
+        # and then close the block
+        yield "}"
+
+class SharedNetwork (Declaration) :
+    """
+        A shared-network declaration is used to define a set of subnets that share the same physical network,
+        optionally with some shared params.
+
+        shared-network <name> {
+            [ parameters ]
+            [ declarations ]
+        }
+    """
+
+    def __init__ (self, name, *args, **kwargs) :
+        """
+            @param name the name of the shared-subnet
+        """
+
+        super(SharedNetwork, self).__init__("shared-network", [name], *args, **kwargs)
+
+class Subnet (Declaration) :
+    """
+        A subnet is used to provide the information about a subnet required to identify whether or not an IP address is
+        on that subnet, and may also be used to specify parameters/declarations for that subnet.
+        
+        subnet <subnet-number> netmask <netmask> {
+            [ parameters ]
+            [ declarations ]
+        }
+    """
+
+    def __init__ (self, network, *args, **kwargs) :
+        """
+            @param network the addr.Network for the subnet
+        """
+
+        super(Subnet, self).__init__("subnet", [network.net(), "netmask", network.netmask()], *args, **kwargs)
+
+class Group (Declaration) :
+    """
+        A group is simply used to apply a set of parameters to a set of declarations.
+
+        group {
+            [ parameters ]
+            [ declarations ]
+        }
+    """
+
+    def __init__ (self, *args, **kwargs) :
+        super(Group, self).__init__("group", [], *args, **kwargs)
+
+
+class Host (Declaration) :
+    """
+        A host is used to match a request against specific host, and then apply settings for that host.
+
+        The "hostname" is the DHCP name to identify the host. 
+
+        If no dhcp-client-identifier option is specified in the parameters, then the host is matched using the
+        "hardware" parameter.
+
+        host <hostname> {
+            [ parameters ]
+            [ declarations ]
+        }
+    """
+
+    def __init__ (self, hostname, *args, **kwargs) :
+        super(Host, self).__init__("host", [hostname], *args, **kwargs)
+
+class Option (Parameter) :
+    """
+        A generic 'option' parameter for a dhcpd.conf file
+    """
+
+    def __init__ (self, name, *args, **kwargs) :
+        """
+            Formatted as a Satement with a name of "option <name>".
+        """
+
+        super(Option, self).__init__("option %s" % name, *args, **kwargs)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/host.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,60 @@
+"""
+    Information about one physica host
+"""
+
+import dhcp
+import bind_conf as bindc
+
+class Interface (object) :
+    """
+        A physical interface for a host
+    """
+
+    def __init__ (self, mac_addr, name=None) :
+        """
+            @param name the short name of the interface (e.g. 'lan' or 'wlan'), or None for no suffix
+            @param mac the physical-layer addr.MAC address
+        """
+
+        self.addr = mac_addr
+        self.name = name
+
+class Host (object) :
+    """
+        A host has a single address/name, an owner, and multiple interfaces
+    """
+
+    def __init__ (self, hostname, address, interfaces) :
+        """
+            @param hostname the short hostname, without the domain name component
+            @param address the addr.IP address
+            @param interfaces a list of zero or more Interface objects
+        """
+
+        self.hostname = hostname
+        self.address = address
+        self.interfaces = interfaces
+    
+    def build_dhcp_hosts (self) :
+        """
+            Build and yield a series of dhcp_conf.Host objects for this host.
+
+            If the host does not have any interfaces defined, this doesn't yield anything
+        """
+        
+        # XXX: do we want to ensure that the host names are unique?
+        
+        for iface in self.interfaces :
+            # the DHCP hostname
+            name = "%s%s" % (self.hostname, ('-%s' % (iface.name)) if iface.name else '')
+            
+            # 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/settings/hosts.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-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, FQDN
-
-# BIND stuff
-domain          = "paivola.fi"
-
-nameservers     = [
-        FQDN("ranssi.paivola.fi"),
-        FQDN("misc1.idler.fi"),
-        FQDN("misc2.idler.fi"),
-        FQDN("srv.marttila.de"),
-    ]
-
-mailservers     = [
-        FQDN("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    = {
-    'domain-name-servers':  IP('194.197.235.145'),
-}
-
-shared_network  = 'PVL'
-subnets         = [
-    Subnet(Network('194.197.235.0/24'), router_idx=1, range=(26, 70), unknown_clients='allow', comment="Public network"),
-    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'),   [ 
-        Interface('00:16:01:37:D1:D2'), 
-        Interface('00:0F:B0:0A:EF:58'),
-    ]),
-]
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/bind.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,70 @@
+#!/usr/bin/env python2.5
+"""
+    Test bind_conf
+"""
+
+import bind_conf as bindc
+import test_conf, addr
+
+import unittest
+
+class TestBINDConf (test_conf._TestConfBase) :
+    def test_comment (self) :
+        self.assert_obj(bindc.Comment("test comment 2"), [ "; test comment 2" ])
+    
+    def assert_str (self, label, value) :
+        self.assertEqual(str(label), value)
+
+    def test_label (self) :
+        self.assert_str(bindc.Label("foo"), "foo")
+
+    def test_origin (self) :
+        self.assert_str(bindc.Origin(), "@")
+
+    def test_fqdn (self) :
+        self.assert_str(bindc.FQDN("foo.com"), "foo.com.")
+
+    def test_interval (self) :
+        self.assert_str(bindc.Interval(12), "12")
+        self.assert_str(bindc.Interval(12, 1), "12s1m")
+        self.assert_str(bindc.Interval(h=2, d=5), "2h5d")
+
+    def assert_rec (self, obj, lines) :
+        """
+            Does a whitespace-insensitive compare of the record's formatted output and the given lines
+        """
+
+        for obj_line, line in zip(obj.fmt_lines(), lines) :
+            obj_line = ' '.join(obj_line.split())
+            self.assertEqual(obj_line, line)
+
+    def test_record (self) :
+        self.assert_rec(bindc.ResourceRecord(None, 'A', addr.IP("1.2.3.4"), cls='TST', ttl=60), [ "60 TST A 1.2.3.4" ])
+        self.assert_rec(bindc.ResourceRecord('foo', 'CNAME', 'blaa'), [ "foo IN CNAME blaa" ])
+        self.assert_rec(bindc.ResourceRecord('bar', 'CNAME', bindc.FQDN('example.com')), [ "bar IN CNAME example.com." ])
+        
+    def test_record_types (self) :
+        self.assert_rec(bindc.SOA(None, 'ns1', 'hostmaster', '2009040200', bindc.Interval(1), 2, 3, 4), [
+                "IN SOA ns1 hostmaster ( 2009040200 1 2 3 4 )"
+            ])
+        self.assert_rec(bindc.A('foo', addr.IP("1.2.3.4")), [ "foo IN A 1.2.3.4" ])
+        self.assert_rec(bindc.AAAA('foo2', addr.IP("2001::5")), [ "foo2 IN AAAA 2001::5" ])
+        self.assert_rec(bindc.CNAME('foo3', bindc.FQDN("foo.com")), [ "foo3 IN CNAME foo.com." ])
+        self.assert_rec(bindc.TXT('test4', "this is some text"), [ "test4 IN TXT \"this is some text\"" ])
+        self.assert_rec(bindc.MX(None, 10, "foo"), [ "IN MX 10 foo" ])
+        self.assert_rec(bindc.NS(None, "ns2"), [ "IN NS ns2" ])
+        self.assert_rec(bindc.PTR(addr.IP("1.2.3.4"), 'bar'), [ "4.3.2.1.in-addr.arpa. IN PTR bar" ])
+    
+    def test_directive (self) :
+        self.assert_obj(bindc.Directive("TEST", "a", None, 2, comments=["hmm..."]), [
+                "; hmm...",
+                "$TEST a 2"
+            ])
+        self.assert_obj(bindc.OriginDirective(bindc.FQDN("foo.com")), [ "$ORIGIN foo.com." ])
+        self.assert_obj(bindc.TTLDirective(bindc.Interval(h=2)), [ "$TTL 2h" ])
+        self.assert_obj(bindc.IncludeDirective("db.foo.com"), [ "$INCLUDE db.foo.com" ])
+        self.assert_obj(bindc.GenerateDirective((1, 10), "dyn${0,2}", 'A', "1.2.3.$"), [ "$GENERATE 1-10 dyn${0,2} A 1.2.3.$" ])
+
+if __name__ == '__main__' :
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/conf.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,16 @@
+"""
+    Test conf.py
+"""
+
+import unittest
+
+class _TestConfBase (unittest.TestCase) :
+    def assert_obj (self, obj, lines) :
+        """
+            Formats the given conf.Object and compares the output against the given lines
+        """
+        
+        for obj_line, line in zip(obj.fmt_lines(), lines) :
+            self.assertEqual(obj_line, line)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/dhcp.py	Sun Jul 12 00:51:08 2009 +0300
@@ -0,0 +1,166 @@
+#!/usr/bin/env python2.5
+"""
+    Test dhcp_conf
+"""
+
+import dhcp_conf as dhcpc
+import test_conf, dhcp, addr
+
+import unittest
+
+class TestDHCPConf (test_conf._TestConfBase) :
+    def assert_stmt (self, stmt, line) :
+        """
+            Formats the given Statement, and compares the output against the given line.
+
+            Note that the dhcpc.Statement doesn't have a working fmt_lines implementation.
+        """
+        
+        self.assertEqual(stmt._fmt_data(), line)
+    
+    def test_comment (self) :
+        self.assert_obj(dhcpc.Comment("foo bar"),                           [ "# foo bar" ])
+
+    def test_section (self) :
+        self.assert_obj(dhcpc.Section(comment="test"),                      [ "# test" ])
+
+        self.assert_obj(dhcpc.Section(params=[
+                dhcpc.Parameter("param0"), None
+            ], comment="foo"), [
+                "# foo",
+                "param0;",
+            ])
+
+    def test_statement (self) :
+        self.assert_stmt(dhcpc.Statement("stmt0"),                           "stmt0")
+        self.assert_stmt(dhcpc.Statement("stmt1", [ "this", "that" ]),       "stmt1 this, that")
+        self.assert_stmt(dhcpc.Statement("stmt2", dhcpc.Literal("...")),     "stmt2 ...")
+        self.assert_stmt(dhcpc.Statement("stmt3", u"quux"),                  "stmt3 quux")
+        self.assert_stmt(dhcpc.Statement("stmt4", "bar"),                    "stmt4 bar")
+        self.assert_stmt(dhcpc.Statement("stmt5", 1),                        "stmt5 1")
+        self.assert_stmt(dhcpc.Statement("stmt6", 1, None, 2),               "stmt6 1 2")
+   
+    def test_literal (self) :
+        self.assert_obj(dhcpc.Literal("///"),                               [ "///" ])
+
+    def test_parameter (self) :
+        self.assert_obj(dhcpc.Parameter("param0", "this", 13, "that"),      [ "param0 this 13 that;" ])
+        self.assert_obj(dhcpc.Parameter("param1", comment="testing"),       [ "# testing", "param1;" ])
+    
+    def test_declaration (self) :
+        self.assert_obj(dhcpc.Declaration("decl0", ["arg0", "arg1"], [
+            dhcpc.Parameter("param0"),
+            None
+        ], [
+            dhcpc.Declaration("decl0.0", params=[
+                dhcpc.Parameter("param0.0.1", "value")
+            ])
+        ], comment="foo"),  [
+            "# foo",
+            "decl0 arg0 arg1 {",
+            "\tparam0;",
+            "\tdecl0.0 {",
+            "\t\tparam0.0.1 value;",
+            "\t}",
+            "}",
+        ])
+    
+    def test_shared_network (self) :
+        self.assert_obj(dhcpc.SharedNetwork("net0", params=[
+            dhcpc.Parameter("param0")
+        ]), [
+            "shared-network net0 {",
+            "\tparam0;",
+            "}"
+        ])
+
+    def test_subnet (self) :
+        self.assert_obj(dhcpc.Subnet(addr.Network("194.197.235.0/24"), params=[
+            dhcpc.Parameter("param0")
+        ]), [
+            "subnet 194.197.235.0 netmask 255.255.255.0 {",
+            "\tparam0;",
+            "}"
+        ])
+
+    def test_group (self) :
+        self.assert_obj(dhcpc.Group(decls=[
+            dhcpc.Declaration("decl0.0", params=[
+                dhcpc.Parameter("param0.0.1", "value")
+            ])
+        ]), [
+            "group {",
+            "\tdecl0.0 {",
+            "\t\tparam0.0.1 value;",
+            "\t}",
+            "}"
+        ])
+    
+    def test_host (self) :
+        self.assert_obj(dhcpc.Host("test-hostname", params=[
+            dhcpc.Parameter("param0")
+        ]), [
+            "host test-hostname {",
+            "\tparam0;",
+            "}"
+        ])
+
+    def test_option (self) :
+        self.assert_obj(dhcpc.Option("foo", "example.com"), [
+            "option foo example.com;",
+        ])
+    
+class TestDHCP (_TestConfObj) :
+    def test_host (self) :
+        self.assert_obj(dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"), comment="foo"), [
+                "# foo",
+                "host testhost {",
+                "\thardware ethernet 12:34:56:78:90:ab;",
+                "\tfixed-address 1.2.3.4;",
+                "}"
+            ])
+
+    def test_subnet (self) :
+        self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), comment="bar"), [
+                "# bar",
+                "subnet 1.2.3.0 netmask 255.255.255.0 {",
+                "\toption routers 1.2.3.1;",
+                "}"
+            ])
+
+        self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), router_idx=10, range=(20, 30), unknown_clients='allow'), [
+                "subnet 1.2.3.0 netmask 255.255.255.0 {",
+                "\toption routers 1.2.3.10;",
+                "\trange 1.2.3.20 1.2.3.30;",
+                "\tallow unknown-clients;",
+                "}"
+            ])
+    
+    def test_config (self) :
+        self.assert_obj(dhcp.Config(
+                settings        = { 'foo-setting': 'someval' }, 
+                options         = { 'bar-opt': ['one', 'two'] },
+                shared_network  = "FOO-NET",
+                subnets         = [
+                        dhcp.Subnet(addr.Network("1.2.3.0/24"))
+                    ],
+                hosts           = [
+                        dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"))
+                    ],
+            ), [
+                "foo-setting someval;",
+                "option bar-opt one, two;",
+                "shared-network FOO-NET {",
+                "\tsubnet 1.2.3.0 netmask 255.255.255.0 {",
+                "\t\toption routers 1.2.3.1;",
+                "\t}",
+                "}",
+                "host testhost {",
+                "\thardware ethernet 12:34:56:78:90:ab;",
+                "\tfixed-address 1.2.3.4;",
+                "}"
+            ])
+
+if __name__ == '__main__' :
+    unittest.main()
+
--- a/test_bind.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-#!/usr/bin/env python2.5
-"""
-    Test bind_conf
-"""
-
-import bind_conf as bindc
-import test_conf, addr
-
-import unittest
-
-class TestBINDConf (test_conf._TestConfBase) :
-    def test_comment (self) :
-        self.assert_obj(bindc.Comment("test comment 2"), [ "; test comment 2" ])
-    
-    def assert_str (self, label, value) :
-        self.assertEqual(str(label), value)
-
-    def test_label (self) :
-        self.assert_str(bindc.Label("foo"), "foo")
-
-    def test_origin (self) :
-        self.assert_str(bindc.Origin(), "@")
-
-    def test_fqdn (self) :
-        self.assert_str(bindc.FQDN("foo.com"), "foo.com.")
-
-    def test_interval (self) :
-        self.assert_str(bindc.Interval(12), "12")
-        self.assert_str(bindc.Interval(12, 1), "12s1m")
-        self.assert_str(bindc.Interval(h=2, d=5), "2h5d")
-
-    def assert_rec (self, obj, lines) :
-        """
-            Does a whitespace-insensitive compare of the record's formatted output and the given lines
-        """
-
-        for obj_line, line in zip(obj.fmt_lines(), lines) :
-            obj_line = ' '.join(obj_line.split())
-            self.assertEqual(obj_line, line)
-
-    def test_record (self) :
-        self.assert_rec(bindc.ResourceRecord(None, 'A', addr.IP("1.2.3.4"), cls='TST', ttl=60), [ "60 TST A 1.2.3.4" ])
-        self.assert_rec(bindc.ResourceRecord('foo', 'CNAME', 'blaa'), [ "foo IN CNAME blaa" ])
-        self.assert_rec(bindc.ResourceRecord('bar', 'CNAME', bindc.FQDN('example.com')), [ "bar IN CNAME example.com." ])
-        
-    def test_record_types (self) :
-        self.assert_rec(bindc.SOA(None, 'ns1', 'hostmaster', '2009040200', bindc.Interval(1), 2, 3, 4), [
-                "IN SOA ns1 hostmaster ( 2009040200 1 2 3 4 )"
-            ])
-        self.assert_rec(bindc.A('foo', addr.IP("1.2.3.4")), [ "foo IN A 1.2.3.4" ])
-        self.assert_rec(bindc.AAAA('foo2', addr.IP("2001::5")), [ "foo2 IN AAAA 2001::5" ])
-        self.assert_rec(bindc.CNAME('foo3', bindc.FQDN("foo.com")), [ "foo3 IN CNAME foo.com." ])
-        self.assert_rec(bindc.TXT('test4', "this is some text"), [ "test4 IN TXT \"this is some text\"" ])
-        self.assert_rec(bindc.MX(None, 10, "foo"), [ "IN MX 10 foo" ])
-        self.assert_rec(bindc.NS(None, "ns2"), [ "IN NS ns2" ])
-        self.assert_rec(bindc.PTR(addr.IP("1.2.3.4"), 'bar'), [ "4.3.2.1.in-addr.arpa. IN PTR bar" ])
-    
-    def test_directive (self) :
-        self.assert_obj(bindc.Directive("TEST", "a", None, 2, comments=["hmm..."]), [
-                "; hmm...",
-                "$TEST a 2"
-            ])
-        self.assert_obj(bindc.OriginDirective(bindc.FQDN("foo.com")), [ "$ORIGIN foo.com." ])
-        self.assert_obj(bindc.TTLDirective(bindc.Interval(h=2)), [ "$TTL 2h" ])
-        self.assert_obj(bindc.IncludeDirective("db.foo.com"), [ "$INCLUDE db.foo.com" ])
-        self.assert_obj(bindc.GenerateDirective((1, 10), "dyn${0,2}", 'A', "1.2.3.$"), [ "$GENERATE 1-10 dyn${0,2} A 1.2.3.$" ])
-
-if __name__ == '__main__' :
-    unittest.main()
-
--- a/test_conf.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-"""
-    Test conf.py
-"""
-
-import unittest
-
-class _TestConfBase (unittest.TestCase) :
-    def assert_obj (self, obj, lines) :
-        """
-            Formats the given conf.Object and compares the output against the given lines
-        """
-        
-        for obj_line, line in zip(obj.fmt_lines(), lines) :
-            self.assertEqual(obj_line, line)
-
-
--- a/test_dhcp.py	Sun Jul 12 00:43:36 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +0,0 @@
-#!/usr/bin/env python2.5
-"""
-    Test dhcp_conf
-"""
-
-import dhcp_conf as dhcpc
-import test_conf, dhcp, addr
-
-import unittest
-
-class TestDHCPConf (test_conf._TestConfBase) :
-    def assert_stmt (self, stmt, line) :
-        """
-            Formats the given Statement, and compares the output against the given line.
-
-            Note that the dhcpc.Statement doesn't have a working fmt_lines implementation.
-        """
-        
-        self.assertEqual(stmt._fmt_data(), line)
-    
-    def test_comment (self) :
-        self.assert_obj(dhcpc.Comment("foo bar"),                           [ "# foo bar" ])
-
-    def test_section (self) :
-        self.assert_obj(dhcpc.Section(comment="test"),                      [ "# test" ])
-
-        self.assert_obj(dhcpc.Section(params=[
-                dhcpc.Parameter("param0"), None
-            ], comment="foo"), [
-                "# foo",
-                "param0;",
-            ])
-
-    def test_statement (self) :
-        self.assert_stmt(dhcpc.Statement("stmt0"),                           "stmt0")
-        self.assert_stmt(dhcpc.Statement("stmt1", [ "this", "that" ]),       "stmt1 this, that")
-        self.assert_stmt(dhcpc.Statement("stmt2", dhcpc.Literal("...")),     "stmt2 ...")
-        self.assert_stmt(dhcpc.Statement("stmt3", u"quux"),                  "stmt3 quux")
-        self.assert_stmt(dhcpc.Statement("stmt4", "bar"),                    "stmt4 bar")
-        self.assert_stmt(dhcpc.Statement("stmt5", 1),                        "stmt5 1")
-        self.assert_stmt(dhcpc.Statement("stmt6", 1, None, 2),               "stmt6 1 2")
-   
-    def test_literal (self) :
-        self.assert_obj(dhcpc.Literal("///"),                               [ "///" ])
-
-    def test_parameter (self) :
-        self.assert_obj(dhcpc.Parameter("param0", "this", 13, "that"),      [ "param0 this 13 that;" ])
-        self.assert_obj(dhcpc.Parameter("param1", comment="testing"),       [ "# testing", "param1;" ])
-    
-    def test_declaration (self) :
-        self.assert_obj(dhcpc.Declaration("decl0", ["arg0", "arg1"], [
-            dhcpc.Parameter("param0"),
-            None
-        ], [
-            dhcpc.Declaration("decl0.0", params=[
-                dhcpc.Parameter("param0.0.1", "value")
-            ])
-        ], comment="foo"),  [
-            "# foo",
-            "decl0 arg0 arg1 {",
-            "\tparam0;",
-            "\tdecl0.0 {",
-            "\t\tparam0.0.1 value;",
-            "\t}",
-            "}",
-        ])
-    
-    def test_shared_network (self) :
-        self.assert_obj(dhcpc.SharedNetwork("net0", params=[
-            dhcpc.Parameter("param0")
-        ]), [
-            "shared-network net0 {",
-            "\tparam0;",
-            "}"
-        ])
-
-    def test_subnet (self) :
-        self.assert_obj(dhcpc.Subnet(addr.Network("194.197.235.0/24"), params=[
-            dhcpc.Parameter("param0")
-        ]), [
-            "subnet 194.197.235.0 netmask 255.255.255.0 {",
-            "\tparam0;",
-            "}"
-        ])
-
-    def test_group (self) :
-        self.assert_obj(dhcpc.Group(decls=[
-            dhcpc.Declaration("decl0.0", params=[
-                dhcpc.Parameter("param0.0.1", "value")
-            ])
-        ]), [
-            "group {",
-            "\tdecl0.0 {",
-            "\t\tparam0.0.1 value;",
-            "\t}",
-            "}"
-        ])
-    
-    def test_host (self) :
-        self.assert_obj(dhcpc.Host("test-hostname", params=[
-            dhcpc.Parameter("param0")
-        ]), [
-            "host test-hostname {",
-            "\tparam0;",
-            "}"
-        ])
-
-    def test_option (self) :
-        self.assert_obj(dhcpc.Option("foo", "example.com"), [
-            "option foo example.com;",
-        ])
-    
-class TestDHCP (_TestConfObj) :
-    def test_host (self) :
-        self.assert_obj(dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"), comment="foo"), [
-                "# foo",
-                "host testhost {",
-                "\thardware ethernet 12:34:56:78:90:ab;",
-                "\tfixed-address 1.2.3.4;",
-                "}"
-            ])
-
-    def test_subnet (self) :
-        self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), comment="bar"), [
-                "# bar",
-                "subnet 1.2.3.0 netmask 255.255.255.0 {",
-                "\toption routers 1.2.3.1;",
-                "}"
-            ])
-
-        self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), router_idx=10, range=(20, 30), unknown_clients='allow'), [
-                "subnet 1.2.3.0 netmask 255.255.255.0 {",
-                "\toption routers 1.2.3.10;",
-                "\trange 1.2.3.20 1.2.3.30;",
-                "\tallow unknown-clients;",
-                "}"
-            ])
-    
-    def test_config (self) :
-        self.assert_obj(dhcp.Config(
-                settings        = { 'foo-setting': 'someval' }, 
-                options         = { 'bar-opt': ['one', 'two'] },
-                shared_network  = "FOO-NET",
-                subnets         = [
-                        dhcp.Subnet(addr.Network("1.2.3.0/24"))
-                    ],
-                hosts           = [
-                        dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"))
-                    ],
-            ), [
-                "foo-setting someval;",
-                "option bar-opt one, two;",
-                "shared-network FOO-NET {",
-                "\tsubnet 1.2.3.0 netmask 255.255.255.0 {",
-                "\t\toption routers 1.2.3.1;",
-                "\t}",
-                "}",
-                "host testhost {",
-                "\thardware ethernet 12:34:56:78:90:ab;",
-                "\tfixed-address 1.2.3.4;",
-                "}"
-            ])
-
-if __name__ == '__main__' :
-    unittest.main()
-