summaryrefslogtreecommitdiffstats
path: root/netaddr/ip/glob.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--netaddr/ip/glob.py312
1 files changed, 312 insertions, 0 deletions
diff --git a/netaddr/ip/glob.py b/netaddr/ip/glob.py
new file mode 100644
index 0000000..ed43b1b
--- /dev/null
+++ b/netaddr/ip/glob.py
@@ -0,0 +1,312 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2008 by David P. D. Moss. All rights reserved.
+#
+# Released under the BSD license. See the LICENSE file for details.
+#-----------------------------------------------------------------------------
+"""
+Routines and classes for supporting and expressing IP address ranges using a
+glob style syntax.
+
+"""
+from netaddr.core import AddrFormatError, AddrConversionError
+from netaddr.ip import IPRange, IPAddress, IPNetwork, iprange_to_cidrs
+from netaddr.compat import _is_str
+
+
+def valid_glob(ipglob):
+ """
+ :param ipglob: An IP address range in a glob-style format.
+
+ :return: ``True`` if IP range glob is valid, ``False`` otherwise.
+ """
+ #TODO: Add support for abbreviated ipglobs.
+ #TODO: e.g. 192.0.*.* == 192.0.*
+ #TODO: *.*.*.* == *
+ #TODO: Add strict flag to enable verbose ipglob checking.
+ if not _is_str(ipglob):
+ return False
+
+ seen_hyphen = False
+ seen_asterisk = False
+
+ octets = ipglob.split('.')
+
+ if len(octets) != 4:
+ return False
+
+ for octet in octets:
+ if '-' in octet:
+ if seen_hyphen:
+ return False
+ seen_hyphen = True
+ if seen_asterisk:
+ # Asterisks cannot precede hyphenated octets.
+ return False
+ try:
+ (octet1, octet2) = [int(i) for i in octet.split('-')]
+ except ValueError:
+ return False
+ if octet1 >= octet2:
+ return False
+ if not 0 <= octet1 <= 254:
+ return False
+ if not 1 <= octet2 <= 255:
+ return False
+ elif octet == '*':
+ seen_asterisk = True
+ else:
+ if seen_hyphen is True:
+ return False
+ if seen_asterisk is True:
+ return False
+ try:
+ if not 0 <= int(octet) <= 255:
+ return False
+ except ValueError:
+ return False
+ return True
+
+
+def glob_to_iptuple(ipglob):
+ """
+ A function that accepts a glob-style IP range and returns the component
+ lower and upper bound IP address.
+
+ :param ipglob: an IP address range in a glob-style format.
+
+ :return: a tuple contain lower and upper bound IP objects.
+ """
+ if not valid_glob(ipglob):
+ raise AddrFormatError('not a recognised IP glob range: %r!' % (ipglob,))
+
+ start_tokens = []
+ end_tokens = []
+
+ for octet in ipglob.split('.'):
+ if '-' in octet:
+ tokens = octet.split('-')
+ start_tokens.append(tokens[0])
+ end_tokens.append(tokens[1])
+ elif octet == '*':
+ start_tokens.append('0')
+ end_tokens.append('255')
+ else:
+ start_tokens.append(octet)
+ end_tokens.append(octet)
+
+ return IPAddress('.'.join(start_tokens)), IPAddress('.'.join(end_tokens))
+
+
+def glob_to_iprange(ipglob):
+ """
+ A function that accepts a glob-style IP range and returns the equivalent
+ IP range.
+
+ :param ipglob: an IP address range in a glob-style format.
+
+ :return: an IPRange object.
+ """
+ if not valid_glob(ipglob):
+ raise AddrFormatError('not a recognised IP glob range: %r!' % (ipglob,))
+
+ start_tokens = []
+ end_tokens = []
+
+ for octet in ipglob.split('.'):
+ if '-' in octet:
+ tokens = octet.split('-')
+ start_tokens.append(tokens[0])
+ end_tokens.append(tokens[1])
+ elif octet == '*':
+ start_tokens.append('0')
+ end_tokens.append('255')
+ else:
+ start_tokens.append(octet)
+ end_tokens.append(octet)
+
+ return IPRange('.'.join(start_tokens), '.'.join(end_tokens))
+
+
+def iprange_to_globs(start, end):
+ """
+ A function that accepts an arbitrary start and end IP address or subnet
+ and returns one or more glob-style IP ranges.
+
+ :param start: the start IP address or subnet.
+
+ :param end: the end IP address or subnet.
+
+ :return: a list containing one or more IP globs.
+ """
+ start = IPAddress(start)
+ end = IPAddress(end)
+
+ if start.version != 4 and end.version != 4:
+ raise AddrConversionError('IP glob ranges only support IPv4!')
+
+ def _iprange_to_glob(lb, ub):
+ # Internal function to process individual IP globs.
+ t1 = [int(_) for _ in str(lb).split('.')]
+ t2 = [int(_) for _ in str(ub).split('.')]
+
+ tokens = []
+
+ seen_hyphen = False
+ seen_asterisk = False
+
+ for i in range(4):
+ if t1[i] == t2[i]:
+ # A normal octet.
+ tokens.append(str(t1[i]))
+ elif (t1[i] == 0) and (t2[i] == 255):
+ # An asterisk octet.
+ tokens.append('*')
+ seen_asterisk = True
+ else:
+ # Create a hyphenated octet - only one allowed per IP glob.
+ if not seen_asterisk:
+ if not seen_hyphen:
+ tokens.append('%s-%s' % (t1[i], t2[i]))
+ seen_hyphen = True
+ else:
+ raise AddrConversionError(
+ 'only 1 hyphenated octet per IP glob allowed!')
+ else:
+ raise AddrConversionError(
+ "asterisks are not allowed before hyphenated octets!")
+
+ return '.'.join(tokens)
+
+ globs = []
+
+ try:
+ # IP range can be represented by a single glob.
+ ipglob = _iprange_to_glob(start, end)
+ if not valid_glob(ipglob):
+ #TODO: this is a workaround, it is produces non-optimal but valid
+ #TODO: glob conversions. Fix inner function so that is always
+ #TODO: produces a valid glob.
+ raise AddrConversionError('invalid ip glob created')
+ globs.append(ipglob)
+ except AddrConversionError:
+ # Break IP range up into CIDRs before conversion to globs.
+ #
+ #TODO: this is still not completely optimised but is good enough
+ #TODO: for the moment.
+ #
+ for cidr in iprange_to_cidrs(start, end):
+ ipglob = _iprange_to_glob(cidr[0], cidr[-1])
+ globs.append(ipglob)
+
+ return globs
+
+
+def glob_to_cidrs(ipglob):
+ """
+ A function that accepts a glob-style IP range and returns a list of one
+ or more IP CIDRs that exactly matches it.
+
+ :param ipglob: an IP address range in a glob-style format.
+
+ :return: a list of one or more IP objects.
+ """
+ return iprange_to_cidrs(*glob_to_iptuple(ipglob))
+
+
+def cidr_to_glob(cidr):
+ """
+ A function that accepts an IP subnet in a glob-style format and returns
+ a list of CIDR subnets that exactly matches the specified glob.
+
+ :param cidr: an IP object CIDR subnet.
+
+ :return: a list of one or more IP addresses and subnets.
+ """
+ ip = IPNetwork(cidr)
+ globs = iprange_to_globs(ip[0], ip[-1])
+ if len(globs) != 1:
+ # There should only ever be a one to one mapping between a CIDR and
+ # an IP glob range.
+ raise AddrConversionError('bad CIDR to IP glob conversion!')
+ return globs[0]
+
+
+class IPGlob(IPRange):
+ """
+ Represents an IP address range using a glob-style syntax ``x.x.x-y.*``
+
+ Individual octets can be represented using the following shortcuts :
+
+ 1. ``*`` - the asterisk octet (represents values ``0`` through ``255``)
+ 2. ``x-y`` - the hyphenated octet (represents values ``x`` through ``y``)
+
+ A few basic rules also apply :
+
+ 1. ``x`` must always be less than ``y``, therefore :
+
+ - ``x`` can only be ``0`` through ``254``
+ - ``y`` can only be ``1`` through ``255``
+
+ 2. only one hyphenated octet per IP glob is allowed
+ 3. only asterisks are permitted after a hyphenated octet
+
+ Examples:
+
+ +------------------+------------------------------+
+ | IP glob | Description |
+ +==================+==============================+
+ | ``192.0.2.1`` | a single address |
+ +------------------+------------------------------+
+ | ``192.0.2.0-31`` | 32 addresses |
+ +------------------+------------------------------+
+ | ``192.0.2.*`` | 256 addresses |
+ +------------------+------------------------------+
+ | ``192.0.2-3.*`` | 512 addresses |
+ +------------------+------------------------------+
+ | ``192.0-1.*.*`` | 131,072 addresses |
+ +------------------+------------------------------+
+ | ``*.*.*.*`` | the whole IPv4 address space |
+ +------------------+------------------------------+
+
+ .. note :: \
+ IP glob ranges are not directly equivalent to CIDR blocks. \
+ They can represent address ranges that do not fall on strict bit mask \
+ boundaries. They are suitable for use in configuration files, being \
+ more obvious and readable than their CIDR counterparts, especially for \
+ admins and end users with little or no networking knowledge or \
+ experience. All CIDR addresses can always be represented as IP globs \
+ but the reverse is not always true.
+ """
+ __slots__ = ('_glob',)
+
+ def __init__(self, ipglob):
+ (start, end) = glob_to_iptuple(ipglob)
+ super(IPGlob, self).__init__(start, end)
+ self.glob = iprange_to_globs(self._start, self._end)[0]
+
+ def __getstate__(self):
+ """:return: Pickled state of an `IPGlob` object."""
+ return super(IPGlob, self).__getstate__()
+
+ def __setstate__(self, state):
+ """:param state: data used to unpickle a pickled `IPGlob` object."""
+ super(IPGlob, self).__setstate__(state)
+ self.glob = iprange_to_globs(self._start, self._end)[0]
+
+ def _get_glob(self):
+ return self._glob
+
+ def _set_glob(self, ipglob):
+ (self._start, self._end) = glob_to_iptuple(ipglob)
+ self._glob = iprange_to_globs(self._start, self._end)[0]
+
+ glob = property(_get_glob, _set_glob, None,
+ 'an arbitrary IP address range in glob format.')
+
+ def __str__(self):
+ """:return: IP glob in common representational format."""
+ return "%s" % self.glob
+
+ def __repr__(self):
+ """:return: Python statement to create an equivalent object"""
+ return "%s('%s')" % (self.__class__.__name__, self.glob)