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