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