--- 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, ))
-
-