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

# Source Code for Module Products.ZenUtils.IpUtil

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