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.ZenHub.PBDaemon import FakeRemote
37 from Products.ZenUtils.Driver import drive
38 from Products.ZenUtils.captureReplay import CaptureReplay
39
40
41
42
43 family = [('family', c.c_ushort)]
44 if sys.platform == 'darwin':
45 family = [('len', c.c_ubyte), ('family', c.c_ubyte)]
46
48 _fields_ = family + [
49 ('port', c.c_ubyte * 2),
50 ('addr', c.c_ubyte * 4)
51 ];
52
53
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
63 "Convert a pointer to an array of longs to an oid"
64 return '.'.join([str(ptr[i]) for i in range(length)])
65
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
72 """
73 A fake object to make packet replaying feasible.
74 """
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
90 EventServer.__init__(self)
91
92
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
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
112 """
113 @returns True if we are replaying a packet instead of capturing one
114 """
115 return len(self.options.replayFilePrefix) > 0
116
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
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"):
161 enterprise = pdu.enterprise
162 else:
163 enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length)
164 return enterprise
165
166
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"):
177 variables = pdu.variables
178 else:
179 variables = netsnmp.getResult(pdu)
180 return variables
181
182
183
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"):
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
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
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
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
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
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
301
302
303 transport = c.cast(pdu.transport_data, c.POINTER(sockaddr_in))
304 if not transport: return
305 transport = transport.contents
306
307
308 if transport.family != socket.AF_INET: return
309
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
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
329
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
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
380
381
382
383
384
385 if pdu.version == 0 or pdu.enterprise_length > 0:
386
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
396
397
398 oid = "%s.0.%d" % (enterprise, specific)
399 name = self.oid2name(oid, exactMatch=True, strip=False)
400
401
402
403 if name == oid:
404 oid = "%s.%d" % (enterprise, specific)
405 name = self.oid2name(oid, exactMatch=False, strip=False)
406
407
408
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
420
421 for vb_oid, vb_value in variables:
422 vb_oid = '.'.join(map(str, vb_oid))
423
424 r = self.oid2name(vb_oid, exactMatch=False, strip=False)
425 result[r] = vb_value
426
427 r = self.oid2name(vb_oid, exactMatch=False, strip=True)
428 result[r] = vb_value
429
430 elif pdu.version == 1:
431
432 variables = self.getResult(pdu)
433 for vb_oid, vb_value in variables:
434 vb_oid = '.'.join(map(str, vb_oid))
435
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
442 r = self.oid2name(vb_oid, exactMatch=False, strip=False)
443 result[r] = vb_value
444
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
469 if len(self.options.replayFilePrefix) > 0:
470 self.replayed += 1
471 return
472
473
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
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