Package Products :: Package ZenEvents :: Module zentrap |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenEvents.zentrap

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2007, 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 as published by
      8  # the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__ = """zentrap
     15
     16  Creates events from SNMP Traps.
     17  Currently a wrapper around the Net-SNMP C library.
     18  """
     19
     20  import time
     21  import sys
     22  import socket
     23  import cPickle
     24  from exceptions import EOFError, IOError
     25
     26  # Magical interfacing with C code
     27  import ctypes as c
     28
     29  import Globals
     30
     31  from EventServer import EventServer
     32
     33  from pynetsnmp import netsnmp, twistedsnmp
     34
     35  from twisted.internet import defer, reactor
     36  from Products.ZenUtils.Driver import drive
     37  from Products.ZenUtils.captureReplay import CaptureReplay
     38
     39
     40
     41  # This is what struct sockaddr_in {} looks like
     42  family = [('family', c.c_ushort)]
     43  if sys.platform == 'darwin':
     44      family = [('len', c.c_ubyte), ('family', c.c_ubyte)]
     45
     46 -class sockaddr_in(c.Structure):
     47      _fields_ = family + [
     48          ('port', c.c_ubyte * 2),        # need to decode from net-byte-order
     49          ('addr', c.c_ubyte * 4)
     50          ];
     51
     52  # teach python that the return type of snmp_clone_pdu is a pdu pointer
     53  netsnmp.lib.snmp_clone_pdu.restype = netsnmp.netsnmp_pdu_p
     54
     55  TRAP_PORT = 162
     56  try:
     57      TRAP_PORT = socket.getservbyname('snmptrap', 'udp')
     58  except socket.error:
     59      pass
     60
     61 -def lp2oid(ptr, length):
     62      "Convert a pointer to an array of longs to an oid"
     63      return '.'.join([str(ptr[i]) for i in range(length)])
     64
     65 -def bp2ip(ptr):
     66      "Convert a pointer to 4 bytes to a dotted-ip-address"
     67      return '.'.join([str(ptr[i]) for i in range(4)])
     68
     69
     70 -class FakePacket(object):
     71      """
     72      A fake object to make packet replaying feasible.
     73      """
     74 -    def __init__(self):
     75          self.fake = True
     76
     77
     78 -class ZenTrap(EventServer, CaptureReplay):
     79      """
     80      Listen for SNMP traps and turn them into events
     81      Connects to the EventService service in zenhub.
     82      """
     83
     84      name = 'zentrap'
     85
     86 -    def __init__(self):
     87          EventServer.__init__(self)
     88
     89          self.oidCache = {}
     90
     91          # Command-line argument sanity checking
     92          self.processCaptureReplayOptions()
     93
     94          if not self.options.useFileDescriptor and self.options.trapport < 1024:
     95              self.openPrivilegedPort('--listen', '--proto=udp',
     96                  '--port=%s:%d' % (self.options.listenip,
     97                  self.options.trapport))
     98          self.session = netsnmp.Session()
     99          if self.options.useFileDescriptor is not None:
    100              fileno = int(self.options.useFileDescriptor)
    101              # open port 1162, but then dup fileno onto it
    102              self.session.awaitTraps('%s:1162' % self.options.listenip, fileno)
    103          else:
    104              self.session.awaitTraps('%s:%d' % (
    105                  self.options.listenip, self.options.trapport))
    106          self.session.callback = self.receiveTrap
    107
    108          twistedsnmp.updateReactor()
    109
    110
    111 -    def getEnterpriseString(self, pdu):
    112          """
    113          Get the enterprise string from the PDU or replayed packet
    114
    115          @param pdu: raw packet
    116          @type pdu: binary
    117          @return: enterprise string
    118          @rtype: string
    119          """
    120          if hasattr(pdu, "fake"): # Replaying a packet
    121              enterprise = pdu.enterprise
    122          else:
    123              enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length)
    124          return enterprise
    125
    126
    127 -    def getResult(self, pdu):
    128          """
    129          Get the values from the PDU or replayed packet
    130
    131          @param pdu: raw packet
    132          @type pdu: binary
    133          @return: variables from the PDU or Fake packet
    134          @rtype: dictionary
    135          """
    136          if hasattr(pdu, "fake"): # Replaying a packet
    137              variables = pdu.variables
    138          else:
    139              variables = netsnmp.getResult(pdu)
    140          return variables
    141
    142
    143
    144 -    def getCommunity(self, pdu):
    145          """
    146          Get the communitry string from the PDU or replayed packet
    147
    148          @param pdu: raw packet
    149          @type pdu: binary
    150          @return: SNMP community
    151          @rtype: string
    152          """
    153          community = ''
    154          if hasattr(pdu, "fake"): # Replaying a packet
    155              community = pdu.community
    156          elif pdu.community_len:
    157                  community = c.string_at(pdu.community, pdu.community_len)
    158
    159          return community
    160
    161
    162 -    def convertPacketToPython(self, addr, pdu):
    163          """
    164          Store the raw packet for later examination and troubleshooting.
    165
    166          @param addr: packet-sending host's IP address and port
    167          @type addr: (string, number)
    168          @param pdu: raw packet
    169          @type pdu: binary
    170          @return: Python FakePacket object
    171          @rtype: Python FakePacket object
    172          """
    173          packet = FakePacket()
    174          packet.version = pdu.version
    175          packet.host = addr[0]
    176          packet.port = addr[1]
    177          packet.variables = netsnmp.getResult(pdu)
    178          packet.community = ''
    179
    180          # Here's where we start to encounter differences between packet types
    181          if pdu.version == 0:
    182              packet.agent_addr =  [pdu.agent_addr[i] for i in range(4)]
    183              packet.trap_type = pdu.trap_type
    184              packet.specific_type = pdu.specific_type
    185              packet.enterprise = self.getEnterpriseString(pdu)
    186              packet.community = self.getCommunity(pdu)
    187
    188          return packet
    189
    190
    191 -    def replay(self, pdu):
    192          """
    193          Replay a captured packet
    194
    195          @param pdu: raw packet
    196          @type pdu: binary
    197          """
    198          ts = time.time()
    199          d = self.asyncHandleTrap([pdu.host, pdu.port], pdu, ts)
    200
    201
    202 -    def oid2name(self, oid, exactMatch=True, strip=False):
    203          """
    204          Get OID name from cache or ZenHub
    205
    206          @param oid: SNMP Object IDentifier
    207          @type oid: string
    208          @param exactMatch: find the full OID or don't match
    209          @type exactMatch: boolean
    210          @param strip: show what matched, or matched + numeric OID remainder
    211          @type strip: boolean
    212          @return: Twisted deferred object
    213          @rtype: Twisted deferred object
    214          """
    215          if type(oid) == type(()):
    216              oid = '.'.join(map(str, oid))
    217          cacheKey = "%s:%r:%r" % (oid, exactMatch, strip)
    218          if self.oidCache.has_key(cacheKey):
    219              return defer.succeed(self.oidCache[cacheKey])
    220
    221          self.log.debug("OID cache miss on %s (exactMatch=%r, strip=%r)" % (
    222              oid, exactMatch, strip))
    223          d = self.model().callRemote('oid2name', oid, exactMatch, strip)
    224
    225          def cache(name, key):
    226              """
    227              Twisted callback to cache and return the name
    228
    229              @param name: human-readable-name form of OID
    230              @type name: string
    231              @param key: key of OID and params
    232              @type key: string
    233              @return: the name parameter
    234              @rtype: string
    235              """
    236              self.oidCache[key] = name
    237              return name
    238
    239          d.addCallback(cache, cacheKey)
    240          return d
    241
    242
    243 -    def receiveTrap(self, pdu):
    244          """
    245          Accept a packet from the network and spin off a Twisted
    246          deferred to handle the packet.
    247
    248          @param pdu: Net-SNMP object
    249          @type pdu: netsnmp_pdu object
    250          """
    251          ts = time.time()
    252
    253          # Is it a trap?
    254          if pdu.sessid != 0: return
    255
    256          if pdu.version not in [ 0, 1 ]:
    257              self.log.error("Unable to handle trap version %d", pdu.version)
    258              return
    259
    260          # What address did it come from?
    261          #   for now, we'll make the scary assumption this data is a
    262          #   sockaddr_in
    263          transport = c.cast(pdu.transport_data, c.POINTER(sockaddr_in))
    264          if not transport: return
    265          transport = transport.contents
    266
    267          #   Just to make sure, check to see that it is type AF_INET
    268          if transport.family != socket.AF_INET: return
    269          # get the address out as ( host-ip, port)
    270          addr = [bp2ip(transport.addr),
    271                  transport.port[0] << 8 | transport.port[1]]
    272
    273          self.log.debug( "Received packet from %s at port %s" % (addr[0], addr[1]) )
    274          self.processPacket(addr, pdu, ts)
    275
    276
    277 -    def processPacket(self, addr, pdu, ts):
    278          """
    279          Wrapper around asyncHandleTrap to process the provided packet.
    280
    281          @param addr: packet-sending host's IP address, port info
    282          @type addr: ( host-ip, port)
    283          @param pdu: Net-SNMP object
    284          @type pdu: netsnmp_pdu object
    285          @param ts: time stamp
    286          @type ts: datetime
    287          """
    288          # At the end of this callback, pdu will be deleted, so copy it
    289          # for asynchronous processing
    290          dup = netsnmp.lib.snmp_clone_pdu(c.addressof(pdu))
    291          if not dup:
    292              self.log.error("Could not clone PDU for asynchronous processing")
    293              return
    294
    295          def cleanup(result):
    296              """
    297              Twisted callback to delete a previous memory allocation
    298
    299              @param result: Net-SNMP object
    300              @type result: netsnmp_pdu object
    301              @return: the result parameter
    302              @rtype: binary
    303              """
    304              netsnmp.lib.snmp_free_pdu(dup)
    305              return result
    306
    307          d = self.asyncHandleTrap(addr, dup.contents, ts)
    308          d.addBoth(cleanup)
    309
    310
    311 -    def asyncHandleTrap(self, addr, pdu, ts):
    312          """
    313          Twisted callback to process a trap
    314
    315          @param addr: packet-sending host's IP address, port info
    316          @type addr: ( host-ip, port)
    317          @param pdu: Net-SNMP object
    318          @type pdu: netsnmp_pdu object
    319          @param ts: time stamp
    320          @type ts: datetime
    321          @return: Twisted deferred object
    322          @rtype: Twisted deferred object
    323          """
    324          def inner(driver):
    325              """
    326              Generator function that actually processes the packet
    327
    328              @param driver: Twisted deferred object
    329              @type driver: Twisted deferred object
    330              @return: Twisted deferred object
    331              @rtype: Twisted deferred object
    332              """
    333              self.capturePacket( addr[0], addr, pdu)
    334
    335              eventType = 'unknown'
    336              result = {}
    337              if pdu.version == 1:
    338                  # SNMP v2
    339                  variables = self.getResult(pdu)
    340                  for oid, value in variables:
    341                      oid = '.'.join(map(str, oid))
    342                      # SNMPv2-MIB/snmpTrapOID
    343                      if oid == '1.3.6.1.6.3.1.1.4.1.0':
    344                          yield self.oid2name(value, exactMatch=False, strip=False)
    345                          eventType = driver.next()
    346                      else:
    347                          # Add a detail for the variable binding.
    348                          yield self.oid2name(oid, exactMatch=False, strip=False)
    349                          result[driver.next()] = value
    350                          # Add a detail for the index-stripped variable binding.
    351                          yield self.oid2name(oid, exactMatch=False, strip=True)
    352                          result[driver.next()] = value
    353
    354              elif pdu.version == 0:
    355                  # SNMP v1
    356                  variables = self.getResult(pdu)
    357                  addr[0] = '.'.join(map(str, [pdu.agent_addr[i] for i in range(4)]))
    358                  enterprise = self.getEnterpriseString(pdu)
    359                  eventType = driver.next()
    360                  generic = pdu.trap_type
    361                  specific = pdu.specific_type
    362
    363                  # Try an exact match with a .0. inserted between enterprise and
    364                  # specific OID. It seems that MIBs frequently expect this .0.
    365                  # to exist, but the device's don't send it in the trap.
    366                  oid = "%s.0.%d" % (enterprise, specific)
    367                  yield self.oid2name(oid, exactMatch=True, strip=False)
    368                  name = driver.next()
    369
    370                  # If we didn't get a match with the .0. inserted we will try
    371                  # resolving withing the .0. inserted and allow partial matches.
    372                  if name == oid:
    373                      oid = "%s.%d" % (enterprise, specific)
    374                      yield self.oid2name(oid, exactMatch=False, strip=False)
    375                      name = driver.next()
    376
    377                  # Look for the standard trap types and decode them without
    378                  # relying on any MIBs being loaded.
    379                  eventType = {
    380                      0: 'snmp_coldStart',
    381                      1: 'snmp_warmStart',
    382                      2: 'snmp_linkDown',
    383                      3: 'snmp_linkUp',
    384                      4: 'snmp_authenticationFailure',
    385                      5: 'snmp_egpNeighorLoss',
    386                      6: name,
    387                      }.get(generic, name)
    388
    389                  # Decode all variable bindings. Allow partial matches and strip
    390                  # off any index values.
    391                  for oid, value in variables:
    392                      oid = '.'.join(map(str, oid))
    393                      # Add a detail for the variable binding.
    394                      yield self.oid2name(oid, exactMatch=False, strip=False)
    395                      result[driver.next()] = value
    396                      # Add a detail for the index-stripped variable binding.
    397                      yield self.oid2name(oid, exactMatch=False, strip=True)
    398                      result[driver.next()] = value
    399              else:
    400                  self.log.error("Unable to handle trap version %d", pdu.version)
    401                  return
    402
    403              summary = 'snmp trap %s' % eventType
    404              self.log.debug(summary)
    405              community = self.getCommunity(pdu)
    406              result['oid'] = oid
    407              result['device'] = addr[0]
    408              result.setdefault('component', '')
    409              result.setdefault('eventClassKey', eventType)
    410              result.setdefault('eventGroup', 'trap')
    411              result.setdefault('severity', 3)
    412              result.setdefault('summary', summary)
    413              result.setdefault('community', community)
    414              result.setdefault('firstTime', ts)
    415              result.setdefault('lastTime', ts)
    416              result.setdefault('monitor', self.options.monitor)
    417              self.sendEvent(result)
    418
    419              # Don't attempt to respond back if we're replaying packets
    420              if len(self.options.replayFilePrefix) > 0:
    421                  self.replayed += 1
    422                  return
    423
    424              # respond to INFORM requests
    425              if pdu.command == netsnmp.SNMP_MSG_INFORM:
    426                  reply = netsnmp.lib.snmp_clone_pdu(c.addressof(pdu))
    427                  if not reply:
    428                      self.log.error("Could not clone PDU for INFORM response")
    429                      raise RuntimeError("Cannot respond to INFORM PDU")
    430                  reply.contents.command = netsnmp.SNMP_MSG_RESPONSE
    431                  reply.contents.errstat = 0
    432                  reply.contents.errindex = 0
    433                  sess = netsnmp.Session(peername='%s:%d' % tuple(addr),
    434                                         version=pdu.version)
    435                  sess.open()
    436                  if not netsnmp.lib.snmp_send(sess.sess, reply):
    437                      netsnmp.lib.snmp_sess_perror("Unable to send inform PDU",
    438                                                   self.session.sess)
    439                      netsnmp.lib.snmp_free_pdu(reply)
    440                  sess.close()
    441          return drive(inner)
    442
    443
    444 -    def buildOptions(self):
    445          """
    446          Command-line options to be supported
    447          """
    448          EventServer.buildOptions(self)
    449          self.parser.add_option('--trapport', '-t',
    450              dest='trapport', type='int', default=TRAP_PORT,
    451              help="Listen for SNMP traps on this port rather than the default")
    452          self.parser.add_option('--listenip',
    453              dest='listenip', default='0.0.0.0',
    454              help="IP address to listen on. Default is 0.0.0.0")
    455          self.parser.add_option('--useFileDescriptor',
    456                                 dest='useFileDescriptor',
    457                                 type='int',
    458                                 help=("Read from an existing connection "
    459                                       " rather than opening a new port."),
    460                                 default=None)
    461
    462          self.buildCaptureReplayOptions()
    463
    464
    465  if __name__ == '__main__':
    466      z = ZenTrap()
    467      z.run()
    468      z.report()
    469