reorganize modules into packages
authorTero Marttila <terom@fixme.fi>
Sun, 12 Jul 2009 02:14:34 +0300
changeset 9 2156906bfbf1
parent 8 46d36bc33086
child 10 65d91f6a2d2a
reorganize modules into packages
pvl/config/bind.py
pvl/config/bind/__init__.py
pvl/config/bind/model.py
pvl/config/bind/zone.py
pvl/config/bind_conf.py
pvl/config/dhcp.py
pvl/config/dhcp_conf.py
pvl/config/dhcpd/__init__.py
pvl/config/dhcpd/conf.py
pvl/config/dhcpd/model.py
test/bind.py
--- a/pvl/config/bind.py	Sun Jul 12 02:14:08 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/bind/model.py	Sun Jul 12 02:14:34 2009 +0300
@@ -0,0 +1,188 @@
+"""
+    High-level BIND stuff
+"""
+
+from __future__ import with_statement
+
+from . import zone
+
+import os.path, datetime
+
+DEFAULT_TTL = zone.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 (zone.Zone) :
+    """
+        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(zone.TTLDirective(settings.ttl))
+
+        # the SOA record
+        self.add_record(zone.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(zone.NS(None, label))
+
+        # the MX records
+        for pref, label in mailservers :
+            self.add_record(zone.MX(None, pref, label))
+        
+        # include?
+        if include :
+            self.add_directive(zone.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/zone.py	Sun Jul 12 02:14:34 2009 +0300
@@ -0,0 +1,355 @@
+"""
+    Configuration file output for the ISC DNS server: BIND
+"""
+
+from .. import file
+
+class Item (file.Item) :
+    """
+        Item that knows how to format comments
+    """
+
+    COMMENT_PREFIX = ';'
+
+class Zone (Item, file.Contents) :
+    """
+        A zone containing a bunch of directives, resource records and comments
+    """
+
+    def __init__ (self, 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
+        """
+        
+        Item.__init__(self, [comment])
+        file.Contents.__init__(self)
+
+        if ttl :
+            self.add(TTLDirective(ttl))
+    
+class Comment (Item) :
+    """
+        A comment, is, well, a comment :)
+    """
+
+    def __init__ (self, comment) :
+        """
+            @param comment the comment string
+        """
+
+        Item.__init__(self, [comment])
+    
+    # only need to iterate over our comments
+    iter_lines = Item.iter_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 (Item) :
+    """
+        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 iter_lines (self) :
+        """
+            Just format the lines, eh
+        """
+        
+        # prefix comments
+        for line in self.iter_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...
+        """
+        
+        # XXX: escaping?
+        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 (Item) :
+    """
+        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 iter_lines (self) :
+        # prefix comments
+        for line in self.iter_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/pvl/config/bind_conf.py	Sun Jul 12 02:14:08 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/pvl/config/dhcp.py	Sun Jul 12 02:14:08 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/pvl/config/dhcp_conf.py	Sun Jul 12 02:14:08 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)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/config/dhcpd/conf.py	Sun Jul 12 02:14:34 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/dhcpd/model.py	Sun Jul 12 02:14:34 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)
+
--- a/test/bind.py	Sun Jul 12 02:14:08 2009 +0300
+++ b/test/bind.py	Sun Jul 12 02:14:34 2009 +0300
@@ -3,8 +3,9 @@
     Test bind_conf
 """
 
-import bind_conf as bindc
-import test_conf, addr
+from pvl.config.bind import zone
+from pvl.config import addr
+from . import conf
 
 import unittest