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