#----------------------------------------------------------------------------- # Copyright (c) 2008-2015, 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 greater 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)