--- 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) :
"""
--- /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:
+ ";" <comment>
+ """
+
+ for comment in self.comments :
+ if comment is not None :
+ yield "; %s" % (comment, )
+
+class ZoneFile (Object, conf.File) :
+ """
+ A zone file containing a bunch of directives, resource records and comments
+ """
+
+ def __init__ (self, name, path, ttl=None, comment=None) :
+ """
+ @param name the name of the zonefile, for status stuff
+ @param path the path to the zonefile
+ @param ttl default TTL to use
+ @param comment optional comments
+ """
+
+ Object.__init__(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)
+
--- 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
--- 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)
--- 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 <name>".
"""
- super(Option, self).__init__("option %s" % name, *args)
+ super(Option, self).__init__("option %s" % name, *args, **kwargs)
--- 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 = [
--- /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()
+
--- /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)
+
+
--- 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;",
"}"