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, 2011 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__ = """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 errno
     24  import base64
     25  import logging
     26  from struct import unpack
     27  from ipaddr import IPAddress
     28
     29  log = logging.getLogger("zen.zentrap")
     30
     31  # Magical interfacing with C code
     32  import ctypes as c
     33
     34  import Globals
     35  import zope.interface
     36  import zope.component
     37
     38  from twisted.python.failure import Failure
     39  from twisted.internet import defer
     40
     41  from Products.ZenCollector.daemon import CollectorDaemon
     42  from Products.ZenCollector.interfaces import ICollector, ICollectorPreferences,\
     43                                               IEventService, \
     44                                               IScheduledTask
     45  from Products.ZenCollector.tasks import SimpleTaskFactory,\
     46                                          SimpleTaskSplitter,\
     47                                          BaseTask, TaskStates
     48  from Products.ZenUtils.observable import ObservableMixin
     49
     50
     51  from pynetsnmp import netsnmp, twistedsnmp
     52
     53  from Products.ZenHub.PBDaemon import FakeRemote
     54  from Products.ZenUtils.captureReplay import CaptureReplay
     55  from Products.ZenEvents.EventServer import Stats
     56  from Products.ZenUtils.Utils import unused
     57  from Products.ZenCollector.services.config import DeviceProxy
     58  unused(DeviceProxy)
     59  from Products.ZenHub.services.SnmpTrapConfig import User
     60  unused(User)
     61
     62  from zenoss.protocols.protobufs.zep_pb2 import (SEVERITY_CRITICAL, SEVERITY_ERROR,
     63                                                  SEVERITY_WARNING, SEVERITY_INFO,
     64                                                  SEVERITY_DEBUG, SEVERITY_CLEAR)
     65
     66
     67  # This is what struct sockaddr_in {} looks like
     68  family = [('family', c.c_ushort)]
     69  if sys.platform == 'darwin':
     70      family = [('len', c.c_ubyte), ('family', c.c_ubyte)]
     71
     72 -class sockaddr_in(c.Structure):
     73      _fields_ = family + [
     74          ('port', c.c_ubyte * 2),     # need to decode from net-byte-order
     75          ('addr', c.c_ubyte * 4),
     76          ]
     77
     78 -class sockaddr_in6(c.Structure):
     79      _fields_ = family + [
     80          ('port', c.c_ushort),        # need to decode from net-byte-order
     81          ('flow', c.c_ubyte * 4),
     82          ('addr', c.c_ubyte * 16),
     83          ('scope_id', c.c_ubyte * 4),
     84          ]
     85
     86  _pre_parse_factory = c.CFUNCTYPE(c.c_int,
     87                                   c.POINTER(netsnmp.netsnmp_session),
     88                                   c.POINTER(netsnmp.netsnmp_transport),
     89                                   c.c_void_p,
     90                                   c.c_int)
     91
     92  # teach python that the return type of snmp_clone_pdu is a pdu pointer
     93  netsnmp.lib.snmp_clone_pdu.restype = netsnmp.netsnmp_pdu_p
     94
     95  # Version codes from the PDU
     96  SNMPv1 = 0
     97  SNMPv2 = 1
     98  SNMPv3 = 3
     99
    100 -class FakePacket(object):
    101      """
    102      A fake object to make packet replaying feasible.
    103      """
    104 -    def __init__(self):
    105          self.fake = True
    106
    107
    108 -class SnmpTrapPreferences(CaptureReplay):
    109      zope.interface.implements(ICollectorPreferences)
    110
    111 -    def __init__(self):
    112          """
    113          Constructs a new PingCollectionPreferences instance and
    114          provides default values for needed attributes.
    115          """
    116          self.collectorName = 'zentrap'
    117          self.defaultRRDCreateCommand = None
    118          self.configCycleInterval = 20 # minutes
    119          self.cycleInterval = 5 * 60 # seconds
    120
    121          # The configurationService attribute is the fully qualified class-name
    122          # of our configuration service that runs within ZenHub
    123          self.configurationService = 'Products.ZenHub.services.SnmpTrapConfig'
    124
    125          # Will be filled in based on buildOptions
    126          self.options = None
    127
    128          self.configCycleInterval = 20*60
    129          self.task = None
    130
    131 -    def postStartupTasks(self):
    132          self.task = TrapTask('zentrap', configId='zentrap')
    133          yield self.task
    134
    135 -    def buildOptions(self, parser):
    136          """
    137          Command-line options to be supported
    138          """
    139          TRAP_PORT = 162
    140          try:
    141              TRAP_PORT = socket.getservbyname('snmptrap', 'udp')
    142          except socket.error:
    143              pass
    144          parser.add_option('--trapport', '-t',
    145              dest='trapport', type='int', default=TRAP_PORT,
    146              help="Listen for SNMP traps on this port rather than the default")
    147          parser.add_option('--useFileDescriptor',
    148                                 dest='useFileDescriptor',
    149                                 type='int',
    150                                 help=("Read from an existing connection "
    151                                       " rather than opening a new port."),
    152                                 default=None)
    153
    154          self.buildCaptureReplayOptions(parser)
    155
    156 -    def postStartup(self):
    157          # Ensure that we always have an oidMap
    158          daemon = zope.component.getUtility(ICollector)
    159          daemon.oidMap = {}
    160
    161 -def ipv6_is_enabled():
    162      "test if ipv6 is enabled"
    163      try:
    164          socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 0)
    165      except socket.error, e:
    166          if e.errno == errno.EAFNOSUPPORT:
    167              return False
    168          raise
    169      return True
    170
    171 -class TrapTask(BaseTask, CaptureReplay):
    172      """
    173      Listen for SNMP traps and turn them into events
    174      Connects to the TrapService service in zenhub.
    175      """
    176      zope.interface.implements(IScheduledTask)
    177
    178 -    def __init__(self, taskName, configId,
    179                   scheduleIntervalSeconds=3600, taskConfig=None):
    180          BaseTask.__init__(self, taskName, configId,
    181                   scheduleIntervalSeconds, taskConfig)
    182          self.log = log
    183
    184          # Needed for interface
    185          self.name = taskName
    186          self.configId = configId
    187          self.state = TaskStates.STATE_IDLE
    188          self.interval = scheduleIntervalSeconds
    189          self._daemon = zope.component.getUtility(ICollector)
    190          self._eventService = zope.component.queryUtility(IEventService)
    191          self._preferences = self._daemon
    192
    193          # For compatibility with captureReplay
    194          self.options = self._daemon.options
    195
    196          self.oidMap = self._daemon.oidMap
    197          self.stats = Stats()
    198
    199          # Command-line argument sanity checking
    200          self.processCaptureReplayOptions()
    201
    202          trapPort = self._preferences.options.trapport
    203          if not self._preferences.options.useFileDescriptor and trapPort < 1024:
    204              listen_ip = "ipv6" if ipv6_is_enabled() else "0.0.0.0"
    205              # Makes call to zensocket here (does an exec* so it never returns)
    206              self._daemon.openPrivilegedPort('--listen', '--proto=udp', '--port=%s:%d' % (listen_ip, trapPort))
    207              self.log("Unexpected return from openPrivilegedPort. Exiting.")
    208              sys.exit(1)
    209
    210          # Start listening for SNMP traps
    211          self.log.info("Starting to listen on SNMP trap port %s", trapPort)
    212          self.session = netsnmp.Session()
    213          listening_protocol = "udp6" if ipv6_is_enabled() else "udp"
    214          if self._preferences.options.useFileDescriptor is not None:
    215              # open port 1162, but then dup fileno onto it
    216              listening_address = listening_protocol + ':1162'
    217              fileno = int(self._preferences.options.useFileDescriptor)
    218          else:
    219              listening_address = '%s:%d' % (listening_protocol, trapPort)
    220              fileno = -1
    221          self._pre_parse_callback = _pre_parse_factory(self._pre_parse)
    222          debug = self.log.isEnabledFor(logging.DEBUG)
    223          self.session.awaitTraps(listening_address, fileno, self._pre_parse_callback, debug)
    224          self.session.callback = self.receiveTrap
    225          twistedsnmp.updateReactor()
    226
    227 -    def doTask(self):
    228          """
    229          This is a wait-around task since we really are called
    230          asynchronously.
    231          """
    232          return defer.succeed("Waiting for SNMP traps...")
    233
    234 -    def isReplaying(self):
    235          """
    236          @returns True if we are replaying a packet instead of capturing one
    237          """
    238          return len(self._preferences.options.replayFilePrefix) > 0
    239
    240 -    def getEnterpriseString(self, pdu):
    241          """
    242          Get the enterprise string from the PDU or replayed packet
    243
    244          @param pdu: raw packet
    245          @type pdu: binary
    246          @return: enterprise string
    247          @rtype: string
    248          """
    249          def lp2oid(ptr, length):
    250              "Convert a pointer to an array of longs to an OID"
    251              return '.'.join([str(ptr[i]) for i in range(length)])
    252
    253          if hasattr(pdu, "fake"): # Replaying a packet
    254              enterprise = pdu.enterprise
    255          else:
    256              enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length)
    257          return enterprise
    258
    259 -    def getResult(self, pdu):
    260          """
    261          Get the values from the PDU or replayed packet
    262
    263          @param pdu: raw packet
    264          @type pdu: binary
    265          @return: variables from the PDU or Fake packet
    266          @rtype: dictionary
    267          """
    268          if hasattr(pdu, "fake"): # Replaying a packet
    269              variables = pdu.variables
    270          else:
    271              variables = netsnmp.getResult(pdu)
    272          return variables
    273
    274 -    def getCommunity(self, pdu):
    275          """
    276          Get the community string from the PDU or replayed packet
    277
    278          @param pdu: raw packet
    279          @type pdu: binary
    280          @return: SNMP community
    281          @rtype: string
    282          """
    283          community = ''
    284          if hasattr(pdu, "fake"): # Replaying a packet
    285              community = pdu.community
    286          elif pdu.community_len:
    287                  community = c.string_at(pdu.community, pdu.community_len)
    288
    289          return community
    290
    291 -    def convertPacketToPython(self, addr, pdu):
    292          """
    293          Store the raw packet for later examination and troubleshooting.
    294
    295          @param addr: packet-sending host's IP address and port
    296          @type addr: (string, number)
    297          @param pdu: raw packet
    298          @type pdu: binary
    299          @return: Python FakePacket object
    300          @rtype: Python FakePacket object
    301          """
    302          packet = FakePacket()
    303          packet.version = pdu.version
    304          packet.host = addr[0]
    305          packet.port = addr[1]
    306          packet.variables = netsnmp.getResult(pdu)
    307          packet.community = ''
    308          packet.enterprise_length = pdu.enterprise_length
    309
    310          # Here's where we start to encounter differences between packet types
    311          if pdu.version == SNMPv1:
    312              # SNMPv1 can't be received via IPv6
    313              packet.agent_addr =  [pdu.agent_addr[i] for i in range(4)]
    314              packet.trap_type = pdu.trap_type
    315              packet.specific_type = pdu.specific_type
    316              packet.enterprise = self.getEnterpriseString(pdu)
    317              packet.community = self.getCommunity(pdu)
    318
    319          return packet
    320
    321 -    def replay(self, pdu):
    322          """
    323          Replay a captured packet
    324
    325          @param pdu: raw packet
    326          @type pdu: binary
    327          """
    328          ts = time.time()
    329          self.asyncHandleTrap([pdu.host, pdu.port], pdu, ts)
    330
    331 -    def oid2name(self, oid, exactMatch=True, strip=False):
    332          """
    333          Returns a MIB name based on an OID and special handling flags.
    334
    335          @param oid: SNMP Object IDentifier
    336          @type oid: string
    337          @param exactMatch: find the full OID or don't match
    338          @type exactMatch: boolean
    339          @param strip: show what matched, or matched + numeric OID remainder
    340          @type strip: boolean
    341          @return: Twisted deferred object
    342          @rtype: Twisted deferred object
    343          """
    344          if isinstance(oid, tuple):
    345              oid = '.'.join(map(str, oid))
    346
    347          oid = oid.strip('.')
    348          if exactMatch:
    349              if oid in self.oidMap:
    350                  return self.oidMap[oid]
    351              else:
    352                  return oid
    353
    354          oidlist = oid.split('.')
    355          for i in range(len(oidlist), 0, -1):
    356              name = self.oidMap.get('.'.join(oidlist[:i]), None)
    357              if name is None:
    358                  continue
    359
    360              oid_trail = oidlist[i:]
    361              if len(oid_trail) > 0 and not strip:
    362                  return "%s.%s" % (name, '.'.join(oid_trail))
    363              else:
    364                  return name
    365
    366          return oid
    367
    368 -    def _pre_parse(self, session, transport, transport_data, transport_data_length):
    369          """Called before the net-snmp library parses the PDU. In the case
    370          where a v3 trap comes in with unkwnown credentials, net-snmp silently
    371          discards the packet. This method gives zentrap a way to log that these
    372          packets were received to help with troubleshooting."""
    373          if self.log.isEnabledFor(logging.DEBUG):
    374              ipv6_socket_address = c.cast(transport_data, c.POINTER(sockaddr_in6)).contents
    375              if ipv6_socket_address.family == socket.AF_INET6:
    376                  self.log.debug("pre_parse: IPv6 %s" % (socket.inet_ntop(socket.AF_INET6, ipv6_socket_address.addr)))
    377              elif ipv6_socket_address.family == socket.AF_INET:
    378                  ipv4_socket_address = c.cast(transport_data, c.POINTER(sockaddr_in)).contents
    379                  self.log.debug("pre_parse: IPv4 %s" % socket.inet_ntop(socket.AF_INET, ipv4_socket_address.addr))
    380              else:
    381                  self.log.debug("pre_parse: unexpected address family: %s" % ipv6_socket_address.family)
    382          return 1
    383
    384 -    def receiveTrap(self, pdu):
    385          """
    386          Accept a packet from the network and spin off a Twisted
    387          deferred to handle the packet.
    388
    389          @param pdu: Net-SNMP object
    390          @type pdu: netsnmp_pdu object
    391          """
    392          if pdu.version not in (SNMPv1, SNMPv2, SNMPv3):
    393              self.log.error("Unable to handle trap version %d", pdu.version)
    394              return
    395          if pdu.transport_data is None:
    396              self.log.error("PDU does not contain transport data")
    397              return
    398
    399          ipv6_socket_address = c.cast(pdu.transport_data, c.POINTER(sockaddr_in6)).contents
    400          if ipv6_socket_address.family == socket.AF_INET6:
    401              if pdu.transport_data_length < c.sizeof(sockaddr_in6):
    402                  self.log.error("PDU transport data is too small for sockaddr_in6 struct.")
    403                  return
    404              ip_address = self.getPacketIp(ipv6_socket_address.addr)
    405          elif ipv6_socket_address.family == socket.AF_INET:
    406              if pdu.transport_data_length < c.sizeof(sockaddr_in):
    407                  self.log.error("PDU transport data is too small for sockaddr_in struct.")
    408                  return
    409              ipv4_socket_address = c.cast(pdu.transport_data, c.POINTER(sockaddr_in)).contents
    410              ip_address = '.'.join(str(i) for i in ipv4_socket_address.addr)
    411          else:
    412              self.log.error("Got a packet with unrecognized network family: %s", ipv6_socket_address.family)
    413              return
    414
    415          port = socket.ntohs(ipv6_socket_address.port)
    416          self.log.debug( "Received packet from %s at port %s" % (ip_address, port) )
    417          self.processPacket(ip_address, port, pdu, time.time())
    418
    419 -    def getPacketIp(self, addr):
    420          """
    421          For IPv4, convert a pointer to 4 bytes to a dotted-ip-address
    422          For IPv6, convert a pointer to 16 bytes to a canonical IPv6 address.
    423          """
    424
    425          def _gen_byte_pairs():
    426              for left, right in zip(addr[::2], addr[1::2]):
    427                  yield "%.2x%.2x" % (left, right)
    428
    429          v4_mapped_prefix = [0x00] * 10 + [0xff] * 2
    430          if addr[:len(v4_mapped_prefix)] == v4_mapped_prefix:
    431              ip_address = '.'.join(str(i) for i in addr[-4:])
    432          else:
    433              try:
    434                  basic_v6_address = ':'.join(_gen_byte_pairs())
    435                  ip_address = str(IPAddress(basic_v6_address, 6))
    436              except ValueError:
    437                  self.log.warn("The IPv6 address is incorrect: %s", addr[:])
    438                  ip_address = "::"
    439          return ip_address
    440
    441 -    def processPacket(self, ip_address, port, pdu, ts):
    442          """
    443          Wrapper around asyncHandleTrap to process the provided packet.
    444
    445          @param pdu: Net-SNMP object
    446          @type pdu: netsnmp_pdu object
    447          @param ts: time stamp
    448          @type ts: datetime
    449          """
    450          # At the end of this callback, pdu will be deleted, so copy it
    451          # for asynchronous processing
    452          dup = netsnmp.lib.snmp_clone_pdu(c.byref(pdu))
    453          if not dup:
    454              self.log.error("Could not clone PDU for asynchronous processing")
    455              return
    456
    457          def cleanup(result):
    458              """
    459              Twisted callback to delete a previous memory allocation
    460
    461              @param result: Net-SNMP object
    462              @type result: netsnmp_pdu object
    463              @return: the result parameter
    464              @rtype: binary
    465              """
    466              netsnmp.lib.snmp_free_pdu(dup)
    467              return result
    468
    469          d = defer.maybeDeferred(self.asyncHandleTrap, (ip_address, port), dup.contents, ts)
    470          d.addBoth(cleanup)
    471
    472 -    def _value_from_dateandtime(self, value):
    473          """
    474          Tries converting a DateAndTime value to a printable string.
    475
    476          A date-time specification.
    477          field  octets  contents                  range
    478          -----  ------  --------                  -----
    479          1      1-2     year*                     0..65536
    480          2        3     month                     1..12
    481          3        4     day                       1..31
    482          4        5     hour                      0..23
    483          5        6     minutes                   0..59
    484          6        7     seconds                   0..60
    485                        (use 60 for leap-second)
    486          7        8     deci-seconds              0..9
    487          8        9     direction from UTC        '+' / '-'
    488          9       10     hours from UTC*           0..13
    489          10      11     minutes from UTC          0..59
    490          """
    491          strval = None
    492          vallen = len(value)
    493          if vallen == 8 or (vallen == 11 and value[8] in ('+','-')):
    494              (year, mon, day, hour, mins, secs, dsecs) = unpack(">HBBBBBB", value[:8])
    495              # Ensure valid date representation
    496              if mon < 1 or mon > 12:
    497                  return None
    498              if day < 1 or day > 31:
    499                  return None
    500              if hour < 0 or hour > 23:
    501                  return None
    502              if mins > 60:
    503                  return None
    504              if secs > 60:
    505                  return None
    506              if dsecs > 9:
    507                  return None
    508              if vallen == 11:
    509                  utc_dir = value[8]
    510                  (utc_hours, utc_mins) = unpack(">BB", value[9:])
    511              else:
    512                  tz_mins = time.timezone / 60
    513                  if tz_mins < 0:
    514                      utc_dir = '-'
    515                      tz_mins = -tz_mins
    516                  else:
    517                      utc_dir = '+'
    518                  utc_hours = tz_mins / 60
    519                  utc_mins = tz_mins % 60
    520              strval = "%04d-%02d-%02dT%02d:%02d:%02d.%d00%s%02d:%02d" % (year,
    521                  mon, day, hour, mins, secs, dsecs, utc_dir, utc_hours, utc_mins)
    522
    523          return strval
    524
    525 -    def _convert_value(self, value):
    526          if not isinstance(value, basestring):
    527              return value
    528          try:
    529              value.decode('utf8')
    530              return value
    531          except UnicodeDecodeError:
    532              # Try converting to a date
    533              decoded = self._value_from_dateandtime(value)
    534              if not decoded:
    535                  decoded = 'BASE64:' + base64.b64encode(value)
    536              return decoded
    537
    538 -    def snmpInform(self, addr, pdu):
    539          """
    540          A SNMP trap can request that the trap recipient return back a response.
    541          This is where we do that.
    542          """
    543          reply = netsnmp.lib.snmp_clone_pdu(c.byref(pdu))
    544          if not reply:
    545              self.log.error("Could not clone PDU for INFORM response")
    546              raise RuntimeError("Cannot respond to INFORM PDU")
    547          reply.contents.command = netsnmp.SNMP_MSG_RESPONSE
    548          reply.contents.errstat = 0
    549          reply.contents.errindex = 0
    550
    551          # FIXME: might need to add udp6 for IPv6 addresses
    552          sess = netsnmp.Session(peername='%s:%d' % tuple(addr),
    553                                 version=pdu.version)
    554          sess.open()
    555          if not netsnmp.lib.snmp_send(sess.sess, reply):
    556              netsnmp.lib.snmp_sess_perror("Unable to send inform PDU",
    557                                           self.session.sess)
    558              netsnmp.lib.snmp_free_pdu(reply)
    559          sess.close()
    560
    561 -    def decodeSnmpv1(self, addr, pdu):
    562          eventType = 'unknown'
    563          result = {}
    564
    565          variables = self.getResult(pdu)
    566
    567          # Sometimes the agent_addr is useless.
    568          # Use addr[0] unchanged in this case.
    569          # Note that SNMPv1 packets *cannot* come in via IPv6
    570          new_addr = '.'.join(map(str, [pdu.agent_addr[i] for i in range(4)]))
    571          result["device"] = addr[0] if new_addr == "0.0.0.0" else new_addr
    572
    573          enterprise = self.getEnterpriseString(pdu)
    574          eventType = self.oid2name(
    575                  enterprise, exactMatch=False, strip=False)
    576          generic = pdu.trap_type
    577          specific = pdu.specific_type
    578
    579          # Try an exact match with a .0. inserted between enterprise and
    580          # specific OID. It seems that MIBs frequently expect this .0.
    581          # to exist, but the device's don't send it in the trap.
    582          result["oid"] = "%s.0.%d" % (enterprise, specific)
    583          name = self.oid2name(result["oid"], exactMatch=True, strip=False)
    584
    585          # If we didn't get a match with the .0. inserted we will try
    586          # resolving with the .0. inserted and allow partial matches.
    587          if name == result["oid"]:
    588              result["oid"] = "%s.%d" % (enterprise, specific)
    589              name = self.oid2name(result["oid"], exactMatch=False, strip=False)
    590
    591          # Look for the standard trap types and decode them without
    592          # relying on any MIBs being loaded.
    593          eventType = {
    594              0: 'snmp_coldStart',
    595              1: 'snmp_warmStart',
    596              2: 'snmp_linkDown',
    597              3: 'snmp_linkUp',
    598              4: 'snmp_authenticationFailure',
    599              5: 'snmp_egpNeighorLoss',
    600              6: name,
    601          }.get(generic, name)
    602
    603          # Decode all variable bindings. Allow partial matches and strip
    604          # off any index values.
    605          for vb_oid, vb_value in variables:
    606              vb_value = self._convert_value(vb_value)
    607              vb_oid = '.'.join(map(str, vb_oid))
    608
    609              # Add a detail for the variable binding.
    610              r = self.oid2name(vb_oid, exactMatch=False, strip=False)
    611              result[r] = vb_value
    612
    613              # Add a detail for the index-stripped variable binding.
    614              r = self.oid2name(vb_oid, exactMatch=False, strip=True)
    615              result[r] = vb_value
    616          return eventType, result
    617
    618 -    def decodeSnmpv2(self, addr, pdu):
    619          eventType = 'unknown'
    620          result = {"oid": "", "device": addr[0]}
    621
    622          variables = self.getResult(pdu)
    623          for vb_oid, vb_value in variables:
    624              vb_value = self._convert_value(vb_value)
    625              vb_oid = '.'.join(map(str, vb_oid))
    626              # SNMPv2-MIB/snmpTrapOID
    627              if vb_oid == '1.3.6.1.6.3.1.1.4.1.0':
    628                  result["oid"] = '.'.join(map(str, vb_value))
    629                  eventType = self.oid2name(
    630                          vb_value, exactMatch=False, strip=False)
    631              else:
    632                  # Add a detail for the variable binding.
    633                  r = self.oid2name(vb_oid, exactMatch=False, strip=False)
    634                  result[r] = vb_value
    635                  # Add a detail for the index-stripped variable binding.
    636                  r = self.oid2name(vb_oid, exactMatch=False, strip=True)
    637                  result[r] = vb_value
    638          if eventType in ["linkUp", "linkDown"]:
    639              eventType = "snmp_" + eventType
    640          return eventType, result
    641
    642 -    def asyncHandleTrap(self, addr, pdu, startProcessTime):
    643          """
    644          Twisted callback to process a trap
    645
    646          @param addr: packet-sending host's IP address, port info
    647          @type addr: ( host-ip, port)
    648          @param pdu: Net-SNMP object
    649          @type pdu: netsnmp_pdu object
    650          @param startProcessTime: time stamp
    651          @type startProcessTime: datetime
    652          @return: Twisted deferred object
    653          @rtype: Twisted deferred object
    654          """
    655          self.capturePacket(addr[0], addr, pdu)
    656
    657          # Some misbehaving agents will send SNMPv1 traps contained within
    658          # an SNMPv2c PDU. So we can't trust tpdu.version to determine what
    659          # version trap exists within the PDU. We need to assume that a
    660          # PDU contains an SNMPv1 trap if the enterprise_length is greater
    661          # than zero in addition to the PDU version being 0.
    662          if pdu.version == SNMPv1 or pdu.enterprise_length > 0:
    663              eventType, result = self.decodeSnmpv1(addr, pdu)
    664          elif pdu.version in (SNMPv2, SNMPv3):
    665              eventType, result = self.decodeSnmpv2(addr, pdu)
    666          else:
    667              self.log.error("Unable to handle trap version %d", pdu.version)
    668              return
    669
    670          summary = 'snmp trap %s' % eventType
    671          self.log.debug(summary)
    672          community = self.getCommunity(pdu)
    673          result.setdefault('component', '')
    674          result.setdefault('eventClassKey', eventType)
    675          result.setdefault('eventGroup', 'trap')
    676          result.setdefault('severity', SEVERITY_WARNING)
    677          result.setdefault('summary', summary)
    678          result.setdefault('community', community)
    679          result.setdefault('firstTime', startProcessTime)
    680          result.setdefault('lastTime', startProcessTime)
    681          result.setdefault('monitor', self.options.monitor)
    682          self._eventService.sendEvent(result)
    683          self.stats.add(time.time() - startProcessTime)
    684
    685          if self.isReplaying():
    686              self.replayed += 1
    687              # Don't attempt to respond back if we're replaying packets
    688              return
    689
    690          if pdu.command == netsnmp.SNMP_MSG_INFORM:
    691              self.snmpInform(addr, pdu)
    692
    693 -    def displayStatistics(self):
    694          totalTime, totalEvents, maxTime = self.stats.report()
    695          display = "%d events processed in %.2f seconds" % (
    696                        totalEvents,
    697                        totalTime)
    698          if totalEvents > 0:
    699              display += """
    700  %.5f average seconds per event
    701  Maximum processing time for one event was %.5f""" % (
    702                         (totalTime / totalEvents), maxTime)
    703          return display
    704
    705 -    def cleanup(self):
    706          self.session.close()
    707          status = self.displayStatistics()
    708          self.log.info(status)
    709
    710
    711 -class MibConfigTask(ObservableMixin):
    712      """
    713      Receive a configuration object containing MIBs and update the
    714      mapping of OIDs to names.
    715      """
    716      zope.interface.implements(IScheduledTask)
    717
    718 -    def __init__(self, taskName, configId,
    719                   scheduleIntervalSeconds=3600, taskConfig=None):
    720          super(MibConfigTask, self).__init__()
    721
    722          # Needed for ZCA interface contract
    723          self.name = taskName
    724          self.configId = configId
    725          self.state = TaskStates.STATE_IDLE
    726          self.interval = scheduleIntervalSeconds
    727          self._preferences = taskConfig
    728          self._daemon = zope.component.getUtility(ICollector)
    729
    730          self._daemon.oidMap = self._preferences.oidMap
    731
    732 -    def doTask(self):
    733          return defer.succeed("Already updated OID -> name mappings...")
    734
    735 -    def cleanup(self):
    736          pass
    737
    738
    739 -class TrapDaemon(CollectorDaemon):
    740
    741 -    def runPostConfigTasks(self, result=None):
    742          # 1) super sets self._prefs.task with the call to postStartupTasks
    743          # 2) call remote createAllUsers
    744          # 3) service in turn walks DeviceClass tree and calls remote_createUser
    745          #      which needs self._prefs.task to be set
    746          CollectorDaemon.runPostConfigTasks(self, result)
    747          if not isinstance(result, Failure):
    748              service = self.getRemoteConfigServiceProxy()
    749              service.callRemote("createAllUsers")
    750
    751 -    def remote_createUser(self, user):
    752          log.debug("TrapDaemon.remote_createUser {0}".format(user))
    753          task = self._prefs.task
    754          if task is not None:
    755              task.session.create_users([user])
    756
    757  if __name__=='__main__':
    758      myPreferences = SnmpTrapPreferences()
    759      myTaskFactory = SimpleTaskFactory(MibConfigTask)
    760      myTaskSplitter = SimpleTaskSplitter(myTaskFactory)
    761      daemon = TrapDaemon(myPreferences, myTaskSplitter)
    762      daemon.run()
    763