pvl.dns: split up modules and import partial-prefix parsing from pvl.hosts-dns
authorTero Marttila <tero.marttila@aalto.fi>
Tue, 24 Feb 2015 21:36:53 +0200
changeset 456 602838dfb6e7
parent 455 595e370e9970
child 457 1e925a1cc8de
pvl.dns: split up modules and import partial-prefix parsing from pvl.hosts-dns
pvl/dns/__init__.py
pvl/dns/generate.py
pvl/dns/labels.py
pvl/dns/reverse.py
pvl/dns/zone.py
--- a/pvl/dns/__init__.py	Tue Feb 24 19:46:05 2015 +0200
+++ b/pvl/dns/__init__.py	Tue Feb 24 21:36:53 2015 +0200
@@ -1,4 +1,21 @@
 
 __version__ = '0.1'
 
-from pvl.dns.zone import join, fqdn
+from pvl.dns.generate import (
+        parse_generate_field,
+        parse_generate_range,
+)
+from pvl.dns.labels import (
+        fqdn,
+        join,
+        relative,
+)
+from pvl.dns.reverse import (
+        reverse_label,
+        parse_prefix,
+)
+from pvl.dns.zone import (
+        ZoneLineError,
+        ZoneDirective,
+        ZoneRecord,
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/dns/generate.py	Tue Feb 24 21:36:53 2015 +0200
@@ -0,0 +1,136 @@
+class OffsetValue (object) :
+    """
+        Magic for $GENERATE offsets.
+
+        >>> OffsetValue(0)[1]
+        1
+        >>> OffsetValue(10)[5]
+        15
+    """
+
+    def __init__ (self, base) :
+        self.base = base
+
+    def __getitem__ (self, offset) :
+        return self.base + offset
+
+def parse_generate_field (field) :
+    """
+        Parse a $GENERATE lhs/rhs field:
+            $
+            ${<offset>[,<width>[,<base>]]}
+            \$
+            $$
+
+        Returns a wrapper that builds the field-value when called with the index.
+        
+        >>> parse_generate_field("foo")(1)
+        'foo'
+        >>> parse_generate_field("foo-$")(1)
+        'foo-1'
+        >>> parse_generate_field("foo-$$")(1)
+        'foo-$'
+        >>> parse_generate_field("\$")(1)
+        '$'
+        >>> parse_generate_field("10.0.0.${100}")(1)
+        '10.0.0.101'
+        >>> parse_generate_field("foo-${0,2,d}")(1)
+        'foo-01'
+
+    """
+
+    input = field
+    expr = []
+
+    while '$' in field :
+        # defaults
+        offset = 0
+        width = 0
+        base = 'd'
+        escape = False
+
+        # different forms
+        if '${' in field :
+            pre, body = field.split('${', 1)
+            body, post = body.split('}', 1)
+
+            # parse body
+            parts = body.split(',')
+
+            # offset
+            offset = int(parts.pop(0))
+
+            # width
+            if parts :
+                width = int(parts.pop(0))
+
+            # base
+            if parts :
+                base = parts.pop(0)
+            
+            if parts:
+                # fail
+                raise ValueError("extra data in ${...} body: {0!r}", parts)
+
+        elif '$$' in field :
+            pre, post = field.split('$$', 1)
+            escape = True
+
+        elif '\\$' in field :
+            pre, post = field.split('\\$', 1)
+            escape = True
+
+        else :
+            pre, post = field.split('$', 1)
+        
+        expr.append(pre)
+
+        if escape :
+            expr.append('$')
+
+        else :
+            # meta-format
+            fmt = '{value[%d]:0%d%s}' % (offset, width, base)
+
+            log.debug("field=%r -> pre=%r, fmt=%r, post=%r", field, expr, fmt, post)
+
+            expr.append(fmt)
+
+        field = post
+
+    # final
+    if field :
+        expr.append(field)
+    
+    # combine
+    expr = ''.join(expr)
+
+    log.debug("%s: %s", input, expr)
+
+    # processed
+    def value_func (value) :
+        # magic wrapper to implement offsets
+        return expr.format(value=OffsetValue(value))
+    
+    return value_func
+
+def parse_generate_range (field) :
+    """
+        Parse a <start>-<stop>[/<step>] field
+    """
+
+    if '/' in field :
+        field, step = field.split('/')
+        step = int(step)
+    else :
+        step = 1
+
+    start, stop = field.split('-')
+    start = int(start)
+    stop = int(stop)
+
+    log.debug("  range: start=%r, stop=%r, step=%r", start, stop, step)
+
+    # inclusive
+    return range(start, stop + 1, step)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/dns/labels.py	Tue Feb 24 21:36:53 2015 +0200
@@ -0,0 +1,32 @@
+# TODO: support fqdns in parts
+def join (*parts) :
+    """
+        Join a domain name from labels.
+    """
+
+    return '.'.join(str(part) for part in parts)
+
+def fqdn (*parts) :
+    """
+        Return an FQND from parts, ending in .
+    """
+
+    fqdn = join(*parts)
+
+    # we may be given an fqdn in parts
+    if not fqdn.endswith('.') :
+        fqdn += '.'
+    
+    return fqdn
+
+def relative (origin, fqdn):
+    """
+        Determine the relative label from the given zone origin to the given host fqnd.
+    """
+
+    if fqdn.endswith('.' + origin) :
+        # strip
+        return fqdn[:(len(fqdn) - len(origin) - 1)]
+    else:
+        raise ValueError("{fqdn} outside of zone {origin}".format(fqdn=fqdn, origin=origin))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/dns/reverse.py	Tue Feb 24 21:36:53 2015 +0200
@@ -0,0 +1,140 @@
+import ipaddr
+import math
+
+def reverse_ipv4 (ip) :
+    """
+        Return in-addr.arpa reverse for given IPv4 prefix.
+    """
+
+    # parse
+    octets = tuple(int(part) for part in ip.split('.'))
+
+    for octet in octets :
+        assert 0 <= octet <= 255
+
+    return '.'.join([str(octet) for octet in reversed(octets)] + ['in-addr', 'arpa'])
+
+def reverse_ipv6 (ip6) :
+    """
+        Return ip6.arpa reverse for given IPv6 prefix.
+    """
+
+    # XXX: this is broken for fd80::1?
+    parts = [int(part, 16) for part in ip6.split(':')]
+    parts = ['{0:04x}'.format(part) for part in parts]
+    parts = ''.join(parts)
+
+    return '.'.join(tuple(reversed(parts)) + ( 'ip6', 'arpa'))
+
+def reverse_label (prefix, address) :
+    """
+        Determine the correct label for the given IP address within the reverse zone for the given prefix.
+
+        This includes all suffix octets (partially) covered by the prefix.
+    """
+
+    assert prefix.version == address.version
+    assert address in prefix
+    
+    hostbits = prefix.max_prefixlen - prefix.prefixlen
+
+    if prefix.version == 4 :
+        # pack into octets
+        octets = [ord(x) for x in address.packed]
+
+        # take the suffix
+        octets = octets[-int(math.ceil(hostbits / 8.0)):]
+        
+        # reverse in decimal
+        return '.'.join(reversed(["{0:d}".format(x) for x in octets]))
+
+    elif prefix.version == 6 :
+        # pack into nibbles
+        nibbles = [((ord(x) >> 4) & 0xf, ord(x) & 0xf) for x in address.packed]
+        nibbles = [nibble for nibblepair in nibbles for nibble in nibblepair]
+
+        # take the suffix
+        nibbles = nibbles[-(hostbits / 4):]
+        
+        # reverse in hex
+        return '.'.join(reversed(["{0:x}".format(x) for x in nibbles]))
+
+    else :
+        raise ValueError("unsupported address version: %s" % (prefix, ))
+
+def _split_ipv6_parts (prefix) :
+    """
+        Split a partial IPv6 address into hexadecimal nibbles
+    """
+
+    if prefix.endswith('::'):
+        prefix = prefix[:-2]
+
+    for hextet in prefix.split(':') :
+        for nibble in hextet.rjust(4, '0') :
+            yield nibble
+
+def _build_ipv6_parts (parts) :
+    """
+        Group an iterable of hexadecimal nibbles into hextets.
+    """
+
+    for i in xrange(0, len(parts), 4) :
+        yield ''.join(parts[i:i+4])
+    
+    # suffix ::
+    if len(parts) < 32 :
+        yield ''
+        yield ''
+
+def parse_prefix (prefix) :
+    """
+        Return an ipaddr.IPNetwork from given IPv4/IPv6 prefix.
+
+        >>> parse_prefix('127.0.0.0/8')
+        IPv4Network('127.0.0.0/8')
+        >>> parse_prefix('192.0.2.128/26')
+        IPv4Network('192.0.2.128/26')
+        >>> parse_prefix('192.0.2.128-26')
+        IPv4Network('192.0.2.128-26')
+        >>> parse_prefix('127.')
+        IPv4Network('127.0.0.0/8')
+        >>> parse_prefix('10')
+        IPv4Network('10.0.0.0/8')
+        >>> parse_prefix('192.168')
+        IPv4Network('192.168.0.0/16')
+        >>> parse_prefix('fe80::')
+        IPv6Network('fe80::/16')
+        >>> parse_prefix('2001:db8::')
+        IPv6Network('2001:db8::/32')
+        >>> parse_prefix('2001:db8:1:2')
+        IPv6Network('2001:db8:1:2::/64')
+    """
+
+    if '/' in prefix :
+        return ipaddr.IPNetwork(prefix)
+    
+    elif '-' in prefix :
+        return ipaddr.IPNetwork(prefix.replace('-', '/'))
+
+    elif '.' in prefix or prefix.isdigit() :
+        parts = prefix.rstrip('.').split('.')
+        prefixlen = len(parts) * 8
+        
+        return ipaddr.IPv4Network('{prefix}/{prefixlen}'.format(
+            prefix      = '.'.join(parts + ['0' for i in xrange(4 - len(parts))]),
+            prefixlen   = prefixlen,
+        ))
+   
+    elif ':' in prefix :
+        parts = list(_split_ipv6_parts(prefix))
+        prefixlen = len(parts) * 4
+
+        return ipaddr.IPv6Network('{prefix}/{prefixlen}'.format(
+            prefix      = ':'.join(_build_ipv6_parts(parts)),
+            prefixlen   = prefixlen,
+        ))
+
+    else :
+        raise ValueError("Unrecognized IP prefix string: %s" % (prefix, ))
+
--- a/pvl/dns/zone.py	Tue Feb 24 19:46:05 2015 +0200
+++ b/pvl/dns/zone.py	Tue Feb 24 21:36:53 2015 +0200
@@ -23,6 +23,37 @@
     def __init__ (self, line, msg, *args, **kwargs) :
         super(ZoneLineError, self).__init__("%s: %s" % (line, msg.format(*args, **kwargs)))
 
+def process_generate (line, origin, parts) :
+    """
+        Process a 
+            $GENERATE <start>-<stop>[/<step>] lhs [ttl] [class] type rhs [comment]
+        directive into a series of ZoneResource's.
+
+        Raises ZoneLineError
+    """
+
+    parts = list(parts)
+    
+    try:
+        range = parse_generate_range(parts.pop(0))
+
+        lhs_func = parse_generate_field(parts.pop(0), line=line)
+        rhs_func = parse_generate_field(parts.pop(-1), line=line)
+    except ValueError as error:
+        raise ZoneLineError(line, "{error}", error=error)
+
+    body = parts
+
+    for i in range :
+        # build
+        parts = [lhs_func(i)] + body + [rhs_func(i)]
+
+        log.debug(" %03d: %r", i, parts)
+
+        # parse
+        yield ZoneRecord.parse(line, parts=parts, origin=origin)
+
+
 class ZoneLine (object) :
     """
         A line parsed from a zonefile.
@@ -467,245 +498,3 @@
         self.expire = expire
         self.nxttl = nxttl
 
-class OffsetValue (object) :
-    """
-        Magic for $GENERATE offsets.
-
-        >>> OffsetValue(0)[1]
-        1
-        >>> OffsetValue(10)[5]
-        15
-    """
-
-    def __init__ (self, base) :
-        self.base = base
-
-    def __getitem__ (self, offset) :
-        return self.base + offset
-
-def parse_generate_field (field, line=None) :
-    """
-        Parse a $GENERATE lhs/rhs field:
-            $
-            ${<offset>[,<width>[,<base>]]}
-            \$
-            $$
-
-        Returns a wrapper that builds the field-value when called with the index.
-        
-        >>> parse_generate_field("foo")(1)
-        'foo'
-        >>> parse_generate_field("foo-$")(1)
-        'foo-1'
-        >>> parse_generate_field("foo-$$")(1)
-        'foo-$'
-        >>> parse_generate_field("\$")(1)
-        '$'
-        >>> parse_generate_field("10.0.0.${100}")(1)
-        '10.0.0.101'
-        >>> parse_generate_field("foo-${0,2,d}")(1)
-        'foo-01'
-
-    """
-
-    input = field
-    expr = []
-
-    while '$' in field :
-        # defaults
-        offset = 0
-        width = 0
-        base = 'd'
-        escape = False
-
-        # different forms
-        if '${' in field :
-            pre, body = field.split('${', 1)
-            body, post = body.split('}', 1)
-
-            # parse body
-            parts = body.split(',')
-
-            # offset
-            offset = int(parts.pop(0))
-
-            # width
-            if parts :
-                width = int(parts.pop(0))
-
-            # base
-            if parts :
-                base = parts.pop(0)
-            
-            if parts:
-                # fail
-                raise ZoneLineError(line, "extra data in ${...} body: {0!r}", parts)
-
-        elif '$$' in field :
-            pre, post = field.split('$$', 1)
-            escape = True
-
-        elif '\\$' in field :
-            pre, post = field.split('\\$', 1)
-            escape = True
-
-        else :
-            pre, post = field.split('$', 1)
-        
-        expr.append(pre)
-
-        if escape :
-            expr.append('$')
-
-        else :
-            # meta-format
-            fmt = '{value[%d]:0%d%s}' % (offset, width, base)
-
-            log.debug("field=%r -> pre=%r, fmt=%r, post=%r", field, expr, fmt, post)
-
-            expr.append(fmt)
-
-        field = post
-
-    # final
-    if field :
-        expr.append(field)
-    
-    # combine
-    expr = ''.join(expr)
-
-    log.debug("%s: %s", input, expr)
-
-    # processed
-    def value_func (value) :
-        # magic wrapper to implement offsets
-        return expr.format(value=OffsetValue(value))
-    
-    return value_func
-
-def parse_generate_range (field) :
-    """
-        Parse a <start>-<stop>[/<step>] field
-    """
-
-    if '/' in field :
-        field, step = field.split('/')
-        step = int(step)
-    else :
-        step = 1
-
-    start, stop = field.split('-')
-    start = int(start)
-    stop = int(stop)
-
-    log.debug("  range: start=%r, stop=%r, step=%r", start, stop, step)
-
-    # inclusive
-    return range(start, stop + 1, step)
-
-def process_generate (line, origin, parts) :
-    """
-        Process a 
-            $GENERATE <start>-<stop>[/<step>] lhs [ttl] [class] type rhs [comment]
-        directive into a series of ZoneResource's.
-    """
-
-    parts = list(parts)
-
-    range = parse_generate_range(parts.pop(0))
-
-    lhs_func = parse_generate_field(parts.pop(0), line=line)
-    rhs_func = parse_generate_field(parts.pop(-1), line=line)
-    body = parts
-
-    for i in range :
-        # build
-        parts = [lhs_func(i)] + body + [rhs_func(i)]
-
-        log.debug(" %03d: %r", i, parts)
-
-        # parse
-        yield ZoneRecord.parse(line, parts=parts, origin=origin)
-
-   
-def reverse_ipv4 (ip) :
-    """
-        Return in-addr.arpa reverse for given IPv4 prefix.
-    """
-
-    # parse
-    octets = tuple(int(part) for part in ip.split('.'))
-
-    for octet in octets :
-        assert 0 <= octet <= 255
-
-    return '.'.join([str(octet) for octet in reversed(octets)] + ['in-addr', 'arpa'])
-
-def reverse_ipv6 (ip6) :
-    """
-        Return ip6.arpa reverse for given IPv6 prefix.
-    """
-
-    parts = [int(part, 16) for part in ip6.split(':')]
-    parts = ['{0:04x}'.format(part) for part in parts]
-    parts = ''.join(parts)
-
-    return '.'.join(tuple(reversed(parts)) + ( 'ip6', 'arpa'))
-
-# TODO: support fqdns in parts
-def join (*parts) :
-    """
-        Join a domain name from labels.
-    """
-
-    return '.'.join(str(part) for part in parts)
-
-def fqdn (*parts) :
-    """
-        Return an FQND from parts, ending in .
-    """
-
-    fqdn = join(*parts)
-
-    # we may be given an fqdn in parts
-    if not fqdn.endswith('.') :
-        fqdn += '.'
-    
-    return fqdn
-
-def reverse_label (prefix, address) :
-    """
-        Determine the correct label for the given IP address within the reverse zone for the given prefix.
-
-        This includes all suffix octets (partially) covered by the prefix.
-    """
-
-    assert prefix.version == address.version
-    
-    hostbits = prefix.max_prefixlen - prefix.prefixlen
-
-    if prefix.version == 4 :
-        # pack into octets
-        octets = [ord(x) for x in address.packed]
-
-        # take the suffix
-        octets = octets[-int(math.ceil(hostbits / 8.0)):]
-        
-        # reverse in decimal
-        return '.'.join(reversed(["{0:d}".format(x) for x in octets]))
-
-    elif prefix.version == 6 :
-        # pack into nibbles
-        nibbles = [((ord(x) >> 4) & 0xf, ord(x) & 0xf) for x in address.packed]
-        nibbles = [nibble for nibblepair in nibbles for nibble in nibblepair]
-
-        # take the suffix
-        nibbles = nibbles[-(hostbits / 4):]
-        
-        # reverse in hex
-        return '.'.join(reversed(["{0:x}".format(x) for x in nibbles]))
-
-    else :
-        raise ValueError("unsupported address version: %s" % (prefix, ))
-
-