# HG changeset patch # User Tero Marttila # Date 1238692758 -10800 # Node ID 2223ade4f25939eb343579a2bac5cadf39c82d7a # Parent 257003279747a1cd8139c0df5407dc226a350e7d continue the overengineering effort, we are now able to generate dhcpd.conf files diff -r 257003279747 -r 2223ade4f259 .hgignore --- /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]$ diff -r 257003279747 -r 2223ade4f259 addr.py --- 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 + diff -r 257003279747 -r 2223ade4f259 conf.py --- 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) - diff -r 257003279747 -r 2223ade4f259 conf_dhcp.py --- 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: - [ [ ... ] ] ";" - """ - - 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. - - [ [ ... ] ] { - [ ] - [ ] - } - - """ - - 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 { - [ 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 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 { - [ 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 ". - """ - - super(Option, self).__init__("option %s" % name, *args) - diff -r 257003279747 -r 2223ade4f259 data.py --- /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 + diff -r 257003279747 -r 2223ade4f259 dhcp.py --- /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) + ]) + diff -r 257003279747 -r 2223ade4f259 dhcp_conf.py --- /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: + [ [ ... ] ] ";" + + 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. + + [ [ ... ] ] { + [
] + } + + """ + + 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 { + [ 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 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 { + [ 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 ". + """ + + super(Option, self).__init__("option %s" % name, *args) + diff -r 257003279747 -r 2223ade4f259 host.py --- /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) + diff -r 257003279747 -r 2223ade4f259 main.py --- /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) + diff -r 257003279747 -r 2223ade4f259 settings/hosts.py --- /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'), + ]), +] + diff -r 257003279747 -r 2223ade4f259 test_dhcp.py --- 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")