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

Source Code for Module Products.ZenEvents.zentrap

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