312 lines
11 KiB
Python
312 lines
11 KiB
Python
|
#-----------------------------------------------------------------------------
|
||
|
# Copyright (c) 2008-2012, 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
|
||
|
|
||
|
#-----------------------------------------------------------------------------
|
||
|
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 hasattr(ipglob, 'split'):
|
||
|
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)
|