# HG changeset patch # User Tero Marttila # Date 1238701946 -10800 # Node ID 8b633782f02d1707d41c64458a561c080143e0ae # Parent ff98fa9b84ce30e14bf13746f8fb5511690a71b4 write bind_conf.py diff -r ff98fa9b84ce -r 8b633782f02d addr.py --- a/addr.py Thu Apr 02 21:11:01 2009 +0300 +++ b/addr.py Thu Apr 02 22:52:26 2009 +0300 @@ -15,6 +15,20 @@ """ 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) : """ diff -r ff98fa9b84ce -r 8b633782f02d bind_conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bind_conf.py Thu Apr 02 22:52:26 2009 +0300 @@ -0,0 +1,380 @@ +""" + 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__(comment) + conf.File.__init__(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 + +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 ff98fa9b84ce -r 8b633782f02d conf.py --- a/conf.py Thu Apr 02 21:11:01 2009 +0300 +++ b/conf.py Thu Apr 02 22:52:26 2009 +0300 @@ -65,6 +65,7 @@ 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 diff -r ff98fa9b84ce -r 8b633782f02d dhcp.py --- a/dhcp.py Thu Apr 02 21:11:01 2009 +0300 +++ b/dhcp.py Thu Apr 02 22:52:26 2009 +0300 @@ -10,7 +10,7 @@ """ def __init__ (self, name=dhcpc.ConfFile.DEFAULT_NAME, path=dhcpc.ConfFile.DEFAULT_PATH, - settings=None, options=None, shared_network=False, subnets=None, hosts=None + settings=None, options=None, shared_network=False, subnets=None, hosts=None, comment=None ) : """ Create a full configuration file for the given settings: @@ -23,7 +23,7 @@ """ - dhcpc.ConfFile.__init__(self, name, path) + dhcpc.ConfFile.__init__(self, name, path, comment=comment) # define global settings if settings : @@ -49,7 +49,7 @@ 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) : + 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 @@ -65,7 +65,7 @@ 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) : @@ -73,9 +73,9 @@ A host declaration with a hardware address and a IP address """ - def __init__ (self, hostname, mac_addr, ip_addr) : + 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 ff98fa9b84ce -r 8b633782f02d dhcp_conf.py --- a/dhcp_conf.py Thu Apr 02 21:11:01 2009 +0300 +++ b/dhcp_conf.py Thu Apr 02 22:52:26 2009 +0300 @@ -115,13 +115,13 @@ DEFAULT_NAME = "dhcpd.conf" DEFAULT_PATH = "/etc/dhcp3/dhcpd.conf" - def __init__ (self, name=DEFAULT_NAME, path=DEFAULT_PATH, params=None, decls=None) : + 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) + Section.__init__(self, params, decls, comment) class Statement (Object) : """ @@ -270,14 +270,12 @@ } """ - def __init__ (self, name, params=[], decls=[]) : + def __init__ (self, name, *args, **kwargs) : """ @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) + super(SharedNetwork, self).__init__("shared-network", [name], *args, **kwargs) class Subnet (Declaration) : """ @@ -290,14 +288,12 @@ } """ - def __init__ (self, network, params=None, decls=None) : + def __init__ (self, network, *args, **kwargs) : """ @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) + super(Subnet, self).__init__("subnet", [network.net(), "netmask", network.netmask()], *args, **kwargs) class Group (Declaration) : """ @@ -309,8 +305,8 @@ } """ - def __init__ (self, params=None, decls=None) : - super(Group, self).__init__("group", [], params, decls) + def __init__ (self, *args, **kwargs) : + super(Group, self).__init__("group", [], *args, **kwargs) class Host (Declaration) : @@ -328,18 +324,18 @@ } """ - def __init__ (self, hostname, params=None, decls=None) : - super(Host, self).__init__("host", [hostname], params, decls) + 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) : + def __init__ (self, name, *args, **kwargs) : """ Formatted as a Satement with a name of "option ". """ - super(Option, self).__init__("option %s" % name, *args) + super(Option, self).__init__("option %s" % name, *args, **kwargs) diff -r ff98fa9b84ce -r 8b633782f02d settings/hosts.py --- a/settings/hosts.py Thu Apr 02 21:11:01 2009 +0300 +++ b/settings/hosts.py Thu Apr 02 22:52:26 2009 +0300 @@ -14,8 +14,8 @@ 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'), + 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"), ] hosts = [ diff -r ff98fa9b84ce -r 8b633782f02d test_bind.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_bind.py Thu Apr 02 22:52:26 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 ff98fa9b84ce -r 8b633782f02d test_conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_conf.py Thu Apr 02 22:52:26 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 ff98fa9b84ce -r 8b633782f02d test_dhcp.py --- a/test_dhcp.py Thu Apr 02 21:11:01 2009 +0300 +++ b/test_dhcp.py Thu Apr 02 22:52:26 2009 +0300 @@ -1,22 +1,14 @@ #!/usr/bin/env python2.5 """ - Test conf_dhcp + Test dhcp_conf """ -import dhcp_conf as dhcpc, dhcp, addr +import dhcp_conf as dhcpc +import test_conf, dhcp, addr import unittest -class _TestConfObj (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) - -class TestDHCPConf (_TestConfObj) : +class TestDHCPConf (test_conf._TestConfBase) : def assert_stmt (self, stmt, line) : """ Formats the given Statement, and compares the output against the given line. @@ -120,7 +112,8 @@ 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")), [ + 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;", @@ -128,7 +121,8 @@ ]) def test_subnet (self) : - self.assert_obj(dhcp.Subnet(addr.Network("1.2.3.0/24")), [ + 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;", "}"