# HG changeset patch # User Tero Marttila # Date 1247349068 -10800 # Node ID 0f9cae2d71479d0c0573d0e46f4fefd7170446e6 # Parent 57e8168ba8c48932a62af978f0601a5a9115f537 restructure, break diff -r 57e8168ba8c4 -r 0f9cae2d7147 addr.py --- a/addr.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -""" - Used to define IP-address/subnet stuff -""" - -import IPy - -class IP (IPy.IP, object) : - """ - A literal IPv4 address - """ - - def __init__ (self, address) : - """ - Parse the given literal IP address in "a.b.c.d" form - """ - - super(IP, self).__init__(address) - - def is_v4 (self) : - """ - Returns True if the address is an IPv4 address - """ - - return self.version() == 4 - - def is_v6 (self) : - """ - Returns True if the address is an IPv6 address - """ - - return self.version() == 6 - -class Network (IPy.IP, object) : - """ - An IPv4 network (subnet) - """ - - def __init__ (self, prefix) : - """ - Parse the given prefix in "a.b.c.d/l" form - """ - - super(Network, self).__init__(prefix) - -class MAC (object) : - """ - A mac address - """ - - def __init__ (self, mac) : - """ - Parse the given MAC address in "aa:bb:cc:dd:ee:ff" form - """ - - # XXX: validate - self.mac = mac - - def __str__ (self) : - return self.mac - diff -r 57e8168ba8c4 -r 0f9cae2d7147 bind.py --- a/bind.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,187 +0,0 @@ -""" - High-level BIND stuff -""" - -from __future__ import with_statement - -import bind_conf as bindc - -import os.path, datetime - -DEFAULT_TTL = bindc.Interval(3600) - -class Settings (object) : - """ - A set of basic settings for a zone, mostly default TTL/refresh/retry/expire/minimum settings - """ - - def __init__ (self, ttl, hostmaster, refresh, retry, expire, minimum) : - self.ttl = ttl - self.hostmaster = hostmaster - self.refresh = refresh - self.retry = retry - self.expire = expire - self.minimum = minimum - -class AutoSerial (object) : - """ - Automatically generate the next serial to use by loading it from a file. - - The generated serials are in YYYYMMDDXX format. - """ - - def __init__ (self, path) : - """ - Load the current serial - - @param path the path to the serial file - """ - - # store - self.path = path - - # load it - # XXX: locking - serial = self.read() - - # current date - today = datetime.date.today() - - # parse it - if serial : - date, code = self.parse(serial) - - else : - date, code = today, 0 - - # increment it - date, code = self.next(date, code) - - # format it - self._serial = self.build(date, code) - - # write it out - self.write(self._serial) - - def parse (self, serial) : - """ - Parse the given serial into a (datetime.date, code) format - """ - - # build it into a date - date = datetime.date( - year = int(serial[0:4]), - month = int(serial[4:6]), - day = int(serial[6:8]) - ) - - code = int(serial[8:]) - - return date, code - - def next (self, date, code) : - """ - Return the next valid serial following the given one - """ - - # current date - today = datetime.date.today() - - # now to increment? - if date < today : - # jump to today's first serial - date = today - code = 0 - - else : - # today or overflowed into the future, just increment the code - code += 1 - - # code overflowed into next day? - if code > 99 : - date += datetime.timedelta(days=1) - code = 0 - - # ok - return date, code - - def build (self, date, code) : - """ - Build a serial code the given date/code - """ - - assert 0 <= code <= 99 - - return "%s%02d" % (date.strftime("%Y%m%d"), code) - - def read (self) : - """ - Read the current serial, returning it, or None, if not found... - """ - - # if it doesn't exist, default - if not os.path.exists(self.path) : - return None - - # read it - with open(self.path) as fh : - return fh.read().strip() - - def write (self, serial) : - """ - Write a new serial - """ - - with open(self.path, 'w') as fh : - fh.write("%s\n" % (serial, )) - - def serial (self) : - """ - Return a new, unused serial code (before __init__) - """ - - return self._serial - -class Domain (bindc.ZoneFile) : - """ - A domain has a skeleton of stuff defined, but the rest is $INCLUDE'd from elsewhere, which is useful for - multi-domain setups where the domains are mostly similar - """ - - def __init__ (self, domain, path, nameservers, mailservers, serial, settings, include=None, objs=None) : - """ - @param domain the domain name - @param path the path to the zone file - @param nameservers list of nameservers as labels - @param mailservers list of (pref, label) tuples for MX records - @param serial the serial code to use - @param settings the TTL/SOA settings to use - @param include the optional zonefile to include - @param objs the optional other objects to add to the zonefile - """ - - super(Domain, self).__init__(domain, path) - - # the default TTL - self.add_directive(bindc.TTLDirective(settings.ttl)) - - # the SOA record - self.add_record(bindc.SOA(None, nameservers[0], - settings.hostmaster, serial, settings.refresh, settings.retry, settings.expire, settings.minimum - )) - - # the NS records - for label in nameservers : - self.add_record(bindc.NS(None, label)) - - # the MX records - for pref, label in mailservers : - self.add_record(bindc.MX(None, pref, label)) - - # include? - if include : - self.add_directive(bindc.IncludeDirective(include)) - - if objs : - for obj in objs : - self.add_obj(obj) diff -r 57e8168ba8c4 -r 0f9cae2d7147 bind_conf.py --- a/bind_conf.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,395 +0,0 @@ -""" - Configuration file output for the ISC DNS server: BIND -""" - -import conf - -class Object (conf.ConfObject) : - """ - Our own version of ConfObject that knows how to format comments - """ - - def _fmt_comments (self) : - """ - Format comments using the standard format: - ";" - """ - - 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) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 conf.py --- a/conf.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,136 +0,0 @@ -""" - Generic configuration file output -""" - -import os, os.path, tempfile, shutil - -class ConfObject (object) : - """ - An object that can be written to a ConfFile, as multiple lines of text. - """ - - def __init__ (self, comments=None) : - """ - Initialize with the given list of comments. Comments that are None should be ignore. - """ - - # init the comments list - self.comments = comments or [] - - def add_comment (self, comment) : - """ - Add a comment to be rendered in the output. - """ - - # add it - self.comments.append(comment) - - def fmt_lines (self) : - """ - Yield a series of lines to be output in the file - """ - - abstract - -class File (ConfObject) : - """ - A single configuration file on the filesystem. - - Configuration files are themselves ConfObject's, although this must be implemented in the inheriting class. - """ - - def __init__ (self, name, path, backup_suffix='.bak', mode=0644) : - """ - Initialize the config file, but don't open it yet - - @param name the human-readable friendly name for the config file - @param path the full filesystem path for the file - @param backup_suffix rename the old file by adding this suffix when replacing it - @param mode the permission bits for the new file - """ - - self.name = name - self.path = path - self.backup_suffix = backup_suffix - self.mode = mode - - def write_file (self, file) : - """ - Write out this config's stuff into the given file - """ - - writer = Writer(file) - writer.write_obj(self) - - def write (self) : - """ - Write out a new config file with this config's stuff using the following procedure: - - * lock the real file - * open a new temporary file, and write out the objects into that, closing it - * move the real file out of the way - * move the temporary file into the real file's place - * unlock the real file - """ - - # XXX: how to aquire the lock? - # XXX: this needs checking over - - # open the new temporary file - # XXX: ensure fd is closed - tmp_fd, tmp_path = tempfile.mkstemp(prefix=self.name) - - # ...as a file - tmp_file = os.fdopen(tmp_fd, "w") - - # fix the permissions - os.chmod(tmp_path, self.mode) - - # write it - self.write_file(tmp_file) - - # close it - tmp_file.close() - - # move the old file out of the way - if os.path.exists(self.path) : - os.rename(self.path, self.path + self.backup_suffix) - - # move the new file in - shutil.move(tmp_path, self.path) - -class Writer (object) : - """ - A conf.Writer is used to write out a new conf.File (as a temporary file) - """ - - def __init__ (self, file) : - """ - @param file the temporary file object - """ - - self.file = file - - def write_line (self, line) : - """ - Write a single line to the file - """ - - self.file.write("%s\n" % (line, )) - - def write_lines (self, lines) : - """ - Write a series of lines into the file - """ - - for line in lines : - self.write_line(line) - - def write_obj (self, obj) : - """ - Write a single object to the file - """ - - # just write out all the lines - self.write_lines(obj.fmt_lines()) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 data.py --- a/data.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" - Functions to load data from various sources -""" - -import imp - -def load_py (name, path) : - """ - Load a python file from the given filesystem path, returning the module itself. - - The "name" of the module must be given, it should be something sane and unique... - """ - - # just load it and return - return imp.load_source(name, path) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 dhcp.py --- a/dhcp.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -""" - Higher-level DHCP config structure model -""" - -import dhcp_conf as dhcpc - -class Config (dhcpc.ConfFile) : - """ - A full configuration file - """ - - def __init__ (self, name=dhcpc.ConfFile.DEFAULT_NAME, path=dhcpc.ConfFile.DEFAULT_PATH, - settings=None, options=None, shared_network=False, subnets=None, hosts=None, comment=None - ) : - """ - Create a full configuration file for the given settings: - - settings: a { name: value } mappping of general settings to set - options: a { opt_name: opt_value } mapping of options to set - shared_network: define the subnets as a shared network of the given name - subnets: an iterable of Subnet's to define - hosts: an iterable of Host's to define - - """ - - dhcpc.ConfFile.__init__(self, name, path, comment=comment) - - # define global settings - if settings : - self.add_params(dhcpc.Parameter(setting, value) for setting, value in settings.iteritems()) - - # define global options - if options : - self.add_params(dhcpc.Option(option, value) for option, value in options.iteritems()) - - # the shared-network section, or a series of subnets - if shared_network : - self.add_decl(dhcpc.SharedNetwork(shared_network, decls=subnets)) - - elif subnets : - self.add_decls(subnets) - - # hosts section - if hosts : - self.add_decls(hosts) - -class Subnet (dhcpc.Subnet) : - """ - A subnet declaration with a router, and optionally a dynamic address pool, and allow/deny unknown clients - """ - - def __init__ (self, subnet, router_idx=1, range=None, unknown_clients=None, comment=None) : - """ - @param subnet the addr.IP representing the subnet - @param router_idx the subnet[index] of the default gateway - @param range optional (from_idx, to_idx) to define a dhcp pool - @param unknown_clients optional 'allow'/'deny' to set a policy for unknown clients - """ - - # validate - if unknown_clients : - assert unknown_clients in ('allow', 'deny') - - super(Subnet, self).__init__(subnet, params=[ - dhcpc.Option("routers", subnet[router_idx]), - dhcpc.Parameter("range", subnet[range[0]], subnet[range[1]]) if range else None, - dhcpc.Parameter(unknown_clients, "unknown-clients") if unknown_clients else None, - ], comment=comment) - - -class Host (dhcpc.Host) : - """ - A host declaration with a hardware address and a IP address - """ - - def __init__ (self, hostname, mac_addr, ip_addr, comment=None) : - super(Host, self).__init__(hostname, params=[ - dhcpc.Parameter("hardware ethernet", mac_addr), - dhcpc.Parameter("fixed-address", ip_addr) - ], comment=comment) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 dhcp_conf.py --- a/dhcp_conf.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,341 +0,0 @@ -""" - Configuration file output for the ISC DHCP server -""" - -import conf - -import itertools - -class Object (conf.ConfObject) : - """ - Our version of ConfObject - """ - - def _fmt_comments (self) : - """ - Format our comment lines - """ - - for comment in self.comments : - if comment is not None : - yield "# %s" % (comment, ) - - -class Comment (Object) : - """ - A comment, is, well, a comment :) - """ - - def __init__ (self, comment) : - """ - @param comment the comment string - """ - - Object.__init__(self, [comment]) - - def fmt_lines (self) : - """ - Yield a single line with the comment - """ - - return self._fmt_comments() - -class _Section (Object) : - """ - Base implementation of Section, but doesn't format comments in output (inheriting class can define how that happens) - """ - - def __init__ (self, params=None, decls=None, comment=None) : - """ - If params/decls are given, those are the used as the initial contents of this section - - If a comment is given, then it will be formatted before the section's stuff - """ - - Object.__init__(self, [comment]) - - self.params = params or [] - self.decls = decls or [] - - def add_param (self, param) : - """ - Add the given Parameter to the end of this section's params - """ - - self.params.append(param) - - def add_params (self, params) : - for param in params : - self.add_param(param) - - def add_decl (self, decl) : - """ - Add the given Declaration to the end of this section's decls - """ - - self.decls.append(decl) - - def add_decls (self, decls) : - for decl in decls : - self.add_decl(decl) - - def fmt_lines (self) : - """ - Format all of our params and decls, in that order - """ - - # then output each content line - for stmt in itertools.chain(self.params, self.decls) : - # skip Nones - if stmt is None : - continue - - for line in stmt.fmt_lines() : - yield line - -class Section (_Section) : - """ - A section holds a list of params and a list of decls, plus some comments at the beginning of the section - """ - - def fmt_lines (self) : - """ - Format all of our comments, and then super - """ - - # comments - for line in self._fmt_comments() : - yield line - - # section stuff - for line in _Section.fmt_lines(self) : - yield line - -class ConfFile (Section, conf.File) : - DEFAULT_NAME = "dhcpd.conf" - DEFAULT_PATH = "/etc/dhcp3/dhcpd.conf" - - def __init__ (self, name=DEFAULT_NAME, path=DEFAULT_PATH, params=None, decls=None, comment=None) : - """ - Initialize the dhcpd config file, but don't open it yet. - """ - - conf.File.__init__(self, name, path) - Section.__init__(self, params, decls, comment) - -class Statement (Object) : - """ - A statement is a single line in the config file - """ - - def __init__ (self, name, *args, **kwargs) : - """ - Arguments given as None will be ignored. - - A comment can be given as a keyword argument - """ - - if kwargs : assert len(kwargs) == 1 and 'comment' in kwargs - - Object.__init__(self, [kwargs.get('comment')]) - - self.name = name - self.args = [arg for arg in args if arg is not None] - - def _fmt_arg (self, arg) : - """ - Formats a arg for use in output, the following types are supported: - - list/tuple/iter: results in a comma-and-space separated list of formatted values - unicode: results in an encoded str - str: results in the string itself, quoted if needed - other: attempt to convert to a str, and then format that - """ - - # format lists specially - # XXX: iterators? - if isinstance(arg, (list, tuple)) : - # recurse as a comma-and-space separated list - return ', '.join(self._fmt_arg(a) for a in arg) - - elif isinstance(arg, Literal) : - # use what it specifies - return arg.fmt_arg() - - elif isinstance(arg, unicode) : - # recurse with the str version - # XXX: what encoding to use? - return self._fmt_arg(arg.encode('utf8')) - - elif isinstance(arg, str) : - # XXX: quoting - return arg - - else : - # try and use it as a string - return self._fmt_arg(str(arg)) - - def _fmt_data (self) : - """ - Formats the statement name/params as a single line, ignoring None - """ - - return "%s%s" % (self.name, (' ' + ' '.join(self._fmt_arg(a) for a in self.args)) if self.args else '') - -class Literal (Statement) : - """ - A literal is something that goes into the config file as-is, with no formatting or escaping applied. - """ - - def __init__ (self, literal) : - self.literal = literal - - def fmt_arg (self) : - return self.literal - - def fmt_lines (self) : - yield self.literal - -class Parameter (Statement) : - """ - A parameter is a single statement that configures the behaviour of something. - - Parameters have a name, and optionally, a number of arguments, and are formatted as statements terminated with - a semicolon. For convenience, params/decls that are None are ignored. - - The parameter will be formatted like this: - [ [ ... ] ] ";" - """ - - 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. - - [ [ ... ] ] { - [
] - } - - """ - - 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 { - [ 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 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 { - [ 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 ". - """ - - super(Option, self).__init__("option %s" % name, *args, **kwargs) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 host.py --- a/host.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -""" - Information about one physica host -""" - -import dhcp -import bind_conf as bindc - -class Interface (object) : - """ - A physical interface for a host - """ - - def __init__ (self, mac_addr, name=None) : - """ - @param name the short name of the interface (e.g. 'lan' or 'wlan'), or None for no suffix - @param mac the physical-layer addr.MAC address - """ - - self.addr = mac_addr - self.name = name - -class Host (object) : - """ - A host has a single address/name, an owner, and multiple interfaces - """ - - def __init__ (self, hostname, address, interfaces) : - """ - @param hostname the short hostname, without the domain name component - @param address the addr.IP address - @param interfaces a list of zero or more Interface objects - """ - - self.hostname = hostname - self.address = address - self.interfaces = interfaces - - def build_dhcp_hosts (self) : - """ - Build and yield a series of dhcp_conf.Host objects for this host. - - If the host does not have any interfaces defined, this doesn't yield anything - """ - - # XXX: do we want to ensure that the host names are unique? - - for iface in self.interfaces : - # the DHCP hostname - name = "%s%s" % (self.hostname, ('-%s' % (iface.name)) if iface.name else '') - - # build it - yield dhcp.Host(name, iface.addr, self.address) - - def build_bind_domain_records (self, origin) : - """ - Build and yield one or more forward records (A/AAAA) for the host, with the given domain as the origin - """ - - yield bindc.A(self.hostname, self.address) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 in/hosts.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/in/hosts.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,55 @@ +from addr import IP, Network +from host import Interface, Host +from dhcp import Subnet +from bind import Settings as BindSettings +from bind_conf import Interval, FQDN + +# BIND stuff +domain = "paivola.fi" + +nameservers = [ + FQDN("ranssi.paivola.fi"), + FQDN("misc1.idler.fi"), + FQDN("misc2.idler.fi"), + FQDN("srv.marttila.de"), + ] + +mailservers = [ + FQDN("mail.paivola.fi"), + ] + +bind_settings = BindSettings( + ttl = 3601, + hostmaster = "hostmaster", + refresh = Interval(h=1), + retry = Interval(m=3), + expire = Interval(d=28), + minimum = Interval(60) + ) + +# DHCP stuff +dhcp_settings = { + 'default-lease-time': 43200, + 'max-lease-time': 86400, + 'authorative': None, +} + +dhcp_options = { + 'domain-name-servers': IP('194.197.235.145'), +} + +shared_network = 'PVL' +subnets = [ + Subnet(Network('194.197.235.0/24'), router_idx=1, range=(26, 70), unknown_clients='allow', comment="Public network"), + Subnet(Network('192.168.0.0/23'), router_idx=1, unknown_clients='deny', comment="Internal network"), +] + +# general stuff +hosts = [ + Host('jumpgate', IP('194.197.235.1'), [ ]), + Host('mikk4', IP('194.197.235.72'), [ + Interface('00:16:01:37:D1:D2'), + Interface('00:0F:B0:0A:EF:58'), + ]), +] + diff -r 57e8168ba8c4 -r 0f9cae2d7147 main.py --- a/main.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +0,0 @@ -#!/usr/bin/env python2.5 - -import data, dhcp, bind_conf, bind - -import optparse, itertools - -def parse_args (argv) : - """ - Parse the command-line arguments from the given argv list, returning a (options_struct, args_list) tuple, - as per optparse. - """ - - usage = "Usage: %prog [options] data-file" - - # define our options - parser = optparse.OptionParser(usage=usage) - parser.add_option('--dhcpd-conf', dest='dhcpd_conf', metavar="PATH", help="path to dhcpd.conf", default='/etc/dhcp3/dhcpd.conf') - parser.add_option('--bind-zone', dest='bind_zone', metavar="PATH", help="path to bind zone file", default=None) - parser.add_option('--autoserial', dest='autoserial_path', metavar="PATH", help="path to autoserial file", default='autoserial') - - # parse them - options, args = parser.parse_args(args=argv[1:]) - - # parse the positional arguments - data_file, = args - - # ok - return options, (data_file, ) - -def write_dhcp (options, settings) : - """ - Write the DHCP config module using the data loaded from the given module - """ - - # build the config file - config = dhcp.Config(path=options.dhcpd_conf, - settings = settings.dhcp_settings, - options = settings.dhcp_options, - shared_network = settings.shared_network, - subnets = settings.subnets, - hosts = itertools.chain(*(host.build_dhcp_hosts() for host in settings.hosts)), - ) - - # write it out - config.write() - -def write_bind (options, settings) : - """ - Write a BIND config for a forward zone - """ - - assert options.bind_zone - - # load the serial - autoserial = bind.AutoSerial(options.autoserial_path) - - # build the zone file - zone = bind.Domain(domain=settings.domain, path=options.bind_zone, - nameservers = settings.nameservers, - mailservers = [((i+1)*10, label) for i, label in enumerate(settings.mailservers)], - serial = autoserial.serial(), - settings = settings.bind_settings, - objs = itertools.chain(*[host.build_bind_domain_records(settings.domain) for host in settings.hosts]), - ) - - # write it out - zone.write() - -def main (argv) : - """ - Our app entry point, parse args, load data, write out the config files - """ - - # parse args - options, (data_file, ) = parse_args(argv) - - # load the data - data_module = data.load_py('pvl_hosts_data', data_file) - - # write out the config files - write_dhcp(options, data_module) - write_bind(options, data_module) - -if __name__ == '__main__' : - from sys import argv - - main(argv) - diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl-config --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl-config Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,88 @@ +#!/usr/bin/env python2.5 + +import data, dhcp, bind_conf, bind + +import optparse, itertools + +def parse_args (argv) : + """ + Parse the command-line arguments from the given argv list, returning a (options_struct, args_list) tuple, + as per optparse. + """ + + usage = "Usage: %prog [options] data-file" + + # define our options + parser = optparse.OptionParser(usage=usage) + parser.add_option('--dhcpd-conf', dest='dhcpd_conf', metavar="PATH", help="path to dhcpd.conf", default='/etc/dhcp3/dhcpd.conf') + parser.add_option('--bind-zone', dest='bind_zone', metavar="PATH", help="path to bind zone file", default=None) + parser.add_option('--autoserial', dest='autoserial_path', metavar="PATH", help="path to autoserial file", default='autoserial') + + # parse them + options, args = parser.parse_args(args=argv[1:]) + + # parse the positional arguments + data_file, = args + + # ok + return options, (data_file, ) + +def write_dhcp (options, settings) : + """ + Write the DHCP config module using the data loaded from the given module + """ + + # build the config file + config = dhcp.Config(path=options.dhcpd_conf, + settings = settings.dhcp_settings, + options = settings.dhcp_options, + shared_network = settings.shared_network, + subnets = settings.subnets, + hosts = itertools.chain(*(host.build_dhcp_hosts() for host in settings.hosts)), + ) + + # write it out + config.write() + +def write_bind (options, settings) : + """ + Write a BIND config for a forward zone + """ + + assert options.bind_zone + + # load the serial + autoserial = bind.AutoSerial(options.autoserial_path) + + # build the zone file + zone = bind.Domain(domain=settings.domain, path=options.bind_zone, + nameservers = settings.nameservers, + mailservers = [((i+1)*10, label) for i, label in enumerate(settings.mailservers)], + serial = autoserial.serial(), + settings = settings.bind_settings, + objs = itertools.chain(*[host.build_bind_domain_records(settings.domain) for host in settings.hosts]), + ) + + # write it out + zone.write() + +def main (argv) : + """ + Our app entry point, parse args, load data, write out the config files + """ + + # parse args + options, (data_file, ) = parse_args(argv) + + # load the data + data_module = data.load_py('pvl_hosts_data', data_file) + + # write out the config files + write_dhcp(options, data_module) + write_bind(options, data_module) + +if __name__ == '__main__' : + from sys import argv + + main(argv) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/addr.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/addr.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,60 @@ +""" + Used to define IP-address/subnet stuff +""" + +import IPy + +class IP (IPy.IP, object) : + """ + A literal IPv4 address + """ + + def __init__ (self, address) : + """ + Parse the given literal IP address in "a.b.c.d" form + """ + + super(IP, self).__init__(address) + + def is_v4 (self) : + """ + Returns True if the address is an IPv4 address + """ + + return self.version() == 4 + + def is_v6 (self) : + """ + Returns True if the address is an IPv6 address + """ + + return self.version() == 6 + +class Network (IPy.IP, object) : + """ + An IPv4 network (subnet) + """ + + def __init__ (self, prefix) : + """ + Parse the given prefix in "a.b.c.d/l" form + """ + + super(Network, self).__init__(prefix) + +class MAC (object) : + """ + A mac address + """ + + def __init__ (self, mac) : + """ + Parse the given MAC address in "aa:bb:cc:dd:ee:ff" form + """ + + # XXX: validate + self.mac = mac + + def __str__ (self) : + return self.mac + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/bind.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/bind.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,187 @@ +""" + High-level BIND stuff +""" + +from __future__ import with_statement + +import bind_conf as bindc + +import os.path, datetime + +DEFAULT_TTL = bindc.Interval(3600) + +class Settings (object) : + """ + A set of basic settings for a zone, mostly default TTL/refresh/retry/expire/minimum settings + """ + + def __init__ (self, ttl, hostmaster, refresh, retry, expire, minimum) : + self.ttl = ttl + self.hostmaster = hostmaster + self.refresh = refresh + self.retry = retry + self.expire = expire + self.minimum = minimum + +class AutoSerial (object) : + """ + Automatically generate the next serial to use by loading it from a file. + + The generated serials are in YYYYMMDDXX format. + """ + + def __init__ (self, path) : + """ + Load the current serial + + @param path the path to the serial file + """ + + # store + self.path = path + + # load it + # XXX: locking + serial = self.read() + + # current date + today = datetime.date.today() + + # parse it + if serial : + date, code = self.parse(serial) + + else : + date, code = today, 0 + + # increment it + date, code = self.next(date, code) + + # format it + self._serial = self.build(date, code) + + # write it out + self.write(self._serial) + + def parse (self, serial) : + """ + Parse the given serial into a (datetime.date, code) format + """ + + # build it into a date + date = datetime.date( + year = int(serial[0:4]), + month = int(serial[4:6]), + day = int(serial[6:8]) + ) + + code = int(serial[8:]) + + return date, code + + def next (self, date, code) : + """ + Return the next valid serial following the given one + """ + + # current date + today = datetime.date.today() + + # now to increment? + if date < today : + # jump to today's first serial + date = today + code = 0 + + else : + # today or overflowed into the future, just increment the code + code += 1 + + # code overflowed into next day? + if code > 99 : + date += datetime.timedelta(days=1) + code = 0 + + # ok + return date, code + + def build (self, date, code) : + """ + Build a serial code the given date/code + """ + + assert 0 <= code <= 99 + + return "%s%02d" % (date.strftime("%Y%m%d"), code) + + def read (self) : + """ + Read the current serial, returning it, or None, if not found... + """ + + # if it doesn't exist, default + if not os.path.exists(self.path) : + return None + + # read it + with open(self.path) as fh : + return fh.read().strip() + + def write (self, serial) : + """ + Write a new serial + """ + + with open(self.path, 'w') as fh : + fh.write("%s\n" % (serial, )) + + def serial (self) : + """ + Return a new, unused serial code (before __init__) + """ + + return self._serial + +class Domain (bindc.ZoneFile) : + """ + A domain has a skeleton of stuff defined, but the rest is $INCLUDE'd from elsewhere, which is useful for + multi-domain setups where the domains are mostly similar + """ + + def __init__ (self, domain, path, nameservers, mailservers, serial, settings, include=None, objs=None) : + """ + @param domain the domain name + @param path the path to the zone file + @param nameservers list of nameservers as labels + @param mailservers list of (pref, label) tuples for MX records + @param serial the serial code to use + @param settings the TTL/SOA settings to use + @param include the optional zonefile to include + @param objs the optional other objects to add to the zonefile + """ + + super(Domain, self).__init__(domain, path) + + # the default TTL + self.add_directive(bindc.TTLDirective(settings.ttl)) + + # the SOA record + self.add_record(bindc.SOA(None, nameservers[0], + settings.hostmaster, serial, settings.refresh, settings.retry, settings.expire, settings.minimum + )) + + # the NS records + for label in nameservers : + self.add_record(bindc.NS(None, label)) + + # the MX records + for pref, label in mailservers : + self.add_record(bindc.MX(None, pref, label)) + + # include? + if include : + self.add_directive(bindc.IncludeDirective(include)) + + if objs : + for obj in objs : + self.add_obj(obj) diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/bind_conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/bind_conf.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,395 @@ +""" + Configuration file output for the ISC DNS server: BIND +""" + +import conf + +class Object (conf.ConfObject) : + """ + Our own version of ConfObject that knows how to format comments + """ + + def _fmt_comments (self) : + """ + Format comments using the standard format: + ";" + """ + + 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) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/conf.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,136 @@ +""" + Generic configuration file output +""" + +import os, os.path, tempfile, shutil + +class ConfObject (object) : + """ + An object that can be written to a ConfFile, as multiple lines of text. + """ + + def __init__ (self, comments=None) : + """ + Initialize with the given list of comments. Comments that are None should be ignore. + """ + + # init the comments list + self.comments = comments or [] + + def add_comment (self, comment) : + """ + Add a comment to be rendered in the output. + """ + + # add it + self.comments.append(comment) + + def fmt_lines (self) : + """ + Yield a series of lines to be output in the file + """ + + abstract + +class File (ConfObject) : + """ + A single configuration file on the filesystem. + + Configuration files are themselves ConfObject's, although this must be implemented in the inheriting class. + """ + + def __init__ (self, name, path, backup_suffix='.bak', mode=0644) : + """ + Initialize the config file, but don't open it yet + + @param name the human-readable friendly name for the config file + @param path the full filesystem path for the file + @param backup_suffix rename the old file by adding this suffix when replacing it + @param mode the permission bits for the new file + """ + + self.name = name + self.path = path + self.backup_suffix = backup_suffix + self.mode = mode + + def write_file (self, file) : + """ + Write out this config's stuff into the given file + """ + + writer = Writer(file) + writer.write_obj(self) + + def write (self) : + """ + Write out a new config file with this config's stuff using the following procedure: + + * lock the real file + * open a new temporary file, and write out the objects into that, closing it + * move the real file out of the way + * move the temporary file into the real file's place + * unlock the real file + """ + + # XXX: how to aquire the lock? + # XXX: this needs checking over + + # open the new temporary file + # XXX: ensure fd is closed + tmp_fd, tmp_path = tempfile.mkstemp(prefix=self.name) + + # ...as a file + tmp_file = os.fdopen(tmp_fd, "w") + + # fix the permissions + os.chmod(tmp_path, self.mode) + + # write it + self.write_file(tmp_file) + + # close it + tmp_file.close() + + # move the old file out of the way + if os.path.exists(self.path) : + os.rename(self.path, self.path + self.backup_suffix) + + # move the new file in + shutil.move(tmp_path, self.path) + +class Writer (object) : + """ + A conf.Writer is used to write out a new conf.File (as a temporary file) + """ + + def __init__ (self, file) : + """ + @param file the temporary file object + """ + + self.file = file + + def write_line (self, line) : + """ + Write a single line to the file + """ + + self.file.write("%s\n" % (line, )) + + def write_lines (self, lines) : + """ + Write a series of lines into the file + """ + + for line in lines : + self.write_line(line) + + def write_obj (self, obj) : + """ + Write a single object to the file + """ + + # just write out all the lines + self.write_lines(obj.fmt_lines()) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/data.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,16 @@ +""" + Functions to load data from various sources +""" + +import imp + +def load_py (name, path) : + """ + Load a python file from the given filesystem path, returning the module itself. + + The "name" of the module must be given, it should be something sane and unique... + """ + + # just load it and return + return imp.load_source(name, path) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/dhcp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/dhcp.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,81 @@ +""" + Higher-level DHCP config structure model +""" + +import dhcp_conf as dhcpc + +class Config (dhcpc.ConfFile) : + """ + A full configuration file + """ + + def __init__ (self, name=dhcpc.ConfFile.DEFAULT_NAME, path=dhcpc.ConfFile.DEFAULT_PATH, + settings=None, options=None, shared_network=False, subnets=None, hosts=None, comment=None + ) : + """ + Create a full configuration file for the given settings: + + settings: a { name: value } mappping of general settings to set + options: a { opt_name: opt_value } mapping of options to set + shared_network: define the subnets as a shared network of the given name + subnets: an iterable of Subnet's to define + hosts: an iterable of Host's to define + + """ + + dhcpc.ConfFile.__init__(self, name, path, comment=comment) + + # define global settings + if settings : + self.add_params(dhcpc.Parameter(setting, value) for setting, value in settings.iteritems()) + + # define global options + if options : + self.add_params(dhcpc.Option(option, value) for option, value in options.iteritems()) + + # the shared-network section, or a series of subnets + if shared_network : + self.add_decl(dhcpc.SharedNetwork(shared_network, decls=subnets)) + + elif subnets : + self.add_decls(subnets) + + # hosts section + if hosts : + self.add_decls(hosts) + +class Subnet (dhcpc.Subnet) : + """ + A subnet declaration with a router, and optionally a dynamic address pool, and allow/deny unknown clients + """ + + def __init__ (self, subnet, router_idx=1, range=None, unknown_clients=None, comment=None) : + """ + @param subnet the addr.IP representing the subnet + @param router_idx the subnet[index] of the default gateway + @param range optional (from_idx, to_idx) to define a dhcp pool + @param unknown_clients optional 'allow'/'deny' to set a policy for unknown clients + """ + + # validate + if unknown_clients : + assert unknown_clients in ('allow', 'deny') + + super(Subnet, self).__init__(subnet, params=[ + dhcpc.Option("routers", subnet[router_idx]), + dhcpc.Parameter("range", subnet[range[0]], subnet[range[1]]) if range else None, + dhcpc.Parameter(unknown_clients, "unknown-clients") if unknown_clients else None, + ], comment=comment) + + +class Host (dhcpc.Host) : + """ + A host declaration with a hardware address and a IP address + """ + + def __init__ (self, hostname, mac_addr, ip_addr, comment=None) : + super(Host, self).__init__(hostname, params=[ + dhcpc.Parameter("hardware ethernet", mac_addr), + dhcpc.Parameter("fixed-address", ip_addr) + ], comment=comment) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/dhcp_conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/dhcp_conf.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,341 @@ +""" + Configuration file output for the ISC DHCP server +""" + +import conf + +import itertools + +class Object (conf.ConfObject) : + """ + Our version of ConfObject + """ + + def _fmt_comments (self) : + """ + Format our comment lines + """ + + for comment in self.comments : + if comment is not None : + yield "# %s" % (comment, ) + + +class Comment (Object) : + """ + A comment, is, well, a comment :) + """ + + def __init__ (self, comment) : + """ + @param comment the comment string + """ + + Object.__init__(self, [comment]) + + def fmt_lines (self) : + """ + Yield a single line with the comment + """ + + return self._fmt_comments() + +class _Section (Object) : + """ + Base implementation of Section, but doesn't format comments in output (inheriting class can define how that happens) + """ + + def __init__ (self, params=None, decls=None, comment=None) : + """ + If params/decls are given, those are the used as the initial contents of this section + + If a comment is given, then it will be formatted before the section's stuff + """ + + Object.__init__(self, [comment]) + + self.params = params or [] + self.decls = decls or [] + + def add_param (self, param) : + """ + Add the given Parameter to the end of this section's params + """ + + self.params.append(param) + + def add_params (self, params) : + for param in params : + self.add_param(param) + + def add_decl (self, decl) : + """ + Add the given Declaration to the end of this section's decls + """ + + self.decls.append(decl) + + def add_decls (self, decls) : + for decl in decls : + self.add_decl(decl) + + def fmt_lines (self) : + """ + Format all of our params and decls, in that order + """ + + # then output each content line + for stmt in itertools.chain(self.params, self.decls) : + # skip Nones + if stmt is None : + continue + + for line in stmt.fmt_lines() : + yield line + +class Section (_Section) : + """ + A section holds a list of params and a list of decls, plus some comments at the beginning of the section + """ + + def fmt_lines (self) : + """ + Format all of our comments, and then super + """ + + # comments + for line in self._fmt_comments() : + yield line + + # section stuff + for line in _Section.fmt_lines(self) : + yield line + +class ConfFile (Section, conf.File) : + DEFAULT_NAME = "dhcpd.conf" + DEFAULT_PATH = "/etc/dhcp3/dhcpd.conf" + + def __init__ (self, name=DEFAULT_NAME, path=DEFAULT_PATH, params=None, decls=None, comment=None) : + """ + Initialize the dhcpd config file, but don't open it yet. + """ + + conf.File.__init__(self, name, path) + Section.__init__(self, params, decls, comment) + +class Statement (Object) : + """ + A statement is a single line in the config file + """ + + def __init__ (self, name, *args, **kwargs) : + """ + Arguments given as None will be ignored. + + A comment can be given as a keyword argument + """ + + if kwargs : assert len(kwargs) == 1 and 'comment' in kwargs + + Object.__init__(self, [kwargs.get('comment')]) + + self.name = name + self.args = [arg for arg in args if arg is not None] + + def _fmt_arg (self, arg) : + """ + Formats a arg for use in output, the following types are supported: + + list/tuple/iter: results in a comma-and-space separated list of formatted values + unicode: results in an encoded str + str: results in the string itself, quoted if needed + other: attempt to convert to a str, and then format that + """ + + # format lists specially + # XXX: iterators? + if isinstance(arg, (list, tuple)) : + # recurse as a comma-and-space separated list + return ', '.join(self._fmt_arg(a) for a in arg) + + elif isinstance(arg, Literal) : + # use what it specifies + return arg.fmt_arg() + + elif isinstance(arg, unicode) : + # recurse with the str version + # XXX: what encoding to use? + return self._fmt_arg(arg.encode('utf8')) + + elif isinstance(arg, str) : + # XXX: quoting + return arg + + else : + # try and use it as a string + return self._fmt_arg(str(arg)) + + def _fmt_data (self) : + """ + Formats the statement name/params as a single line, ignoring None + """ + + return "%s%s" % (self.name, (' ' + ' '.join(self._fmt_arg(a) for a in self.args)) if self.args else '') + +class Literal (Statement) : + """ + A literal is something that goes into the config file as-is, with no formatting or escaping applied. + """ + + def __init__ (self, literal) : + self.literal = literal + + def fmt_arg (self) : + return self.literal + + def fmt_lines (self) : + yield self.literal + +class Parameter (Statement) : + """ + A parameter is a single statement that configures the behaviour of something. + + Parameters have a name, and optionally, a number of arguments, and are formatted as statements terminated with + a semicolon. For convenience, params/decls that are None are ignored. + + The parameter will be formatted like this: + [ [ ... ] ] ";" + """ + + 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. + + [ [ ... ] ] { + [
] + } + + """ + + 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 { + [ 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 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 { + [ 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 ". + """ + + super(Option, self).__init__("option %s" % name, *args, **kwargs) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 pvl/config/host.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/config/host.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,60 @@ +""" + Information about one physica host +""" + +import dhcp +import bind_conf as bindc + +class Interface (object) : + """ + A physical interface for a host + """ + + def __init__ (self, mac_addr, name=None) : + """ + @param name the short name of the interface (e.g. 'lan' or 'wlan'), or None for no suffix + @param mac the physical-layer addr.MAC address + """ + + self.addr = mac_addr + self.name = name + +class Host (object) : + """ + A host has a single address/name, an owner, and multiple interfaces + """ + + def __init__ (self, hostname, address, interfaces) : + """ + @param hostname the short hostname, without the domain name component + @param address the addr.IP address + @param interfaces a list of zero or more Interface objects + """ + + self.hostname = hostname + self.address = address + self.interfaces = interfaces + + def build_dhcp_hosts (self) : + """ + Build and yield a series of dhcp_conf.Host objects for this host. + + If the host does not have any interfaces defined, this doesn't yield anything + """ + + # XXX: do we want to ensure that the host names are unique? + + for iface in self.interfaces : + # the DHCP hostname + name = "%s%s" % (self.hostname, ('-%s' % (iface.name)) if iface.name else '') + + # build it + yield dhcp.Host(name, iface.addr, self.address) + + def build_bind_domain_records (self, origin) : + """ + Build and yield one or more forward records (A/AAAA) for the host, with the given domain as the origin + """ + + yield bindc.A(self.hostname, self.address) + diff -r 57e8168ba8c4 -r 0f9cae2d7147 settings/hosts.py --- a/settings/hosts.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -from addr import IP, Network -from host import Interface, Host -from dhcp import Subnet -from bind import Settings as BindSettings -from bind_conf import Interval, FQDN - -# BIND stuff -domain = "paivola.fi" - -nameservers = [ - FQDN("ranssi.paivola.fi"), - FQDN("misc1.idler.fi"), - FQDN("misc2.idler.fi"), - FQDN("srv.marttila.de"), - ] - -mailservers = [ - FQDN("mail.paivola.fi"), - ] - -bind_settings = BindSettings( - ttl = 3601, - hostmaster = "hostmaster", - refresh = Interval(h=1), - retry = Interval(m=3), - expire = Interval(d=28), - minimum = Interval(60) - ) - -# DHCP stuff -dhcp_settings = { - 'default-lease-time': 43200, - 'max-lease-time': 86400, - 'authorative': None, -} - -dhcp_options = { - 'domain-name-servers': IP('194.197.235.145'), -} - -shared_network = 'PVL' -subnets = [ - Subnet(Network('194.197.235.0/24'), router_idx=1, range=(26, 70), unknown_clients='allow', comment="Public network"), - Subnet(Network('192.168.0.0/23'), router_idx=1, unknown_clients='deny', comment="Internal network"), -] - -# general stuff -hosts = [ - Host('jumpgate', IP('194.197.235.1'), [ ]), - Host('mikk4', IP('194.197.235.72'), [ - Interface('00:16:01:37:D1:D2'), - Interface('00:0F:B0:0A:EF:58'), - ]), -] - diff -r 57e8168ba8c4 -r 0f9cae2d7147 test/bind.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/bind.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,70 @@ +#!/usr/bin/env python2.5 +""" + Test bind_conf +""" + +import bind_conf as bindc +import test_conf, addr + +import unittest + +class TestBINDConf (test_conf._TestConfBase) : + def test_comment (self) : + self.assert_obj(bindc.Comment("test comment 2"), [ "; test comment 2" ]) + + def assert_str (self, label, value) : + self.assertEqual(str(label), value) + + def test_label (self) : + self.assert_str(bindc.Label("foo"), "foo") + + def test_origin (self) : + self.assert_str(bindc.Origin(), "@") + + def test_fqdn (self) : + self.assert_str(bindc.FQDN("foo.com"), "foo.com.") + + def test_interval (self) : + self.assert_str(bindc.Interval(12), "12") + self.assert_str(bindc.Interval(12, 1), "12s1m") + self.assert_str(bindc.Interval(h=2, d=5), "2h5d") + + def assert_rec (self, obj, lines) : + """ + Does a whitespace-insensitive compare of the record's formatted output and the given lines + """ + + for obj_line, line in zip(obj.fmt_lines(), lines) : + obj_line = ' '.join(obj_line.split()) + self.assertEqual(obj_line, line) + + def test_record (self) : + self.assert_rec(bindc.ResourceRecord(None, 'A', addr.IP("1.2.3.4"), cls='TST', ttl=60), [ "60 TST A 1.2.3.4" ]) + self.assert_rec(bindc.ResourceRecord('foo', 'CNAME', 'blaa'), [ "foo IN CNAME blaa" ]) + self.assert_rec(bindc.ResourceRecord('bar', 'CNAME', bindc.FQDN('example.com')), [ "bar IN CNAME example.com." ]) + + def test_record_types (self) : + self.assert_rec(bindc.SOA(None, 'ns1', 'hostmaster', '2009040200', bindc.Interval(1), 2, 3, 4), [ + "IN SOA ns1 hostmaster ( 2009040200 1 2 3 4 )" + ]) + self.assert_rec(bindc.A('foo', addr.IP("1.2.3.4")), [ "foo IN A 1.2.3.4" ]) + self.assert_rec(bindc.AAAA('foo2', addr.IP("2001::5")), [ "foo2 IN AAAA 2001::5" ]) + self.assert_rec(bindc.CNAME('foo3', bindc.FQDN("foo.com")), [ "foo3 IN CNAME foo.com." ]) + self.assert_rec(bindc.TXT('test4', "this is some text"), [ "test4 IN TXT \"this is some text\"" ]) + self.assert_rec(bindc.MX(None, 10, "foo"), [ "IN MX 10 foo" ]) + self.assert_rec(bindc.NS(None, "ns2"), [ "IN NS ns2" ]) + self.assert_rec(bindc.PTR(addr.IP("1.2.3.4"), 'bar'), [ "4.3.2.1.in-addr.arpa. IN PTR bar" ]) + + def test_directive (self) : + self.assert_obj(bindc.Directive("TEST", "a", None, 2, comments=["hmm..."]), [ + "; hmm...", + "$TEST a 2" + ]) + self.assert_obj(bindc.OriginDirective(bindc.FQDN("foo.com")), [ "$ORIGIN foo.com." ]) + self.assert_obj(bindc.TTLDirective(bindc.Interval(h=2)), [ "$TTL 2h" ]) + self.assert_obj(bindc.IncludeDirective("db.foo.com"), [ "$INCLUDE db.foo.com" ]) + self.assert_obj(bindc.GenerateDirective((1, 10), "dyn${0,2}", 'A', "1.2.3.$"), [ "$GENERATE 1-10 dyn${0,2} A 1.2.3.$" ]) + +if __name__ == '__main__' : + unittest.main() + diff -r 57e8168ba8c4 -r 0f9cae2d7147 test/conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/conf.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,16 @@ +""" + Test conf.py +""" + +import unittest + +class _TestConfBase (unittest.TestCase) : + def assert_obj (self, obj, lines) : + """ + Formats the given conf.Object and compares the output against the given lines + """ + + for obj_line, line in zip(obj.fmt_lines(), lines) : + self.assertEqual(obj_line, line) + + diff -r 57e8168ba8c4 -r 0f9cae2d7147 test/dhcp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/dhcp.py Sun Jul 12 00:51:08 2009 +0300 @@ -0,0 +1,166 @@ +#!/usr/bin/env python2.5 +""" + Test dhcp_conf +""" + +import dhcp_conf as dhcpc +import test_conf, dhcp, addr + +import unittest + +class TestDHCPConf (test_conf._TestConfBase) : + def assert_stmt (self, stmt, line) : + """ + Formats the given Statement, and compares the output against the given line. + + Note that the dhcpc.Statement doesn't have a working fmt_lines implementation. + """ + + self.assertEqual(stmt._fmt_data(), line) + + def test_comment (self) : + self.assert_obj(dhcpc.Comment("foo bar"), [ "# foo bar" ]) + + def test_section (self) : + self.assert_obj(dhcpc.Section(comment="test"), [ "# test" ]) + + self.assert_obj(dhcpc.Section(params=[ + dhcpc.Parameter("param0"), None + ], comment="foo"), [ + "# foo", + "param0;", + ]) + + def test_statement (self) : + self.assert_stmt(dhcpc.Statement("stmt0"), "stmt0") + self.assert_stmt(dhcpc.Statement("stmt1", [ "this", "that" ]), "stmt1 this, that") + self.assert_stmt(dhcpc.Statement("stmt2", dhcpc.Literal("...")), "stmt2 ...") + self.assert_stmt(dhcpc.Statement("stmt3", u"quux"), "stmt3 quux") + self.assert_stmt(dhcpc.Statement("stmt4", "bar"), "stmt4 bar") + self.assert_stmt(dhcpc.Statement("stmt5", 1), "stmt5 1") + self.assert_stmt(dhcpc.Statement("stmt6", 1, None, 2), "stmt6 1 2") + + def test_literal (self) : + self.assert_obj(dhcpc.Literal("///"), [ "///" ]) + + def test_parameter (self) : + self.assert_obj(dhcpc.Parameter("param0", "this", 13, "that"), [ "param0 this 13 that;" ]) + self.assert_obj(dhcpc.Parameter("param1", comment="testing"), [ "# testing", "param1;" ]) + + def test_declaration (self) : + self.assert_obj(dhcpc.Declaration("decl0", ["arg0", "arg1"], [ + dhcpc.Parameter("param0"), + None + ], [ + dhcpc.Declaration("decl0.0", params=[ + dhcpc.Parameter("param0.0.1", "value") + ]) + ], comment="foo"), [ + "# foo", + "decl0 arg0 arg1 {", + "\tparam0;", + "\tdecl0.0 {", + "\t\tparam0.0.1 value;", + "\t}", + "}", + ]) + + def test_shared_network (self) : + self.assert_obj(dhcpc.SharedNetwork("net0", params=[ + dhcpc.Parameter("param0") + ]), [ + "shared-network net0 {", + "\tparam0;", + "}" + ]) + + def test_subnet (self) : + self.assert_obj(dhcpc.Subnet(addr.Network("194.197.235.0/24"), params=[ + dhcpc.Parameter("param0") + ]), [ + "subnet 194.197.235.0 netmask 255.255.255.0 {", + "\tparam0;", + "}" + ]) + + def test_group (self) : + self.assert_obj(dhcpc.Group(decls=[ + dhcpc.Declaration("decl0.0", params=[ + dhcpc.Parameter("param0.0.1", "value") + ]) + ]), [ + "group {", + "\tdecl0.0 {", + "\t\tparam0.0.1 value;", + "\t}", + "}" + ]) + + def test_host (self) : + self.assert_obj(dhcpc.Host("test-hostname", params=[ + dhcpc.Parameter("param0") + ]), [ + "host test-hostname {", + "\tparam0;", + "}" + ]) + + def test_option (self) : + self.assert_obj(dhcpc.Option("foo", "example.com"), [ + "option foo example.com;", + ]) + +class TestDHCP (_TestConfObj) : + def test_host (self) : + self.assert_obj(dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"), comment="foo"), [ + "# foo", + "host testhost {", + "\thardware ethernet 12:34:56:78:90:ab;", + "\tfixed-address 1.2.3.4;", + "}" + ]) + + def test_subnet (self) : + self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), comment="bar"), [ + "# bar", + "subnet 1.2.3.0 netmask 255.255.255.0 {", + "\toption routers 1.2.3.1;", + "}" + ]) + + self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), router_idx=10, range=(20, 30), unknown_clients='allow'), [ + "subnet 1.2.3.0 netmask 255.255.255.0 {", + "\toption routers 1.2.3.10;", + "\trange 1.2.3.20 1.2.3.30;", + "\tallow unknown-clients;", + "}" + ]) + + def test_config (self) : + self.assert_obj(dhcp.Config( + settings = { 'foo-setting': 'someval' }, + options = { 'bar-opt': ['one', 'two'] }, + shared_network = "FOO-NET", + subnets = [ + dhcp.Subnet(addr.Network("1.2.3.0/24")) + ], + hosts = [ + dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4")) + ], + ), [ + "foo-setting someval;", + "option bar-opt one, two;", + "shared-network FOO-NET {", + "\tsubnet 1.2.3.0 netmask 255.255.255.0 {", + "\t\toption routers 1.2.3.1;", + "\t}", + "}", + "host testhost {", + "\thardware ethernet 12:34:56:78:90:ab;", + "\tfixed-address 1.2.3.4;", + "}" + ]) + +if __name__ == '__main__' : + unittest.main() + diff -r 57e8168ba8c4 -r 0f9cae2d7147 test_bind.py --- a/test_bind.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -#!/usr/bin/env python2.5 -""" - Test bind_conf -""" - -import bind_conf as bindc -import test_conf, addr - -import unittest - -class TestBINDConf (test_conf._TestConfBase) : - def test_comment (self) : - self.assert_obj(bindc.Comment("test comment 2"), [ "; test comment 2" ]) - - def assert_str (self, label, value) : - self.assertEqual(str(label), value) - - def test_label (self) : - self.assert_str(bindc.Label("foo"), "foo") - - def test_origin (self) : - self.assert_str(bindc.Origin(), "@") - - def test_fqdn (self) : - self.assert_str(bindc.FQDN("foo.com"), "foo.com.") - - def test_interval (self) : - self.assert_str(bindc.Interval(12), "12") - self.assert_str(bindc.Interval(12, 1), "12s1m") - self.assert_str(bindc.Interval(h=2, d=5), "2h5d") - - def assert_rec (self, obj, lines) : - """ - Does a whitespace-insensitive compare of the record's formatted output and the given lines - """ - - for obj_line, line in zip(obj.fmt_lines(), lines) : - obj_line = ' '.join(obj_line.split()) - self.assertEqual(obj_line, line) - - def test_record (self) : - self.assert_rec(bindc.ResourceRecord(None, 'A', addr.IP("1.2.3.4"), cls='TST', ttl=60), [ "60 TST A 1.2.3.4" ]) - self.assert_rec(bindc.ResourceRecord('foo', 'CNAME', 'blaa'), [ "foo IN CNAME blaa" ]) - self.assert_rec(bindc.ResourceRecord('bar', 'CNAME', bindc.FQDN('example.com')), [ "bar IN CNAME example.com." ]) - - def test_record_types (self) : - self.assert_rec(bindc.SOA(None, 'ns1', 'hostmaster', '2009040200', bindc.Interval(1), 2, 3, 4), [ - "IN SOA ns1 hostmaster ( 2009040200 1 2 3 4 )" - ]) - self.assert_rec(bindc.A('foo', addr.IP("1.2.3.4")), [ "foo IN A 1.2.3.4" ]) - self.assert_rec(bindc.AAAA('foo2', addr.IP("2001::5")), [ "foo2 IN AAAA 2001::5" ]) - self.assert_rec(bindc.CNAME('foo3', bindc.FQDN("foo.com")), [ "foo3 IN CNAME foo.com." ]) - self.assert_rec(bindc.TXT('test4', "this is some text"), [ "test4 IN TXT \"this is some text\"" ]) - self.assert_rec(bindc.MX(None, 10, "foo"), [ "IN MX 10 foo" ]) - self.assert_rec(bindc.NS(None, "ns2"), [ "IN NS ns2" ]) - self.assert_rec(bindc.PTR(addr.IP("1.2.3.4"), 'bar'), [ "4.3.2.1.in-addr.arpa. IN PTR bar" ]) - - def test_directive (self) : - self.assert_obj(bindc.Directive("TEST", "a", None, 2, comments=["hmm..."]), [ - "; hmm...", - "$TEST a 2" - ]) - self.assert_obj(bindc.OriginDirective(bindc.FQDN("foo.com")), [ "$ORIGIN foo.com." ]) - self.assert_obj(bindc.TTLDirective(bindc.Interval(h=2)), [ "$TTL 2h" ]) - self.assert_obj(bindc.IncludeDirective("db.foo.com"), [ "$INCLUDE db.foo.com" ]) - self.assert_obj(bindc.GenerateDirective((1, 10), "dyn${0,2}", 'A', "1.2.3.$"), [ "$GENERATE 1-10 dyn${0,2} A 1.2.3.$" ]) - -if __name__ == '__main__' : - unittest.main() - diff -r 57e8168ba8c4 -r 0f9cae2d7147 test_conf.py --- a/test_conf.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" - Test conf.py -""" - -import unittest - -class _TestConfBase (unittest.TestCase) : - def assert_obj (self, obj, lines) : - """ - Formats the given conf.Object and compares the output against the given lines - """ - - for obj_line, line in zip(obj.fmt_lines(), lines) : - self.assertEqual(obj_line, line) - - diff -r 57e8168ba8c4 -r 0f9cae2d7147 test_dhcp.py --- a/test_dhcp.py Sun Jul 12 00:43:36 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -#!/usr/bin/env python2.5 -""" - Test dhcp_conf -""" - -import dhcp_conf as dhcpc -import test_conf, dhcp, addr - -import unittest - -class TestDHCPConf (test_conf._TestConfBase) : - def assert_stmt (self, stmt, line) : - """ - Formats the given Statement, and compares the output against the given line. - - Note that the dhcpc.Statement doesn't have a working fmt_lines implementation. - """ - - self.assertEqual(stmt._fmt_data(), line) - - def test_comment (self) : - self.assert_obj(dhcpc.Comment("foo bar"), [ "# foo bar" ]) - - def test_section (self) : - self.assert_obj(dhcpc.Section(comment="test"), [ "# test" ]) - - self.assert_obj(dhcpc.Section(params=[ - dhcpc.Parameter("param0"), None - ], comment="foo"), [ - "# foo", - "param0;", - ]) - - def test_statement (self) : - self.assert_stmt(dhcpc.Statement("stmt0"), "stmt0") - self.assert_stmt(dhcpc.Statement("stmt1", [ "this", "that" ]), "stmt1 this, that") - self.assert_stmt(dhcpc.Statement("stmt2", dhcpc.Literal("...")), "stmt2 ...") - self.assert_stmt(dhcpc.Statement("stmt3", u"quux"), "stmt3 quux") - self.assert_stmt(dhcpc.Statement("stmt4", "bar"), "stmt4 bar") - self.assert_stmt(dhcpc.Statement("stmt5", 1), "stmt5 1") - self.assert_stmt(dhcpc.Statement("stmt6", 1, None, 2), "stmt6 1 2") - - def test_literal (self) : - self.assert_obj(dhcpc.Literal("///"), [ "///" ]) - - def test_parameter (self) : - self.assert_obj(dhcpc.Parameter("param0", "this", 13, "that"), [ "param0 this 13 that;" ]) - self.assert_obj(dhcpc.Parameter("param1", comment="testing"), [ "# testing", "param1;" ]) - - def test_declaration (self) : - self.assert_obj(dhcpc.Declaration("decl0", ["arg0", "arg1"], [ - dhcpc.Parameter("param0"), - None - ], [ - dhcpc.Declaration("decl0.0", params=[ - dhcpc.Parameter("param0.0.1", "value") - ]) - ], comment="foo"), [ - "# foo", - "decl0 arg0 arg1 {", - "\tparam0;", - "\tdecl0.0 {", - "\t\tparam0.0.1 value;", - "\t}", - "}", - ]) - - def test_shared_network (self) : - self.assert_obj(dhcpc.SharedNetwork("net0", params=[ - dhcpc.Parameter("param0") - ]), [ - "shared-network net0 {", - "\tparam0;", - "}" - ]) - - def test_subnet (self) : - self.assert_obj(dhcpc.Subnet(addr.Network("194.197.235.0/24"), params=[ - dhcpc.Parameter("param0") - ]), [ - "subnet 194.197.235.0 netmask 255.255.255.0 {", - "\tparam0;", - "}" - ]) - - def test_group (self) : - self.assert_obj(dhcpc.Group(decls=[ - dhcpc.Declaration("decl0.0", params=[ - dhcpc.Parameter("param0.0.1", "value") - ]) - ]), [ - "group {", - "\tdecl0.0 {", - "\t\tparam0.0.1 value;", - "\t}", - "}" - ]) - - def test_host (self) : - self.assert_obj(dhcpc.Host("test-hostname", params=[ - dhcpc.Parameter("param0") - ]), [ - "host test-hostname {", - "\tparam0;", - "}" - ]) - - def test_option (self) : - self.assert_obj(dhcpc.Option("foo", "example.com"), [ - "option foo example.com;", - ]) - -class TestDHCP (_TestConfObj) : - def test_host (self) : - self.assert_obj(dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4"), comment="foo"), [ - "# foo", - "host testhost {", - "\thardware ethernet 12:34:56:78:90:ab;", - "\tfixed-address 1.2.3.4;", - "}" - ]) - - def test_subnet (self) : - self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), comment="bar"), [ - "# bar", - "subnet 1.2.3.0 netmask 255.255.255.0 {", - "\toption routers 1.2.3.1;", - "}" - ]) - - self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24"), router_idx=10, range=(20, 30), unknown_clients='allow'), [ - "subnet 1.2.3.0 netmask 255.255.255.0 {", - "\toption routers 1.2.3.10;", - "\trange 1.2.3.20 1.2.3.30;", - "\tallow unknown-clients;", - "}" - ]) - - def test_config (self) : - self.assert_obj(dhcp.Config( - settings = { 'foo-setting': 'someval' }, - options = { 'bar-opt': ['one', 'two'] }, - shared_network = "FOO-NET", - subnets = [ - dhcp.Subnet(addr.Network("1.2.3.0/24")) - ], - hosts = [ - dhcp.Host("testhost", addr.MAC("12:34:56:78:90:ab"), addr.IP("1.2.3.4")) - ], - ), [ - "foo-setting someval;", - "option bar-opt one, two;", - "shared-network FOO-NET {", - "\tsubnet 1.2.3.0 netmask 255.255.255.0 {", - "\t\toption routers 1.2.3.1;", - "\t}", - "}", - "host testhost {", - "\thardware ethernet 12:34:56:78:90:ab;", - "\tfixed-address 1.2.3.4;", - "}" - ]) - -if __name__ == '__main__' : - unittest.main() -