continue the overengineering effort, we are now able to generate dhcpd.conf files
authorTero Marttila <terom@fixme.fi>
Thu, 02 Apr 2009 20:19:18 +0300
changeset 1 2223ade4f259
parent 0 257003279747
child 2 e66102ab7048
continue the overengineering effort, we are now able to generate dhcpd.conf files
.hgignore
__init__.py
addr.py
conf.py
conf_dhcp.py
data.py
dhcp.py
dhcp_conf.py
host.py
main.py
settings/__init__.py
settings/hosts.py
test_dhcp.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,4 @@
+syntax: regexp
+
+\.pyc$
+\.[^/]+\.sw[op]$
--- a/addr.py	Thu Apr 02 17:47:43 2009 +0300
+++ b/addr.py	Thu Apr 02 20:19:18 2009 +0300
@@ -28,3 +28,19 @@
 
         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/conf.py	Thu Apr 02 17:47:43 2009 +0300
+++ b/conf.py	Thu Apr 02 20:19:18 2009 +0300
@@ -2,9 +2,9 @@
     Generic configuration file output
 """
 
-import os, tempfile, shutil
+import os, os.path, tempfile, shutil
 
-class Object (object) :
+class ConfObject (object) :
     """
         An object that can be written to a ConfFile, as multiple lines of text.
     """
@@ -16,11 +16,11 @@
         
         abstract
 
-class File (object) :
+class File (ConfObject) :
     """
         A single configuration file on the filesystem.
 
-        Configuration files are 
+        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) :
@@ -38,17 +38,17 @@
         self.backup_suffix = backup_suffix
         self.mode = mode
     
-    def write_file (self, file, objects) :
+    def write_file (self, file) :
         """
-            Write out the given config objects into the given file
+            Write out this config's stuff into the given file
         """
 
         writer = Writer(file)
-        writer.write_objs(objects)
+        writer.write_obj(self)
 
-    def write (self, objects) :
+    def write (self) :
         """
-            Write out a new config file with the given series of objects using the following procedure:
+            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
@@ -70,15 +70,14 @@
         os.chmod(tmp_path, self.mode)
 
         # write it
-        self.write_file(tmp_file, objects)
+        self.write_file(tmp_file)
 
         # close it
         tmp_file.close()
-        del writer
-        del tmp_file
         
         # move the old file out of the way
-        os.rename(self.path, self.path + self.backup_suffix)
+        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)
@@ -95,20 +94,26 @@
 
         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.file.writelines(obj.fmt_lines())
+        self.write_lines(obj.fmt_lines())
 
-    def write_objs (self, objs) :
-        """
-            Write a series of objects to the file
-        """
-        
-        # just write each in turn
-        for obj in objs :
-            self.write_obj(obj)
-
--- a/conf_dhcp.py	Thu Apr 02 17:47:43 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-"""
-    Configuration file output for the ISC DHCP server
-"""
-
-import conf
-
-import itertools
-
-class ConfDHCP (conf.File) :
-    def __init__ (self, name="dhcpd.conf", path="/etc/dhcp3/dhcpd.conf") :
-        """
-            Initialize the dhcpd config file, but don't open it yet
-
-            @see conf.ConfFile.__init__
-        """
-        
-        super(ConfDHCP, self).__init__(name, path)
-
-class Statement (conf.Object) :
-    """
-      A statement is a single line in the config file
-    """
-
-    def __init__ (self, name, *args) :
-        """
-            The statement will be formatted like this:
-                <name> [ <arg> [ ... ] ] ";"
-        """
-
-        self.name = name
-        self.args = args
-    
-    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
-        """
-
-        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.
-    """
-    
-    def fmt_lines (self) :
-        """
-            Yields a single ;-terminated line
-        """
-
-        yield "%s;" % self._fmt_data()
-
-class Declaration (Statement) :
-    """
-        A declaration begins like a statement (with name and args), but then contains a block of any number of
-        parameters followed by any number of nested declarations.
-
-        <name> [ <args> [ ... ] ] {
-            [ <parameters> ]
-            [ <declarations> ]
-        }
-        
-    """
-
-    def __init__ (self, name, args=[], params=[], decls=[]) :
-        """
-            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
-        Statement.__init__(self, name, *args)
-
-        # store the iterables
-        self.params = params
-        self.decls = decls
-
-    def fmt_lines (self) :
-        """
-            Yields a header line, a series of indented body lines, and the footer line
-        """
-        
-        # the header to open the block
-        yield "%s {" % self._fmt_data()
-        
-        # then output each content line
-        for stmt in itertools.chain(self.params, self.decls) :
-            # ..indented
-            for line in stmt.fmt_lines() :
-                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, params=[], decls=[]) :
-        """
-            @param name the name of the shared-subnet
-            @param params optional parameters
-            @param decls the iterable of subnets or other declarations in the shared network
-        """
-
-        super(SharedNetwork, self).__init__("shared-network", [name], params, decls)
-
-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, params=[], decls=[]) :
-        """
-            @param network the addr.Network for the subnet
-            @param params optional parameters
-            @param decls optional decls, e.g. subnets
-        """
-
-        super(Subnet, self).__init__("subnet", [network.net(), "netmask", network.netmask()], params, decls)
-
-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, params=[], decls=[]) :
-        super(Group, self).__init__("group", [], params, decls)
-
-
-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, params=[], decls=[]) :
-        super(Host, self).__init__("host", [hostname], params, decls)
-
-class Option (Parameter) :
-    """
-        A generic 'option' parameter for a dhcpd.conf file
-    """
-
-    def __init__ (self, name, *args) :
-        """
-            Formatted as a Satement with a name of "option <name>".
-        """
-
-        super(Option, self).__init__("option %s" % name, *args)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data.py	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,23 @@
+"""
+    Functions to load data from various sources
+"""
+
+import imp
+
+def load_py (path) :
+    """
+        Load a python file from the given filesystem path, returning the module itself
+    """
+
+    # XXX: what name to use?
+    name = "hosts"
+    
+    # find the module
+    file, pathname, info = imp.find_module(name, [path])
+
+    # load it
+    module = imp.load_module(name, file, pathname, info)
+
+    # ok
+    return module
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dhcp.py	Thu Apr 02 20:19:18 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
+    ) :
+        """
+            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)
+
+        # 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) :
+        """
+            @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,
+        ])
+
+
+class Host (dhcpc.Host) :
+    """
+        A host declaration with a hardware address and a IP address
+    """
+
+    def __init__ (self, hostname, mac_addr, ip_addr) :
+        super(Host, self).__init__(hostname, params=[
+            dhcpc.Parameter("hardware ethernet", mac_addr),
+            dhcpc.Parameter("fixed-address", ip_addr)
+        ])
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dhcp_conf.py	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,274 @@
+"""
+    Configuration file output for the ISC DHCP server
+"""
+
+import conf
+
+import itertools
+
+class Section (conf.ConfObject) :
+    """
+        A section holds a list of params and a list of decls
+    """
+
+    def __init__ (self, params=None, decls=None) :
+        """
+            If params/decls are given, those are the used as the initial contents of this section
+        """
+
+        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 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) :
+        """
+            Initialize the dhcpd config file, but don't open it yet.
+        """
+        
+        conf.File.__init__(self, name, path)
+        Section.__init__(self, params, decls)
+
+class Statement (conf.ConfObject) :
+    """
+        A statement is a single line in the config file
+    """
+
+    def __init__ (self, name, *args) :
+        """
+            The statement will be formatted like this:
+                <name> [ <arg> [ ... ] ] ";"
+
+            Arguments given as None will be ignored.
+        """
+
+        self.name = name
+        self.args = args
+    
+    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 a is not None)) 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.
+    """
+    
+    def fmt_lines (self) :
+        """
+            Yields a single ;-terminated 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) :
+        """
+            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)
+
+    def fmt_lines (self) :
+        """
+            Yields a header line, a series of indented body lines, and the footer 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, params=[], decls=[]) :
+        """
+            @param name the name of the shared-subnet
+            @param params optional parameters
+            @param decls the iterable of subnets or other declarations in the shared network
+        """
+
+        super(SharedNetwork, self).__init__("shared-network", [name], params, decls)
+
+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, params=None, decls=None) :
+        """
+            @param network the addr.Network for the subnet
+            @param params optional parameters
+            @param decls optional decls, e.g. subnets
+        """
+
+        super(Subnet, self).__init__("subnet", [network.net(), "netmask", network.netmask()], params, decls)
+
+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, params=None, decls=None) :
+        super(Group, self).__init__("group", [], params, decls)
+
+
+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, params=None, decls=None) :
+        super(Host, self).__init__("host", [hostname], params, decls)
+
+class Option (Parameter) :
+    """
+        A generic 'option' parameter for a dhcpd.conf file
+    """
+
+    def __init__ (self, name, *args) :
+        """
+            Formatted as a Satement with a name of "option <name>".
+        """
+
+        super(Option, self).__init__("option %s" % name, *args)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/host.py	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,52 @@
+"""
+    Information about one physica host
+"""
+
+import dhcp
+
+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)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.py	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,63 @@
+#!/usr/bin/env python2.5
+
+import data, dhcp_conf, dhcp
+
+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')
+    
+    # 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 f 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 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(data_file)
+    
+    # write out the config files
+    write_dhcp(options, data_module)
+
+if __name__ == '__main__' :
+    from sys import argv
+
+    main(argv)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/settings/hosts.py	Thu Apr 02 20:19:18 2009 +0300
@@ -0,0 +1,28 @@
+from addr import IP, Network
+from host import Interface, Host
+from dhcp import Subnet
+
+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'),
+    Subnet(Network('192.168.0.0/23'),   router_idx=1, unknown_clients='deny'),
+]
+
+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/test_dhcp.py	Thu Apr 02 17:47:43 2009 +0300
+++ b/test_dhcp.py	Thu Apr 02 20:19:18 2009 +0300
@@ -1,8 +1,9 @@
+#!/usr/bin/env python2.5
 """
     Test conf_dhcp
 """
 
-import conf_dhcp as dhcpc, conf, addr
+import dhcp_conf as dhcpc, conf, addr
 
 import unittest
 from cStringIO import StringIO
@@ -17,11 +18,12 @@
     
     def test_statement (self) :
         self.assert_stmt(dhcpc.Statement("stmt0"),                           "stmt0")
-        self.assert_stmt(dhcpc.Statement("stmt3", [ "this", "that" ]),       "stmt3 this, that")
-        self.assert_stmt(dhcpc.Statement("stmt4", dhcpc.Literal("...")),     "stmt4 ...")
-        self.assert_stmt(dhcpc.Statement("stmt1", u"quux"),                  "stmt1 quux")
-        self.assert_stmt(dhcpc.Statement("stmt1", "bar"),                    "stmt1 bar")
-        self.assert_stmt(dhcpc.Statement("stmt2", 1),                        "stmt2 1")
+        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 assert_obj (self, obj, lines) :
         """
@@ -38,7 +40,8 @@
     
     def test_declaration (self) :
         self.assert_obj(dhcpc.Declaration("decl0", ["arg0", "arg1"], [
-            dhcpc.Parameter("param0")
+            dhcpc.Parameter("param0"),
+            None
         ], [
             dhcpc.Declaration("decl0.0", params=[
                 dhcpc.Parameter("param0.0.1", "value")