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 errno
24 import base64
25 import logging
26 from struct import unpack
27 from ipaddr import IPAddress
28
29 log = logging.getLogger("zen.zentrap")
30
31
32 import ctypes as c
33
34 import Globals
35 import zope.interface
36 import zope.component
37
38 from twisted.python.failure import Failure
39 from twisted.internet import defer
40
41 from Products.ZenCollector.daemon import CollectorDaemon
42 from Products.ZenCollector.interfaces import ICollector, ICollectorPreferences,\
43 IEventService, \
44 IScheduledTask
45 from Products.ZenCollector.tasks import SimpleTaskFactory,\
46 SimpleTaskSplitter,\
47 BaseTask, TaskStates
48 from Products.ZenUtils.observable import ObservableMixin
49
50
51 from pynetsnmp import netsnmp, twistedsnmp
52
53 from Products.ZenHub.PBDaemon import FakeRemote
54 from Products.ZenUtils.captureReplay import CaptureReplay
55 from Products.ZenEvents.EventServer import Stats
56 from Products.ZenUtils.Utils import unused
57 from Products.ZenCollector.services.config import DeviceProxy
58 unused(DeviceProxy)
59 from Products.ZenHub.services.SnmpTrapConfig import User
60 unused(User)
61
62 from zenoss.protocols.protobufs.zep_pb2 import (SEVERITY_CRITICAL, SEVERITY_ERROR,
63 SEVERITY_WARNING, SEVERITY_INFO,
64 SEVERITY_DEBUG, SEVERITY_CLEAR)
65
66
67
68 family = [('family', c.c_ushort)]
69 if sys.platform == 'darwin':
70 family = [('len', c.c_ubyte), ('family', c.c_ubyte)]
71
73 _fields_ = family + [
74 ('port', c.c_ubyte * 2),
75 ('addr', c.c_ubyte * 4),
76 ]
77
79 _fields_ = family + [
80 ('port', c.c_ushort),
81 ('flow', c.c_ubyte * 4),
82 ('addr', c.c_ubyte * 16),
83 ('scope_id', c.c_ubyte * 4),
84 ]
85
86 _pre_parse_factory = c.CFUNCTYPE(c.c_int,
87 c.POINTER(netsnmp.netsnmp_session),
88 c.POINTER(netsnmp.netsnmp_transport),
89 c.c_void_p,
90 c.c_int)
91
92
93 netsnmp.lib.snmp_clone_pdu.restype = netsnmp.netsnmp_pdu_p
94
95
96 SNMPv1 = 0
97 SNMPv2 = 1
98 SNMPv3 = 3
99
101 """
102 A fake object to make packet replaying feasible.
103 """
106
107
109 zope.interface.implements(ICollectorPreferences)
110
112 """
113 Constructs a new PingCollectionPreferences instance and
114 provides default values for needed attributes.
115 """
116 self.collectorName = 'zentrap'
117 self.defaultRRDCreateCommand = None
118 self.configCycleInterval = 20
119 self.cycleInterval = 5 * 60
120
121
122
123 self.configurationService = 'Products.ZenHub.services.SnmpTrapConfig'
124
125
126 self.options = None
127
128 self.configCycleInterval = 20*60
129 self.task = None
130
132 self.task = TrapTask('zentrap', configId='zentrap')
133 yield self.task
134
136 """
137 Command-line options to be supported
138 """
139 TRAP_PORT = 162
140 try:
141 TRAP_PORT = socket.getservbyname('snmptrap', 'udp')
142 except socket.error:
143 pass
144 parser.add_option('--trapport', '-t',
145 dest='trapport', type='int', default=TRAP_PORT,
146 help="Listen for SNMP traps on this port rather than the default")
147 parser.add_option('--useFileDescriptor',
148 dest='useFileDescriptor',
149 type='int',
150 help=("Read from an existing connection "
151 " rather than opening a new port."),
152 default=None)
153
154 self.buildCaptureReplayOptions(parser)
155
156 - def postStartup(self):
157
158 daemon = zope.component.getUtility(ICollector)
159 daemon.oidMap = {}
160
162 "test if ipv6 is enabled"
163 try:
164 socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 0)
165 except socket.error, e:
166 if e.errno == errno.EAFNOSUPPORT:
167 return False
168 raise
169 return True
170
171 -class TrapTask(BaseTask, CaptureReplay):
172 """
173 Listen for SNMP traps and turn them into events
174 Connects to the TrapService service in zenhub.
175 """
176 zope.interface.implements(IScheduledTask)
177
178 - def __init__(self, taskName, configId,
179 scheduleIntervalSeconds=3600, taskConfig=None):
180 BaseTask.__init__(self, taskName, configId,
181 scheduleIntervalSeconds, taskConfig)
182 self.log = log
183
184
185 self.name = taskName
186 self.configId = configId
187 self.state = TaskStates.STATE_IDLE
188 self.interval = scheduleIntervalSeconds
189 self._daemon = zope.component.getUtility(ICollector)
190 self._eventService = zope.component.queryUtility(IEventService)
191 self._preferences = self._daemon
192
193
194 self.options = self._daemon.options
195
196 self.oidMap = self._daemon.oidMap
197 self.stats = Stats()
198
199
200 self.processCaptureReplayOptions()
201
202 trapPort = self._preferences.options.trapport
203 if not self._preferences.options.useFileDescriptor and trapPort < 1024:
204 listen_ip = "ipv6" if ipv6_is_enabled() else "0.0.0.0"
205
206 self._daemon.openPrivilegedPort('--listen', '--proto=udp', '--port=%s:%d' % (listen_ip, trapPort))
207 self.log("Unexpected return from openPrivilegedPort. Exiting.")
208 sys.exit(1)
209
210
211 self.log.info("Starting to listen on SNMP trap port %s", trapPort)
212 self.session = netsnmp.Session()
213 listening_protocol = "udp6" if ipv6_is_enabled() else "udp"
214 if self._preferences.options.useFileDescriptor is not None:
215
216 listening_address = listening_protocol + ':1162'
217 fileno = int(self._preferences.options.useFileDescriptor)
218 else:
219 listening_address = '%s:%d' % (listening_protocol, trapPort)
220 fileno = -1
221 self._pre_parse_callback = _pre_parse_factory(self._pre_parse)
222 debug = self.log.isEnabledFor(logging.DEBUG)
223 self.session.awaitTraps(listening_address, fileno, self._pre_parse_callback, debug)
224 self.session.callback = self.receiveTrap
225 twistedsnmp.updateReactor()
226
228 """
229 This is a wait-around task since we really are called
230 asynchronously.
231 """
232 return defer.succeed("Waiting for SNMP traps...")
233
235 """
236 @returns True if we are replaying a packet instead of capturing one
237 """
238 return len(self._preferences.options.replayFilePrefix) > 0
239
241 """
242 Get the enterprise string from the PDU or replayed packet
243
244 @param pdu: raw packet
245 @type pdu: binary
246 @return: enterprise string
247 @rtype: string
248 """
249 def lp2oid(ptr, length):
250 "Convert a pointer to an array of longs to an OID"
251 return '.'.join([str(ptr[i]) for i in range(length)])
252
253 if hasattr(pdu, "fake"):
254 enterprise = pdu.enterprise
255 else:
256 enterprise = lp2oid(pdu.enterprise, pdu.enterprise_length)
257 return enterprise
258
260 """
261 Get the values from the PDU or replayed packet
262
263 @param pdu: raw packet
264 @type pdu: binary
265 @return: variables from the PDU or Fake packet
266 @rtype: dictionary
267 """
268 if hasattr(pdu, "fake"):
269 variables = pdu.variables
270 else:
271 variables = netsnmp.getResult(pdu)
272 return variables
273
275 """
276 Get the community string from the PDU or replayed packet
277
278 @param pdu: raw packet
279 @type pdu: binary
280 @return: SNMP community
281 @rtype: string
282 """
283 community = ''
284 if hasattr(pdu, "fake"):
285 community = pdu.community
286 elif pdu.community_len:
287 community = c.string_at(pdu.community, pdu.community_len)
288
289 return community
290
292 """
293 Store the raw packet for later examination and troubleshooting.
294
295 @param addr: packet-sending host's IP address and port
296 @type addr: (string, number)
297 @param pdu: raw packet
298 @type pdu: binary
299 @return: Python FakePacket object
300 @rtype: Python FakePacket object
301 """
302 packet = FakePacket()
303 packet.version = pdu.version
304 packet.host = addr[0]
305 packet.port = addr[1]
306 packet.variables = netsnmp.getResult(pdu)
307 packet.community = ''
308 packet.enterprise_length = pdu.enterprise_length
309
310
311 if pdu.version == SNMPv1:
312
313 packet.agent_addr = [pdu.agent_addr[i] for i in range(4)]
314 packet.trap_type = pdu.trap_type
315 packet.specific_type = pdu.specific_type
316 packet.enterprise = self.getEnterpriseString(pdu)
317 packet.community = self.getCommunity(pdu)
318
319 return packet
320
322 """
323 Replay a captured packet
324
325 @param pdu: raw packet
326 @type pdu: binary
327 """
328 ts = time.time()
329 self.asyncHandleTrap([pdu.host, pdu.port], pdu, ts)
330
331 - def oid2name(self, oid, exactMatch=True, strip=False):
332 """
333 Returns a MIB name based on an OID and special handling flags.
334
335 @param oid: SNMP Object IDentifier
336 @type oid: string
337 @param exactMatch: find the full OID or don't match
338 @type exactMatch: boolean
339 @param strip: show what matched, or matched + numeric OID remainder
340 @type strip: boolean
341 @return: Twisted deferred object
342 @rtype: Twisted deferred object
343 """
344 if isinstance(oid, tuple):
345 oid = '.'.join(map(str, oid))
346
347 oid = oid.strip('.')
348 if exactMatch:
349 if oid in self.oidMap:
350 return self.oidMap[oid]
351 else:
352 return oid
353
354 oidlist = oid.split('.')
355 for i in range(len(oidlist), 0, -1):
356 name = self.oidMap.get('.'.join(oidlist[:i]), None)
357 if name is None:
358 continue
359
360 oid_trail = oidlist[i:]
361 if len(oid_trail) > 0 and not strip:
362 return "%s.%s" % (name, '.'.join(oid_trail))
363 else:
364 return name
365
366 return oid
367
368 - def _pre_parse(self, session, transport, transport_data, transport_data_length):
369 """Called before the net-snmp library parses the PDU. In the case
370 where a v3 trap comes in with unkwnown credentials, net-snmp silently
371 discards the packet. This method gives zentrap a way to log that these
372 packets were received to help with troubleshooting."""
373 if self.log.isEnabledFor(logging.DEBUG):
374 ipv6_socket_address = c.cast(transport_data, c.POINTER(sockaddr_in6)).contents
375 if ipv6_socket_address.family == socket.AF_INET6:
376 self.log.debug("pre_parse: IPv6 %s" % (socket.inet_ntop(socket.AF_INET6, ipv6_socket_address.addr)))
377 elif ipv6_socket_address.family == socket.AF_INET:
378 ipv4_socket_address = c.cast(transport_data, c.POINTER(sockaddr_in)).contents
379 self.log.debug("pre_parse: IPv4 %s" % socket.inet_ntop(socket.AF_INET, ipv4_socket_address.addr))
380 else:
381 self.log.debug("pre_parse: unexpected address family: %s" % ipv6_socket_address.family)
382 return 1
383
385 """
386 Accept a packet from the network and spin off a Twisted
387 deferred to handle the packet.
388
389 @param pdu: Net-SNMP object
390 @type pdu: netsnmp_pdu object
391 """
392 if pdu.version not in (SNMPv1, SNMPv2, SNMPv3):
393 self.log.error("Unable to handle trap version %d", pdu.version)
394 return
395 if pdu.transport_data is None:
396 self.log.error("PDU does not contain transport data")
397 return
398
399 ipv6_socket_address = c.cast(pdu.transport_data, c.POINTER(sockaddr_in6)).contents
400 if ipv6_socket_address.family == socket.AF_INET6:
401 if pdu.transport_data_length < c.sizeof(sockaddr_in6):
402 self.log.error("PDU transport data is too small for sockaddr_in6 struct.")
403 return
404 ip_address = self.getPacketIp(ipv6_socket_address.addr)
405 elif ipv6_socket_address.family == socket.AF_INET:
406 if pdu.transport_data_length < c.sizeof(sockaddr_in):
407 self.log.error("PDU transport data is too small for sockaddr_in struct.")
408 return
409 ipv4_socket_address = c.cast(pdu.transport_data, c.POINTER(sockaddr_in)).contents
410 ip_address = '.'.join(str(i) for i in ipv4_socket_address.addr)
411 else:
412 self.log.error("Got a packet with unrecognized network family: %s", ipv6_socket_address.family)
413 return
414
415 port = socket.ntohs(ipv6_socket_address.port)
416 self.log.debug( "Received packet from %s at port %s" % (ip_address, port) )
417 self.processPacket(ip_address, port, pdu, time.time())
418
420 """
421 For IPv4, convert a pointer to 4 bytes to a dotted-ip-address
422 For IPv6, convert a pointer to 16 bytes to a canonical IPv6 address.
423 """
424
425 def _gen_byte_pairs():
426 for left, right in zip(addr[::2], addr[1::2]):
427 yield "%.2x%.2x" % (left, right)
428
429 v4_mapped_prefix = [0x00] * 10 + [0xff] * 2
430 if addr[:len(v4_mapped_prefix)] == v4_mapped_prefix:
431 ip_address = '.'.join(str(i) for i in addr[-4:])
432 else:
433 try:
434 basic_v6_address = ':'.join(_gen_byte_pairs())
435 ip_address = str(IPAddress(basic_v6_address, 6))
436 except ValueError:
437 self.log.warn("The IPv6 address is incorrect: %s", addr[:])
438 ip_address = "::"
439 return ip_address
440
442 """
443 Wrapper around asyncHandleTrap to process the provided packet.
444
445 @param pdu: Net-SNMP object
446 @type pdu: netsnmp_pdu object
447 @param ts: time stamp
448 @type ts: datetime
449 """
450
451
452 dup = netsnmp.lib.snmp_clone_pdu(c.byref(pdu))
453 if not dup:
454 self.log.error("Could not clone PDU for asynchronous processing")
455 return
456
457 def cleanup(result):
458 """
459 Twisted callback to delete a previous memory allocation
460
461 @param result: Net-SNMP object
462 @type result: netsnmp_pdu object
463 @return: the result parameter
464 @rtype: binary
465 """
466 netsnmp.lib.snmp_free_pdu(dup)
467 return result
468
469 d = defer.maybeDeferred(self.asyncHandleTrap, (ip_address, port), dup.contents, ts)
470 d.addBoth(cleanup)
471
473 """
474 Tries converting a DateAndTime value to a printable string.
475
476 A date-time specification.
477 field octets contents range
478 ----- ------ -------- -----
479 1 1-2 year* 0..65536
480 2 3 month 1..12
481 3 4 day 1..31
482 4 5 hour 0..23
483 5 6 minutes 0..59
484 6 7 seconds 0..60
485 (use 60 for leap-second)
486 7 8 deci-seconds 0..9
487 8 9 direction from UTC '+' / '-'
488 9 10 hours from UTC* 0..13
489 10 11 minutes from UTC 0..59
490 """
491 strval = None
492 vallen = len(value)
493 if vallen == 8 or (vallen == 11 and value[8] in ('+','-')):
494 (year, mon, day, hour, mins, secs, dsecs) = unpack(">HBBBBBB", value[:8])
495
496 if mon < 1 or mon > 12:
497 return None
498 if day < 1 or day > 31:
499 return None
500 if hour < 0 or hour > 23:
501 return None
502 if mins > 60:
503 return None
504 if secs > 60:
505 return None
506 if dsecs > 9:
507 return None
508 if vallen == 11:
509 utc_dir = value[8]
510 (utc_hours, utc_mins) = unpack(">BB", value[9:])
511 else:
512 tz_mins = time.timezone / 60
513 if tz_mins < 0:
514 utc_dir = '-'
515 tz_mins = -tz_mins
516 else:
517 utc_dir = '+'
518 utc_hours = tz_mins / 60
519 utc_mins = tz_mins % 60
520 strval = "%04d-%02d-%02dT%02d:%02d:%02d.%d00%s%02d:%02d" % (year,
521 mon, day, hour, mins, secs, dsecs, utc_dir, utc_hours, utc_mins)
522
523 return strval
524
537
560
562 eventType = 'unknown'
563 result = {}
564
565 variables = self.getResult(pdu)
566
567
568
569
570 new_addr = '.'.join(map(str, [pdu.agent_addr[i] for i in range(4)]))
571 result["device"] = addr[0] if new_addr == "0.0.0.0" else new_addr
572
573 enterprise = self.getEnterpriseString(pdu)
574 eventType = self.oid2name(
575 enterprise, exactMatch=False, strip=False)
576 generic = pdu.trap_type
577 specific = pdu.specific_type
578
579
580
581
582 result["oid"] = "%s.0.%d" % (enterprise, specific)
583 name = self.oid2name(result["oid"], exactMatch=True, strip=False)
584
585
586
587 if name == result["oid"]:
588 result["oid"] = "%s.%d" % (enterprise, specific)
589 name = self.oid2name(result["oid"], exactMatch=False, strip=False)
590
591
592
593 eventType = {
594 0: 'snmp_coldStart',
595 1: 'snmp_warmStart',
596 2: 'snmp_linkDown',
597 3: 'snmp_linkUp',
598 4: 'snmp_authenticationFailure',
599 5: 'snmp_egpNeighorLoss',
600 6: name,
601 }.get(generic, name)
602
603
604
605 for vb_oid, vb_value in variables:
606 vb_value = self._convert_value(vb_value)
607 vb_oid = '.'.join(map(str, vb_oid))
608
609
610 r = self.oid2name(vb_oid, exactMatch=False, strip=False)
611 result[r] = vb_value
612
613
614 r = self.oid2name(vb_oid, exactMatch=False, strip=True)
615 result[r] = vb_value
616 return eventType, result
617
619 eventType = 'unknown'
620 result = {"oid": "", "device": addr[0]}
621
622 variables = self.getResult(pdu)
623 for vb_oid, vb_value in variables:
624 vb_value = self._convert_value(vb_value)
625 vb_oid = '.'.join(map(str, vb_oid))
626
627 if vb_oid == '1.3.6.1.6.3.1.1.4.1.0':
628 result["oid"] = '.'.join(map(str, vb_value))
629 eventType = self.oid2name(
630 vb_value, exactMatch=False, strip=False)
631 else:
632
633 r = self.oid2name(vb_oid, exactMatch=False, strip=False)
634 result[r] = vb_value
635
636 r = self.oid2name(vb_oid, exactMatch=False, strip=True)
637 result[r] = vb_value
638 if eventType in ["linkUp", "linkDown"]:
639 eventType = "snmp_" + eventType
640 return eventType, result
641
643 """
644 Twisted callback to process a trap
645
646 @param addr: packet-sending host's IP address, port info
647 @type addr: ( host-ip, port)
648 @param pdu: Net-SNMP object
649 @type pdu: netsnmp_pdu object
650 @param startProcessTime: time stamp
651 @type startProcessTime: datetime
652 @return: Twisted deferred object
653 @rtype: Twisted deferred object
654 """
655 self.capturePacket(addr[0], addr, pdu)
656
657
658
659
660
661
662 if pdu.version == SNMPv1 or pdu.enterprise_length > 0:
663 eventType, result = self.decodeSnmpv1(addr, pdu)
664 elif pdu.version in (SNMPv2, SNMPv3):
665 eventType, result = self.decodeSnmpv2(addr, pdu)
666 else:
667 self.log.error("Unable to handle trap version %d", pdu.version)
668 return
669
670 summary = 'snmp trap %s' % eventType
671 self.log.debug(summary)
672 community = self.getCommunity(pdu)
673 result.setdefault('component', '')
674 result.setdefault('eventClassKey', eventType)
675 result.setdefault('eventGroup', 'trap')
676 result.setdefault('severity', SEVERITY_WARNING)
677 result.setdefault('summary', summary)
678 result.setdefault('community', community)
679 result.setdefault('firstTime', startProcessTime)
680 result.setdefault('lastTime', startProcessTime)
681 result.setdefault('monitor', self.options.monitor)
682 self._eventService.sendEvent(result)
683 self.stats.add(time.time() - startProcessTime)
684
685 if self.isReplaying():
686 self.replayed += 1
687
688 return
689
690 if pdu.command == netsnmp.SNMP_MSG_INFORM:
691 self.snmpInform(addr, pdu)
692
704
709
710
712 """
713 Receive a configuration object containing MIBs and update the
714 mapping of OIDs to names.
715 """
716 zope.interface.implements(IScheduledTask)
717
718 - def __init__(self, taskName, configId,
719 scheduleIntervalSeconds=3600, taskConfig=None):
720 super(MibConfigTask, self).__init__()
721
722
723 self.name = taskName
724 self.configId = configId
725 self.state = TaskStates.STATE_IDLE
726 self.interval = scheduleIntervalSeconds
727 self._preferences = taskConfig
728 self._daemon = zope.component.getUtility(ICollector)
729
730 self._daemon.oidMap = self._preferences.oidMap
731
733 return defer.succeed("Already updated OID -> name mappings...")
734
737
738
740
741 - def runPostConfigTasks(self, result=None):
742
743
744
745
746 CollectorDaemon.runPostConfigTasks(self, result)
747 if not isinstance(result, Failure):
748 service = self.getRemoteConfigServiceProxy()
749 service.callRemote("createAllUsers")
750
752 log.debug("TrapDaemon.remote_createUser {0}".format(user))
753 task = self._prefs.task
754 if task is not None:
755 task.session.create_users([user])
756
757 if __name__=='__main__':
758 myPreferences = SnmpTrapPreferences()
759 myTaskFactory = SimpleTaskFactory(MibConfigTask)
760 myTaskSplitter = SimpleTaskSplitter(myTaskFactory)
761 daemon = TrapDaemon(myPreferences, myTaskSplitter)
762 daemon.run()
763