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

Source Code for Module 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   
 38   
 39  # This is what struct sockaddr_in {} looks like 
 40  family = [('family', c.c_ushort)] 
 41  if sys.platform == 'darwin': 
 42      family = [('len', c.c_ubyte), ('family', c.c_ubyte)] 
 43   
44 -class sockaddr_in(c.Structure):
45 _fields_ = family + [ 46 ('port', c.c_ubyte * 2), # need to decode from net-byte-order 47 ('addr', c.c_ubyte * 4) 48 ];
49 50 # teach python that the return type of snmp_clone_pdu is a pdu pointer 51 netsnmp.lib.snmp_clone_pdu.restype = netsnmp.netsnmp_pdu_p 52 53 TRAP_PORT = 162 54 try: 55 TRAP_PORT = socket.getservbyname('snmptrap', 'udp') 56 except socket.error: 57 pass 58
59 -def lp2oid(ptr, length):
60 "Convert a pointer to an array of longs to an oid" 61 return '.'.join([str(ptr[i]) for i in range(length)])
62
63 -def bp2ip(ptr):
64 "Convert a pointer to 4 bytes to a dotted-ip-address" 65 return '.'.join([str(ptr[i]) for i in range(4)])
66 67
68 -class FakePacket(object):
69 """ 70 A fake object to make packet replaying feasible. 71 """
72 - def __init__(self):
73 self.fake = True
74 75
76 -class ZenTrap(EventServer):
77 """ 78 Listen for SNMP traps and turn them into events 79 Connects to the EventService service in zenhub. 80 """ 81 82 name = 'zentrap' 83
84 - def __init__(self):
85 EventServer.__init__(self) 86 87 self.oidCache = {} 88 89 # Command-line argument sanity checking 90 if self.options.captureFilePrefix and len(self.options.replayFilePrefix) > 0: 91 self.log.error( "Can't specify both --captureFilePrefix and -replayFilePrefix" \ 92 " at the same time. Exiting" ) 93 sys.exit(1) 94 95 if self.options.captureFilePrefix and not self.options.captureAll and \ 96 self.options.captureIps == '': 97 self.log.warn( "Must specify either --captureIps or --captureAll for" + \ 98 " --capturePrefix to take effect. Ignoring option --capturePrefix" ) 99 100 if len(self.options.replayFilePrefix) > 0: 101 self.connected = self.replayAll 102 return 103 104 if not self.options.useFileDescriptor and self.options.trapport < 1024: 105 self.openPrivilegedPort('--listen', '--proto=udp', 106 '--port=%s:%d' % (self.options.listenip, 107 self.options.trapport)) 108 self.session = netsnmp.Session() 109 if self.options.useFileDescriptor is not None: 110 fileno = int(self.options.useFileDescriptor) 111 # open port 1162, but then dup fileno onto it 112 self.session.awaitTraps('%s:1162' % self.options.listenip, fileno) 113 else: 114 self.session.awaitTraps('%s:%d' % ( 115 self.options.listenip, self.options.trapport)) 116 self.session.callback = self.receiveTrap 117 118 self.captureSerialNum = 0 119 self.captureIps = self.options.captureIps.split(',') 120 121 twistedsnmp.updateReactor()
122 123
124 - def getEnterpriseString(self, pdu):
125 """ 126 Get the enterprise string from the PDU or replayed packet 127 128 @param pdu: raw packet 129 @type pdu: binary 130 @return: enterprise string 131 @rtype: string 132 """ 133 if hasattr(pdu, "fake"): # Replaying a packet 134 enterprise = pdu.enterprise 135 else: 136 enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length) 137 return enterprise
138 139
140 - def getResult(self, pdu):
141 """ 142 Get the values from the PDU or replayed packet 143 144 @param pdu: raw packet 145 @type pdu: binary 146 @return: variables from the PDU or Fake packet 147 @rtype: dictionary 148 """ 149 if hasattr(pdu, "fake"): # Replaying a packet 150 variables = pdu.variables 151 else: 152 variables = netsnmp.getResult(pdu) 153 return variables
154 155 156
157 - def getCommunity(self, pdu):
158 """ 159 Get the communitry string from the PDU or replayed packet 160 161 @param pdu: raw packet 162 @type pdu: binary 163 @return: SNMP community 164 @rtype: string 165 """ 166 community = '' 167 if hasattr(pdu, "fake"): # Replaying a packet 168 community = pdu.community 169 elif pdu.community_len: 170 community = c.string_at(pdu.community, pdu.community_len) 171 172 return community
173 174
175 - def convertPacketToPython(self, addr, pdu):
176 """ 177 Store the raw packet for later examination and troubleshooting. 178 179 @param addr: packet-sending host's IP address and port 180 @type addr: (string, number) 181 @param pdu: raw packet 182 @type pdu: binary 183 @return: Python FakePacket object 184 @rtype: Python FakePacket object 185 """ 186 packet = FakePacket() 187 packet.version = pdu.version 188 packet.host = addr[0] 189 packet.port = addr[1] 190 packet.variables = netsnmp.getResult(pdu) 191 packet.community = '' 192 193 # Here's where we start to encounter differences between packet types 194 if pdu.version == 0: 195 packet.agent_addr = [pdu.agent_addr[i] for i in range(4)] 196 packet.trap_type = pdu.trap_type 197 packet.specific_type = pdu.specific_type 198 packet.enterprise = self.getEnterpriseString(pdu) 199 packet.community = self.getCommunity(pdu) 200 201 return packet
202 203
204 - def capturePacket(self, addr, pdu):
205 """ 206 Store the raw packet for later examination and troubleshooting. 207 208 @param addr: packet-sending host's IP address and port 209 @type addr: (string, number) 210 @param pdu: raw packet 211 @type pdu: binary 212 """ 213 # Save the raw data if requested to do so 214 if not self.options.captureFilePrefix: 215 return 216 host = addr[0] 217 if not self.options.captureAll and host not in self.captureIps: 218 self.log.debug( "Received packet from %s, but not in %s" % (host, 219 self.captureIps)) 220 return 221 222 self.log.debug( "Capturing packet from %s" % host ) 223 name = "%s-%s-%d" % (self.options.captureFilePrefix, host, self.captureSerialNum) 224 try: 225 packet = self.convertPacketToPython(addr, pdu) 226 capFile = open( name, "wb") 227 data= cPickle.dumps(packet, cPickle.HIGHEST_PROTOCOL) 228 capFile.write(data) 229 capFile.close() 230 self.captureSerialNum += 1 231 except: 232 self.log.exception("Couldn't write capture data to '%s'" % name )
233 234
235 - def replayAll(self):
236 """ 237 Replay all captured packets using the files specified in 238 the --replayFilePrefix option and then exit. 239 240 Note that this calls the Twisted stop() method 241 """ 242 # Note what you are about to see below is a direct result of optparse 243 # adding in the arguments *TWICE* each time --replayFilePrefix is used. 244 import glob 245 files = [] 246 for filespec in self.options.replayFilePrefix: 247 files += glob.glob( filespec + '*' ) 248 249 self.loaded = 0 250 self.replayed = 0 251 from sets import Set 252 for file in Set(files): 253 self.log.debug( "Attempting to read packet data from '%s'" % file ) 254 try: 255 fp = open( file, "rb" ) 256 pdu= cPickle.load(fp) 257 fp.close() 258 self.loaded += 1 259 260 except (IOError, EOFError): 261 fp.close() 262 self.log.exception( "Unable to load packet data from %s" % file ) 263 continue 264 265 self.replay(pdu) 266 267 self.replayStop()
268 269
270 - def replay(self, pdu):
271 """ 272 Replay a captured packet 273 274 @param pdu: raw packet 275 @type pdu: binary 276 """ 277 ts = time.time() 278 d = self.asyncHandleTrap([pdu.host, pdu.port], pdu, ts)
279 280
281 - def replayStop(self):
282 """ 283 Twisted method that we use to override the default stop() method 284 for when we are replaying packets. This version waits to make 285 sure that all of our deferreds have exited before pulling the plug. 286 """ 287 self.log.debug( "Replayed %d of %d packets" % (self.replayed, self.loaded ) ) 288 if self.replayed == self.loaded: 289 self.log.info( "Loaded and replayed %d packets" % self.replayed ) 290 self.stop() 291 else: 292 reactor.callLater( 1, self.replayStop )
293 294 295
296 - def oid2name(self, oid, exactMatch=True, strip=False):
297 """ 298 Get OID name from cache or ZenHub 299 300 @param oid: SNMP Object IDentifier 301 @type oid: string 302 @param exactMatch: find the full OID or don't match 303 @type exactMatch: boolean 304 @param strip: show what matched, or matched + numeric OID remainder 305 @type strip: boolean 306 @return: Twisted deferred object 307 @rtype: Twisted deferred object 308 """ 309 if type(oid) == type(()): 310 oid = '.'.join(map(str, oid)) 311 cacheKey = "%s:%r:%r" % (oid, exactMatch, strip) 312 if self.oidCache.has_key(cacheKey): 313 return defer.succeed(self.oidCache[cacheKey]) 314 315 self.log.debug("OID cache miss on %s (exactMatch=%r, strip=%r)" % ( 316 oid, exactMatch, strip)) 317 d = self.model().callRemote('oid2name', oid, exactMatch, strip) 318 319 def cache(name, key): 320 """ 321 Twisted callback to cache and return the name 322 323 @param name: human-readable-name form of OID 324 @type name: string 325 @param key: key of OID and params 326 @type key: string 327 @return: the name parameter 328 @rtype: string 329 """ 330 self.oidCache[key] = name 331 return name
332 333 d.addCallback(cache, cacheKey) 334 return d
335 336
337 - def receiveTrap(self, pdu):
338 """ 339 Accept a packet from the network and spin off a Twisted 340 deferred to handle the packet. 341 342 @param pdu: Net-SNMP object 343 @type pdu: netsnmp_pdu object 344 """ 345 ts = time.time() 346 347 # Is it a trap? 348 if pdu.sessid != 0: return 349 350 if pdu.version not in [ 0, 1 ]: 351 self.log.error("Unable to handle trap version %d", pdu.version) 352 return 353 354 # What address did it come from? 355 # for now, we'll make the scary assumption this data is a 356 # sockaddr_in 357 transport = c.cast(pdu.transport_data, c.POINTER(sockaddr_in)) 358 if not transport: return 359 transport = transport.contents 360 361 # Just to make sure, check to see that it is type AF_INET 362 if transport.family != socket.AF_INET: return 363 # get the address out as ( host-ip, port) 364 addr = [bp2ip(transport.addr), 365 transport.port[0] << 8 | transport.port[1]] 366 367 self.log.debug( "Received packet from %s at port %s" % (addr[0], addr[1]) ) 368 self.processPacket(addr, pdu, ts)
369 370
371 - def processPacket(self, addr, pdu, ts):
372 """ 373 Wrapper around asyncHandleTrap to process the provided packet. 374 375 @param addr: packet-sending host's IP address, port info 376 @type addr: ( host-ip, port) 377 @param pdu: Net-SNMP object 378 @type pdu: netsnmp_pdu object 379 @param ts: time stamp 380 @type ts: datetime 381 """ 382 # At the end of this callback, pdu will be deleted, so copy it 383 # for asynchronous processing 384 dup = netsnmp.lib.snmp_clone_pdu(c.addressof(pdu)) 385 if not dup: 386 self.log.error("Could not clone PDU for asynchronous processing") 387 return 388 389 def cleanup(result): 390 """ 391 Twisted callback to delete a previous memory allocation 392 393 @param result: Net-SNMP object 394 @type result: netsnmp_pdu object 395 @return: the result parameter 396 @rtype: binary 397 """ 398 netsnmp.lib.snmp_free_pdu(dup) 399 return result
400 401 d = self.asyncHandleTrap(addr, dup.contents, ts) 402 d.addBoth(cleanup) 403 404
405 - def asyncHandleTrap(self, addr, pdu, ts):
406 """ 407 Twisted callback to process a trap 408 409 @param addr: packet-sending host's IP address, port info 410 @type addr: ( host-ip, port) 411 @param pdu: Net-SNMP object 412 @type pdu: netsnmp_pdu object 413 @param ts: time stamp 414 @type ts: datetime 415 @return: Twisted deferred object 416 @rtype: Twisted deferred object 417 """ 418 def inner(driver): 419 """ 420 Generator function that actually processes the packet 421 422 @param driver: Twisted deferred object 423 @type driver: Twisted deferred object 424 @return: Twisted deferred object 425 @rtype: Twisted deferred object 426 """ 427 self.capturePacket( addr, pdu) 428 429 eventType = 'unknown' 430 result = {} 431 if pdu.version == 1: 432 # SNMP v2 433 variables = self.getResult(pdu) 434 for oid, value in variables: 435 oid = '.'.join(map(str, oid)) 436 # SNMPv2-MIB/snmpTrapOID 437 if oid == '1.3.6.1.6.3.1.1.4.1.0': 438 yield self.oid2name(value, exactMatch=False, strip=False) 439 eventType = driver.next() 440 else: 441 # Add a detail for the variable binding. 442 yield self.oid2name(oid, exactMatch=False, strip=False) 443 result[driver.next()] = value 444 # Add a detail for the index-stripped variable binding. 445 yield self.oid2name(oid, exactMatch=False, strip=True) 446 result[driver.next()] = value 447 448 elif pdu.version == 0: 449 # SNMP v1 450 variables = self.getResult(pdu) 451 addr[0] = '.'.join(map(str, [pdu.agent_addr[i] for i in range(4)])) 452 enterprise = self.getEnterpriseString(pdu) 453 eventType = driver.next() 454 generic = pdu.trap_type 455 specific = pdu.specific_type 456 457 # Try an exact match with a .0. inserted between enterprise and 458 # specific OID. It seems that MIBs frequently expect this .0. 459 # to exist, but the device's don't send it in the trap. 460 oid = "%s.0.%d" % (enterprise, specific) 461 yield self.oid2name(oid, exactMatch=True, strip=False) 462 name = driver.next() 463 464 # If we didn't get a match with the .0. inserted we will try 465 # resolving withing the .0. inserted and allow partial matches. 466 if name == oid: 467 oid = "%s.%d" % (enterprise, specific) 468 yield self.oid2name(oid, exactMatch=False, strip=False) 469 name = driver.next() 470 471 # Look for the standard trap types and decode them without 472 # relying on any MIBs being loaded. 473 eventType = { 474 0: 'snmp_coldStart', 475 1: 'snmp_warmStart', 476 2: 'snmp_linkDown', 477 3: 'snmp_linkUp', 478 4: 'snmp_authenticationFailure', 479 5: 'snmp_egpNeighorLoss', 480 6: name, 481 }.get(generic, name) 482 483 # Decode all variable bindings. Allow partial matches and strip 484 # off any index values. 485 for oid, value in variables: 486 oid = '.'.join(map(str, oid)) 487 # Add a detail for the variable binding. 488 yield self.oid2name(oid, exactMatch=False, strip=False) 489 result[driver.next()] = value 490 # Add a detail for the index-stripped variable binding. 491 yield self.oid2name(oid, exactMatch=False, strip=True) 492 result[driver.next()] = value 493 else: 494 self.log.error("Unable to handle trap version %d", pdu.version) 495 return 496 497 summary = 'snmp trap %s' % eventType 498 self.log.debug(summary) 499 community = self.getCommunity(pdu) 500 result['oid'] = oid 501 result['device'] = addr[0] 502 result.setdefault('component', '') 503 result.setdefault('eventClassKey', eventType) 504 result.setdefault('eventGroup', 'trap') 505 result.setdefault('severity', 3) 506 result.setdefault('summary', summary) 507 result.setdefault('community', community) 508 result.setdefault('firstTime', ts) 509 result.setdefault('lastTime', ts) 510 result.setdefault('monitor', self.options.monitor) 511 self.sendEvent(result) 512 513 # Don't attempt to respond back if we're replaying packets 514 if len(self.options.replayFilePrefix) > 0: 515 self.replayed += 1 516 return 517 518 # respond to INFORM requests 519 if pdu.command == netsnmp.SNMP_MSG_INFORM: 520 reply = netsnmp.lib.snmp_clone_pdu(c.addressof(pdu)) 521 if not reply: 522 self.log.error("Could not clone PDU for INFORM response") 523 raise RuntimeError("Cannot respond to INFORM PDU") 524 reply.contents.command = netsnmp.SNMP_MSG_RESPONSE 525 reply.contents.errstat = 0 526 reply.contents.errindex = 0 527 sess = netsnmp.Session(peername='%s:%d' % tuple(addr), 528 version=pdu.version) 529 sess.open() 530 if not netsnmp.lib.snmp_send(sess.sess, reply): 531 netsnmp.lib.snmp_sess_perror("Unable to send inform PDU", 532 self.session.sess) 533 netsnmp.lib.snmp_free_pdu(reply) 534 sess.close()
535 return drive(inner) 536 537
538 - def buildOptions(self):
539 """ 540 Command-line options to be supported 541 """ 542 EventServer.buildOptions(self) 543 self.parser.add_option('--trapport', '-t', 544 dest='trapport', type='int', default=TRAP_PORT, 545 help="Listen for SNMP traps on this port rather than the default") 546 self.parser.add_option('--listenip', 547 dest='listenip', default='0.0.0.0', 548 help="IP address to listen on. Default is 0.0.0.0") 549 self.parser.add_option('--useFileDescriptor', 550 dest='useFileDescriptor', 551 type='int', 552 help=("Read from an existing connection " 553 " rather than opening a new port."), 554 default=None) 555 self.parser.add_option('--captureFilePrefix', 556 dest='captureFilePrefix', 557 default=None, 558 help="Directory and filename to use as a template" + \ 559 " to store captured raw trap packets.") 560 self.parser.add_option('--captureAll', 561 dest='captureAll', 562 action='store_true', 563 default=False, 564 help="Capture all packets.") 565 self.parser.add_option('--captureIps', 566 dest='captureIps', 567 default='', 568 help="Comma-separated list of IP addresses to capture.") 569 self.parser.add_option('--replayFilePrefix', 570 dest='replayFilePrefix', 571 action='append', 572 default=[], 573 help="Filename prefix containing captured packet data. Can specify more than once.")
574 575 576 577 if __name__ == '__main__': 578 z = ZenTrap() 579 z.run() 580 z.report() 581