diff options
Diffstat (limited to 'netaddr/ip/glob.py')
-rw-r--r-- | netaddr/ip/glob.py | 312 |
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) |