Package Products :: Package ZenUtils :: Module IpUtil |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenUtils.IpUtil

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2007, 2009 Zenoss Inc.
      5  #
      6  # This program is free software; you can redistribute it and/or modify it
      7  # under the terms of the GNU General Public License version 2 or (at your
      8  # option) any later version as published by the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__ = """IpUtil
     15
     16  IPv4 and IPv6 utility functions
     17
     18  Internally, IPv6 addresses will use another character rather than ':' as an underscore
     19  is representable in URLs.  Zope object names *MUST* be okay in URLs.
     20
     21  """
     22
     23  import re
     24  import socket
     25
     26  from ipaddr import IPAddress, IPNetwork
     27
     28  from Products.ZenUtils.Exceptions import ZentinelException
     29  from twisted.names.client import lookupPointer
     30  from twisted.internet import threads
     31
     32  IP_DELIM = '..'
     33  INTERFACE_DELIM = '...'
     34
     35 -def ipwrap(ip):
     36      """
     37      Convert IP addresses to a Zope-friendly format.
     38      """
     39      if isinstance(ip, str):
     40          wrapped = ip.replace(':', IP_DELIM)
     41          return wrapped.replace('%', INTERFACE_DELIM)
     42      return ip
     43
     44 -def ipunwrap(ip):
     45      """
     46      Convert IP addresses from a Zope-friendly format to a real address.
     47      """
     48      if isinstance(ip, str):
     49          unwrapped = ip.replace(IP_DELIM, ':')
     50          return unwrapped.replace(INTERFACE_DELIM, '%')
     51      return ip
     52
     53 -def ipstrip(ip):
     54      "strip interface off link local IPv6 addresses"
     55      if '%' in ip:
     56          address = ip[:ip.index('%')]
     57      else:
     58          address = ip
     59      return address
     60
     61 -def ipunwrap_strip(ip):
     62      """
     63      ipunwrap + strip interface off link local IPv6 addresses
     64      """
     65      unwrapped = ipunwrap(ip)
     66      return ipstrip(unwrapped)
     67
     68 -def bytesToCanonIpv6(byteString):
     69      """
     70      SNMP provides a 16-byte index to table items, where the index
     71      represents the IPv6 address.
     72      Return an empty string or the canonicalized IPv6 address.
     73
     74      >>> bytesToCanonIpv6( ['254','128','0','0','0','0','0','0','2','80','86','255','254','138','46','210'])
     75      'fe80::250:56ff:fe8a:2ed2'
     76      >>> bytesToCanonIpv6( ['253','0','0','0','0','0','0','0','0','0','0','0','10','175','210','5'])
     77      'fd00::aaf:d205'
     78      >>> bytesToCanonIpv6( ['hello','world'])
     79      ''
     80      >>> bytesToCanonIpv6( ['253','0','0','0','0','0','0','0','0','0','0','0','10','175','210','5'])
     81      'fd00::aaf:d205'
     82      """
     83      # To form an IPv6 address, need to combine pairs of octets (in hex)
     84      # and join them with a colon
     85      try:
     86          left =  map(int, byteString[::2])
     87          right = map(int, byteString[1::2])
     88      except ValueError:
     89          return ''
     90      ipv6 = ':'.join("%x%02x" % tuple(x) for x in zip(left, right))
     91
     92      # Now canonicalize the IP
     93      ip = ''
     94      try:
     95          ip = str(IPAddress(ipv6))
     96      except ValueError:
     97          pass
     98      return ip
     99
    100
    101 -class IpAddressError(ZentinelException): pass
    102
    103 -class InvalidIPRangeError(Exception):
    104      """
    105      Attempted to parse an invalid IP range.
    106      """
    107
    108
    109 -def isip(ip):
    110      uIp = str(ipunwrap(ip))
    111      # Twisted monkey patches socket.inet_pton on systems that have IPv6
    112      # disabled and raise a ValueError instead of the expected socket.error
    113      # if the passed in address is invalid.
    114      try:
    115          socket.inet_pton(socket.AF_INET, uIp)
    116      except (socket.error, ValueError):
    117          try:
    118              socket.inet_pton(socket.AF_INET6, uIp)
    119          except (socket.error, ValueError):
    120              return False
    121      return True
    122
    123
    124 -def checkip(ip):
    125      """
    126      Check that an IPv4 address is valid. Return true
    127      or raise an exception(!)
    128
    129      >>> checkip('10.10.20.5')
    130      True
    131      >>> try: checkip(10)
    132      ... except IpAddressError, ex: print ex
    133      10 is an invalid address
    134      >>> try: checkip('10')
    135      ... except IpAddressError, ex: print ex
    136      10 is an invalid address
    137      >>> try: checkip('10.10.20.500')
    138      ... except IpAddressError, ex: print ex
    139      10.10.20.500 is an invalid address
    140      >>> checkip('10.10.20.0')
    141      True
    142      >>> checkip('10.10.20.255')
    143      True
    144      """
    145      if not isip(ip):
    146          raise IpAddressError( "%s is an invalid address" % ip )
    147      return True
    148
    149
    150 -def numbip(ip):
    151      """
    152      Convert a string IP to a decimal representation easier for
    153      calculating netmasks etc.
    154
    155      Deprecated in favour of ipToDecimal()
    156      """
    157      return ipToDecimal(ip)
    158
    159 -def ipToDecimal(ip):
    160      """
    161      Convert a string IP to a decimal representation easier for
    162      calculating netmasks etc.
    163
    164      >>> ipToDecimal('10.10.20.5')
    165      168432645L
    166      >>> try: ipToDecimal('10.10.20.500')
    167      ... except IpAddressError, ex: print ex
    168      10.10.20.500 is an invalid address
    169      """
    170      checkip(ip)
    171      # The unit tests expect to always get a long, while the
    172      # ipaddr.IPaddress class doesn't provide a direct "to long" capability
    173      unwrapped = ipunwrap(ip)
    174      if '%' in unwrapped:
    175          address = unwrapped[:unwrapped.index('%')]
    176      else:
    177          address = unwrapped
    178      return long(int(IPAddress(address)))
    179
    180 -def ipFromIpMask(ipmask):
    181      """
    182      Get just the IP address from an CIDR string like 1.1.1.1/24
    183
    184      >>> ipFromIpMask('1.1.1.1')
    185      '1.1.1.1'
    186      >>> ipFromIpMask('1.1.1.1/24')
    187      '1.1.1.1'
    188      """
    189      return ipmask.split("/")[0]
    190
    191 -def strip(ip):
    192      """
    193      Convert a numeric IP address to a string
    194
    195      Deprecated in favour of decimalIpToStr()
    196      """
    197      return decimalIpToStr(ip)
    198
    199 -def decimalIpToStr(ip):
    200      """
    201      Convert a decimal IP address (as returned by ipToDecimal)
    202      to a regular IPv4 dotted quad address.
    203
    204      >>> decimalIpToStr(ipToDecimal('10.23.44.57'))
    205      '10.23.44.57'
    206      """
    207      return str(IPAddress(ipunwrap(ip)))
    208
    209 -def hexToBits(hex):
    210      """
    211      Convert hex netbits (0xff000000) to decimal netmask (8)
    212
    213      >>> hexToBits("0xff000000")
    214      8
    215      >>> hexToBits("0xffffff00")
    216      24
    217      """
    218      return maskToBits(hexToMask(hex))
    219
    220
    221 -def hexToMask(hex):
    222      """
    223      Converts a netmask represented in hex to octets represented in
    224      decimal.
    225
    226      >>> hexToMask("0xffffff00")
    227      '255.255.255.0'
    228      >>> hexToMask("0xffffffff")
    229      '255.255.255.255'
    230      >>> hexToMask("0x00000000")
    231      '0.0.0.0'
    232      >>> hexToMask("trash")
    233      '255.255.255.255'
    234      """
    235      if hex.find('x') < 0:
    236          return "255.255.255.255"
    237
    238      hex = list(hex.lower().split('x')[1])
    239      octets = []
    240      while len(hex) > 0:
    241          snippit = list(hex.pop() + hex.pop())
    242          snippit.reverse()
    243          decimal = int(''.join(snippit), 16)
    244          octets.append(str(decimal))
    245
    246      octets.reverse()
    247      return '.'.join(octets)
    248
    249
    250 -def maskToBits(netmask):
    251      """
    252      Convert string rep of netmask to number of bits
    253
    254      >>> maskToBits('255.255.255.255')
    255      32
    256      >>> maskToBits('255.255.224.0')
    257      19
    258      >>> maskToBits('0.0.0.0')
    259      0
    260      """
    261      if isinstance(netmask, basestring) and '.' in netmask:
    262          test = 0xffffffffL
    263          if netmask[0]=='0': return 0
    264          masknumb = ipToDecimal(netmask)
    265          for i in range(32):
    266              if test == masknumb: return 32-i
    267              test = test - 2 ** i
    268          return None
    269      else:
    270          return int(netmask)
    271
    272
    273 -def bitsToMaskNumb(netbits):
    274      """
    275      Convert integer number of netbits to a decimal number
    276
    277      Deprecated in favour of bitsToDecimalMask()
    278      """
    279      return bitsToDecimalMask(netbits)
    280
    281 -def bitsToDecimalMask(netbits):
    282      """
    283      Convert integer number of netbits to a decimal number
    284
    285      >>> bitsToDecimalMask(32)
    286      4294967295L
    287      >>> bitsToDecimalMask(19)
    288      4294959104L
    289      >>> bitsToDecimalMask(0)
    290      0L
    291      """
    292      masknumb = 0L
    293      netbits=int(netbits)
    294      for i in range(32-netbits, 32):
    295          masknumb += 2L ** i
    296      return masknumb
    297
    298
    299 -def bitsToMask(netbits):
    300      """
    301      Convert netbits into a dotted-quad subnetmask
    302
    303      >>> bitsToMask(12)
    304      '255.240.0.0'
    305      >>> bitsToMask(0)
    306      '0.0.0.0'
    307      >>> bitsToMask(32)
    308      '255.255.255.255'
    309      """
    310      return decimalIpToStr(bitsToDecimalMask(netbits))
    311
    312
    313 -def getnet(ip, netmask):
    314      """
    315      Deprecated in favour of decimalNetFromIpAndNet()
    316      """
    317      return decimalNetFromIpAndNet(ip, netmask)
    318
    319 -def decimalNetFromIpAndNet(ip, netmask):
    320      """
    321      Get network address of IP as string netmask as in the form 255.255.255.0
    322
    323      >>> getnet('10.12.25.33', 24)
    324      168564992L
    325      >>> getnet('10.12.25.33', '255.255.255.0')
    326      168564992L
    327      """
    328      checkip(ip)
    329      return long(int(IPNetwork( ipunwrap(ip) + '/' + str(netmask)).network))
    330
    331 -def getnetstr(ip, netmask):
    332      """
    333      Deprecated in favour of netFromIpAndNet()
    334      """
    335      return netFromIpAndNet(ip, netmask)
    336
    337 -def netFromIpAndNet(ip, netmask):
    338      """
    339      Return network number as string
    340
    341      >>> netFromIpAndNet('10.12.25.33', 24)
    342      '10.12.25.0'
    343      >>> netFromIpAndNet('250.12.25.33', 1)
    344      '128.0.0.0'
    345      >>> netFromIpAndNet('10.12.25.33', 16)
    346      '10.12.0.0'
    347      >>> netFromIpAndNet('10.12.25.33', 32)
    348      '10.12.25.33'
    349      """
    350      checkip(ip)
    351      return str(IPNetwork( ipunwrap(ip) + '/' + str(netmask)).network)
    352
    353 -def asyncNameLookup(address, uselibcresolver = True):
    354      """
    355      Turn IP addreses into names using deferreds
    356      """
    357      if uselibcresolver:
    358          # This is the most reliable way to do a lookup use it
    359          return threads.deferToThread(lambda : socket.gethostbyaddr(address)[0])
    360      else:
    361          # There is a problem with this method because it will ignore /etc/hosts
    362          address = '.'.join(address.split('.')[::-1]) + '.in-addr.arpa'
    363          d = lookupPointer(address, [1,2,4])
    364          def ip(result):
    365              return str(result[0][0].payload.name)
    366          d.addCallback(ip)
    367          return d
    368
    369 -def asyncIpLookup(name):
    370      """
    371      Look up an IP based on the name passed in.  We use gethostbyname to make
    372      sure that we use /etc/hosts as mentioned above.
    373
    374      This hasn't been tested.
    375      """
    376      return threads.deferToThread(lambda : socket.gethostbyname(name))
    377
    378 -def generateAddrInfos(hostname):
    379      """
    380      generator for dicts from addrInfo structs for hostname
    381      """
    382      for addrInfo in socket.getaddrinfo(hostname, None):
    383          yield {"ipAddress": addrInfo[-1][0],
    384                  "ipFamily": addrInfo[0]}
    385
    386 -def getHostByName(hostname, preferredIpVersion=None):
    387      """
    388      Look up an IP based on the name passed in, synchronously. Not using
    389      socket.gethostbyname() because it does not support IPv6.
    390
    391      preferredIpVersion should equal something like socket.AF_INET or
    392      socket.AF_INET6
    393      """
    394      addrInfos = generateAddrInfos(hostname)
    395      if preferredIpVersion is not None:
    396          for addrInfo in addrInfos:
    397              if addrInfo['ipFamily'] == preferredIpVersion:
    398                  return addrInfo['ipAddress']
    399      firstInfo = addrInfos.next()
    400      return firstInfo['ipAddress']
    401
    402 -def parse_iprange(iprange):
    403      """
    404      Turn a string specifying an IP range into a list of IPs.
    405
    406      @param iprange: The range string, in the format '10.0.0.a-b'
    407      @type iprange: str
    408
    409          >>> parse_iprange('10.0.0.1-5')
    410          ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5']
    411          >>> parse_iprange('10.0.0.2-5')
    412          ['10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5']
    413          >>> parse_iprange('10.0.0.1')
    414          ['10.0.0.1']
    415          >>> try: parse_iprange('10.0.0.1-2-3')
    416          ... except InvalidIPRangeError: print "Invalid"
    417          Invalid
    418          >>> parse_iprange('fd00::aaf:d201-d203')
    419          ['fd00::aaf:d201', 'fd00::aaf:d202', 'fd00::aaf:d203']
    420      """
    421      rangeList = iprange.split('-')
    422      if len(rangeList) > 2: # Nothing we can do about this
    423          raise InvalidIPRangeError('%s is an invalid IP range.' % iprange)
    424      elif len(rangeList) == 1: # A single IP was passed
    425          return [iprange]
    426
    427      beginIp, endIp = rangeList
    428      # Is it just an int?
    429      separator = '.'
    430      strFn = lambda x:x
    431      base = 10
    432      if beginIp.find(':')> -1:
    433          separator=':'
    434          base=16
    435          strFn = lambda x: '{0:x}'.format(x)
    436      net, start = beginIp.rsplit(separator, 1)
    437      start = int(start, base)
    438      end = int(endIp, base)
    439
    440      return ['%s%s%s' % (net, separator, strFn(x)) for x in xrange(start, end+1)]
    441
    442
    443 -def getSubnetBounds(ip):
    444      """
    445      Given a string representing the lower limit of a subnet, return decimal
    446      representations of the first and last IP of that subnet.
    447
    448      0 is considered to define the beginning of a subnet, so x.x.x.0 represents
    449      a /24, x.x.0.0 represents a /16, etc. An octet of 0 followed by a non-zero
    450      octet, of course, is not considered to define a lower limit.
    451
    452          >>> map(decimalIpToStr, getSubnetBounds('10.1.1.0'))
    453          ['10.1.1.0', '10.1.1.255']
    454          >>> map(decimalIpToStr, getSubnetBounds('10.1.1.1'))
    455          ['10.1.1.1', '10.1.1.1']
    456          >>> map(decimalIpToStr, getSubnetBounds('10.0.1.0'))
    457          ['10.0.1.0', '10.0.1.255']
    458          >>> map(decimalIpToStr, getSubnetBounds('0.0.0.0'))
    459          ['0.0.0.0', '255.255.255.255']
    460          >>> map(decimalIpToStr, getSubnetBounds('10.0.0.0'))
    461          ['10.0.0.0', '10.255.255.255']
    462          >>> map(decimalIpToStr, getSubnetBounds('100.0.0.0'))
    463          ['100.0.0.0', '100.255.255.255']
    464          >>> map(decimalIpToStr, getSubnetBounds('::100.0.0.0'))
    465          ['100.0.0.0', '100.255.255.255']
    466
    467      """
    468      octets = ip.split('.')
    469      otherend = []
    470      while octets:
    471          o = octets.pop()
    472          if o=='0':
    473              otherend.append('255')
    474          else:
    475              otherend.append(o)
    476              break
    477      otherend.reverse()
    478      octets.extend(otherend)
    479      upper = '.'.join(octets)
    480      return numbip(ip), numbip(upper)
    481
    482 -def ensureIp(ip):
    483      """
    484      Given a partially formed IP address this will return a complete Ip address
    485      with four octets with the invalid or missing entries replaced by 0
    486
    487      @param ip partially formed ip (will strip out alpha characters)
    488      @return valid IP address field
    489
    490      >>> from Products.ZenUtils.IpUtil import ensureIp
    491      >>> ensureIp('20')
    492      '20.0.0.0'
    493      >>> ensureIp('2000')
    494      '0.0.0.0'
    495      >>> ensureIp('10.175.X')
    496      '10.175.0.0'
    497      >>> ensureIp('10.0.1')
    498      '10.0.1.0'
    499      >>>
    500      """
    501      # filter out the alpha characters
    502      stripped = ''.join(c for c in ip if c in '1234567890.')
    503      octets = stripped.split('.')
    504
    505      # make sure we always have 4
    506      while (len(octets) < 4):
    507          octets.append('0')
    508
    509      # validate each octet
    510      for (idx, octet) in enumerate(octets):
    511          # cast it to an integer
    512          try:
    513              octet = int(octet)
    514          except ValueError:
    515              octet = 0
    516
    517          # make it 0 if not in the valid ip range
    518          if not (0 < octet < 255):
    519              octets[idx] = '0'
    520
    521      return '.'.join(octets)
    522