1
2
3
4
5
6
7
8
9
10
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
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
40 family = [('family', c.c_ushort)]
41 if sys.platform == 'darwin':
42 family = [('len', c.c_ubyte), ('family', c.c_ubyte)]
43
45 _fields_ = family + [
46 ('port', c.c_ubyte * 2),
47 ('addr', c.c_ubyte * 4)
48 ];
49
50
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
60 "Convert a pointer to an array of longs to an oid"
61 return '.'.join([str(ptr[i]) for i in range(length)])
62
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
69 """
70 A fake object to make packet replaying feasible.
71 """
74
75
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
85 EventServer.__init__(self)
86
87 self.oidCache = {}
88
89
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
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
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"):
134 enterprise = pdu.enterprise
135 else:
136 enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length)
137 return enterprise
138
139
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"):
150 variables = pdu.variables
151 else:
152 variables = netsnmp.getResult(pdu)
153 return variables
154
155
156
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"):
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
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
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
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
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
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
243
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
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
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
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
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
355
356
357 transport = c.cast(pdu.transport_data, c.POINTER(sockaddr_in))
358 if not transport: return
359 transport = transport.contents
360
361
362 if transport.family != socket.AF_INET: return
363
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
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
383
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
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
433 variables = self.getResult(pdu)
434 for oid, value in variables:
435 oid = '.'.join(map(str, oid))
436
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
442 yield self.oid2name(oid, exactMatch=False, strip=False)
443 result[driver.next()] = value
444
445 yield self.oid2name(oid, exactMatch=False, strip=True)
446 result[driver.next()] = value
447
448 elif pdu.version == 0:
449
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
458
459
460 oid = "%s.0.%d" % (enterprise, specific)
461 yield self.oid2name(oid, exactMatch=True, strip=False)
462 name = driver.next()
463
464
465
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
472
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
484
485 for oid, value in variables:
486 oid = '.'.join(map(str, oid))
487
488 yield self.oid2name(oid, exactMatch=False, strip=False)
489 result[driver.next()] = value
490
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
514 if len(self.options.replayFilePrefix) > 0:
515 self.replayed += 1
516 return
517
518
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
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