1
2
3
4
5
6
7
8
9
10
11 import re
12 from socket import getaddrinfo
13 from traceback import format_exc
14 from zope.interface import implements
15 from zope.component import getUtilitiesFor
16
17 from pynetsnmp import netsnmp
18
19 from twisted.internet.protocol import ProcessProtocol
20
21 from email.MIMEText import MIMEText
22 from email.MIMEMultipart import MIMEMultipart
23 from email.Utils import formatdate
24
25 from zenoss.protocols.protobufs import zep_pb2
26 from Products.ZenEvents.events2.proxy import EventSummaryProxy
27 from Products.ZenUtils.Utils import sendEmail
28 from Products.Zuul.interfaces.actions import IEmailActionContentInfo, IPageActionContentInfo, ICommandActionContentInfo, ISnmpTrapActionContentInfo
29 from Products.Zuul.form.interfaces import IFormBuilder
30 from Products.ZenModel.UserSettings import GroupSettings
31 from Products.ZenModel.interfaces import IAction, IProvidesEmailAddresses, IProvidesPagerAddresses, IProcessSignal, INotificationContextProvider
32 from Products.ZenModel.NotificationSubscription import NotificationEventContextWrapper
33 from Products.ZenEvents.Event import Event
34 from Products.ZenUtils import Utils
35 from Products.ZenUtils.guid.guid import GUIDManager
36 from Products.ZenUtils.ProcessQueue import ProcessQueue
37 from Products.ZenEvents.ZenEventClasses import Warning as SEV_WARNING
38 from Products.ZenUtils.ZenTales import talEval
39
40 import logging
41
42 log = logging.getLogger("zen.actions")
43
44
45
46
48
49
51
52
54 - def __init__(self, action, notification, exceptionTargets):
55 self.action = action
56 self.notificationId = notification.id
57 self.exceptionTargets = exceptionTargets
59 return "Failed {action} for notification {notification} on targets {targets}".format(
60 action=self.action.name,
61 notification=self.notificationId,
62 targets = ','.join(self.exceptionTargets)
63 )
64
66 """
67 This function is used to parse fields made available to actions that allow
68 for TAL expressions.
69 """
70 sourceStr = source
71 context = kwargs.get('here', {})
72 context.update(kwargs)
73 return talEval(sourceStr, context, kwargs)
74
75
76 -def _signalToContextDict(signal, zopeurl, notification=None, guidManager=None):
77 summary = signal.event
78
79 if signal.clear:
80
81
82 if summary.status == zep_pb2.STATUS_AGED:
83 occur = signal.clear_event.occurrence.add()
84 occur.summary = "Event aging task aged out the event."
85 summary.cleared_by_event_uuid = "Event aging task"
86 elif summary.status == zep_pb2.STATUS_CLOSED:
87 occur = signal.clear_event.occurrence.add()
88 occur.summary = "User '" + summary.current_user_name + "' closed the event in the Zenoss event console."
89 summary.cleared_by_event_uuid = "User action"
90 data = NotificationEventContextWrapper(summary, signal.clear_event)
91 else:
92 data = NotificationEventContextWrapper(summary)
93
94
95 data['urls']['eventUrl'] = getEventUrl(zopeurl, summary.uuid)
96 data['urls']['ackUrl'] = getAckUrl(zopeurl, summary.uuid)
97 data['urls']['closeUrl'] = getCloseUrl(zopeurl, summary.uuid)
98 proxy = EventSummaryProxy(summary)
99 data['urls']['deviceUrl'] = _getBaseDeviceUrl(zopeurl, proxy.DeviceClass, proxy.device)
100 data['urls']['eventsUrl'] = getEventsUrl(zopeurl, proxy.DeviceClass, proxy.device)
101 data['urls']['reopenUrl'] = getReopenUrl(zopeurl, summary.uuid)
102 data['urls']['baseUrl'] = zopeurl
103
104
105 for key, processor in getUtilitiesFor(IProcessSignal):
106 data[key] = processor.process(signal)
107
108
109 for key, contextProvider in getUtilitiesFor(INotificationContextProvider):
110 contextProvider.updateContext(signal, data)
111
112
113 if notification:
114 data['notification']['name'] = notification.titleOrId()
115 if guidManager:
116 trigger = guidManager.getObject(signal.trigger_uuid)
117 if trigger:
118 data['trigger']['name'] = trigger.titleOrId()
119
120 return data
121
122
127
128
131
132
134 """
135 Builds the URL for a device.
136 Example: "http://.../Devices/Server/Linux/devices/localhost/devicedetail"
137 """
138 return '%s/Devices%s/devices/%s/devicedetail' % (_getBaseUrl(zopeurl), device_class, device_name)
139
140
143
144
145 -def getEventsUrl(zopeurl, device_class=None, device_name=None):
146 if device_class and device_name:
147
148 return "%s#deviceDetailNav:device_events" % _getBaseDeviceUrl(zopeurl, device_class, device_name)
149 else:
150
151 return "%s/viewEvents" % _getBaseUrl(zopeurl)
152
153
155 return "%s/manage_ackEvents?evids=%s&zenScreenName=viewEvents" %\
156 (_getBaseEventUrl(zopeurl), evid)
157
158
160 return "%s/manage_deleteEvents?evids=%s&zenScreenName=viewHistoryEvents" %\
161 (_getBaseEventUrl(zopeurl), evid)
162
163
165 return "%s/manage_undeleteEvents?evids=%s&zenScreenName=viewEvents" %\
166 (_getBaseEventUrl(zopeurl), evid)
167
168
170 """
171 Mixin class for provided some common, necessary, methods.
172 """
173
176
179
180 - def generateJavascriptContent(self, notification):
181 content = self.getInfo(notification)
182 return IFormBuilder(content).render(fieldsets=False)
183
186
187
189
190 shouldExecuteInBatch = False
191
193 """
194 Some actions need to configure themselves with properties from the dmd.
195 This is their opportunity to do so.
196 """
197 pass
198
200 targets = set()
201 for recipient in notification.recipients:
202 if recipient['type'] in ['group', 'user']:
203 guid = recipient['value']
204 target_obj = self.guidManager.getObject(guid)
205 if target_obj:
206 for target in self.getActionableTargets(target_obj):
207 targets.add(target)
208 else:
209 targets.add(recipient['value'])
210 return targets
211
230
231
233 raise NotImplemented()
234
235
236 - def execute(self, notification, signal):
237 self.setupAction(notification.dmd)
238
239 exceptionTargets = []
240 targets = self.getTargets(notification)
241 if self.shouldExecuteInBatch:
242 try:
243 log.debug("Executing batch action for targets.")
244 self.executeBatch(notification, signal, targets)
245 except Exception, e:
246 self.handleExecuteError(e, notification, targets)
247 exceptionTargets.extend(targets)
248 else:
249 log.debug("Executing action serially for targets.")
250 for target in targets:
251 try:
252 self.executeOnTarget(notification, signal, target)
253 log.debug('Done executing action for target: %s' % target)
254 except Exception, e:
255 self.handleExecuteError(e, notification, target)
256 exceptionTargets.append(target)
257
258 if exceptionTargets:
259 raise TargetableActionException(self, notification, exceptionTargets)
260
262 implements(IAction)
263 id = 'email'
264 name = 'Email'
265 actionContentInfo = IEmailActionContentInfo
266
267 shouldExecuteInBatch = True
268
271
279
282
284 log.debug("Executing %s action for targets: %s", self.name, targets)
285 self.setupAction(notification.dmd)
286
287 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
288 if signal.clear:
289 log.debug('This is a clearing signal.')
290 subject = processTalSource(notification.content['clear_subject_format'], **data)
291 body = processTalSource(notification.content['clear_body_format'], **data)
292 else:
293 subject = processTalSource(notification.content['subject_format'], **data)
294 body = processTalSource(notification.content['body_format'], **data)
295
296 log.debug('Sending this subject: %s' % subject)
297 log.debug('Sending this body: %s' % body)
298
299 plain_body = MIMEText(self._stripTags(body))
300 email_message = plain_body
301
302 if notification.content['body_content_type'] == 'html':
303 email_message = MIMEMultipart('related')
304 email_message_alternative = MIMEMultipart('alternative')
305 email_message_alternative.attach(plain_body)
306
307 html_body = MIMEText(body.replace('\n', '<br />\n'))
308 html_body.set_type('text/html')
309 email_message_alternative.attach(html_body)
310
311 email_message.attach(email_message_alternative)
312
313 host = notification.content['host']
314 port = notification.content['port']
315 user = notification.content['user']
316 password = notification.content['password']
317 useTls = notification.content['useTls']
318 email_from = notification.content['email_from']
319
320 email_message['Subject'] = subject
321 email_message['From'] = email_from
322 email_message['To'] = ','.join(targets)
323 email_message['Date'] = formatdate(None, True)
324
325 result, errorMsg = sendEmail(
326 email_message,
327 host, port,
328 useTls,
329 user, password
330 )
331
332 if result:
333 log.debug("Notification '%s' sent emails to: %s",
334 notification.id, targets)
335 else:
336 raise ActionExecutionException(
337 "Notification '%s' FAILED to send emails to %s: %s" %
338 (notification.id, targets, errorMsg)
339 )
340
349
363
364 - def updateContent(self, content=None, data=None):
365 updates = dict()
366 updates['body_content_type'] = data.get('body_content_type', 'html')
367
368 properties = ['subject_format', 'body_format', 'clear_subject_format', 'clear_body_format']
369 properties.extend(['host', 'port', 'user', 'password', 'useTls', 'email_from'])
370 for k in properties:
371 updates[k] = data.get(k)
372
373 content.update(updates)
374
375
376 -class PageAction(IActionBase, TargetableAction):
377 implements(IAction)
378
379 id = 'page'
380 name = 'Page'
381 actionContentInfo = IPageActionContentInfo
382
383 - def __init__(self):
384 super(PageAction, self).__init__()
385
386 - def setupAction(self, dmd):
387 self.guidManager = GUIDManager(dmd)
388 self.page_command = dmd.pageCommand
389
390 - def executeOnTarget(self, notification, signal, target):
391 """
392 @TODO: handle the deferred parameter on the sendPage call.
393 """
394 log.debug('Executing action: Page')
395
396 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
397 if signal.clear:
398 log.debug('This is a clearing signal.')
399 subject = processTalSource(notification.content['clear_subject_format'], **data)
400 else:
401 subject = processTalSource(notification.content['subject_format'], **data)
402
403 success, errorMsg = Utils.sendPage(
404 target, subject, self.page_command,
405
406 deferred=False)
407
408 if success:
409 log.debug("Notification '%s' sent page to %s." % (notification, target))
410 else:
411 raise ActionExecutionException(
412 "Notification '%s' failed to send page to %s. (%s)" % (notification, target, errorMsg))
413
414 - def getActionableTargets(self, target):
415 """
416 @param target: This is an object that implements the IProvidesPagerAddresses
417 interface.
418 @type target: UserSettings or GroupSettings.
419 """
420 if IProvidesPagerAddresses.providedBy(target):
421 return target.getPagerAddresses()
422
423 - def updateContent(self, content=None, data=None):
424 updates = dict()
425
426 properties = ['subject_format', 'clear_subject_format', ]
427 for k in properties:
428 updates[k] = data.get(k)
429
430 content.update(updates)
431
432
435 self.cmd = cmd
436 self.data = ''
437 self.error = ''
438
443
445 log.debug("Command finished: '%s'" % reason.getErrorMessage())
446
447
448
449
450
451
452
453
454
455
456
457
458
459
462
465
466
468 implements(IAction)
469
470 id = 'command'
471 name = 'Command'
472 actionContentInfo = ICommandActionContentInfo
473
474 shouldExecuteInBatch = False
475
480
484
485 - def execute(self, notification, signal):
491
493 log.debug('Executing command action: %s on %s', self.name, target)
494 user_env_format = notification.content.get('user_env_format', '')
495 environ ={}
496 env = dict( envvar.split('=') for envvar in user_env_format.split(';') if '=' in envvar)
497 environ['env'] = env
498 environ['user'] = getattr(self.dmd.ZenUsers, target, None)
499 self._execute(notification, signal, environ)
500
501 - def _execute(self, notification, signal, extra_env= {}):
502 self.setupAction(notification.dmd)
503 log.debug('Executing command action: %s', self.name)
504
505 if signal.clear:
506 command = notification.content['clear_body_format']
507 else:
508 command = notification.content['body_format']
509
510 log.debug('Executing this command: %s', command)
511
512 actor = signal.event.occurrence[0].actor
513 device = None
514 if actor.element_uuid:
515 device = self.guidManager.getObject(actor.element_uuid)
516
517 component = None
518 if actor.element_sub_uuid:
519 component = self.guidManager.getObject(actor.element_sub_uuid)
520
521 environ = {'dev': device, 'component': component, 'dmd': notification.dmd, 'env':None}
522 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
523 environ.update(data)
524
525 if environ.get('evt', None):
526 environ['evt'] = self._escapeEvent(environ['evt'])
527
528 if environ.get('clearEvt', None):
529 environ['clearEvt'] = self._escapeEvent(environ['clearEvt'])
530
531 environ.update(extra_env)
532 try:
533 command = processTalSource(command, **environ)
534 except Exception:
535 raise ActionExecutionException('Unable to perform TALES evaluation on "%s" -- is there an unescaped $?' % command)
536
537 log.debug('Executing this compiled command: "%s"' % command)
538 _protocol = EventCommandProtocol(command)
539
540 log.debug('Queueing up command action process.')
541 self.processQueue.queueProcess(
542 '/bin/sh',
543 ('/bin/sh', '-c', command),
544 env=environ['env'],
545 processProtocol=_protocol,
546 timeout=int(notification.content['action_timeout']),
547 timeout_callback=_protocol.timedOut
548 )
549
555
556 - def updateContent(self, content=None, data=None):
557 updates = dict()
558
559 properties = ['body_format', 'clear_body_format', 'action_timeout', 'user_env_format']
560 for k in properties:
561 updates[k] = data.get(k)
562
563 content.update(updates)
564
574
576 """
577 Wraps the message in quotes, escaping any existing quote.
578
579 Before: How do you pronounce "Zenoss"?
580 After: "How do you pronounce \"Zenoss\"?"
581 """
582 QUOTE = '"'
583 BACKSLASH = '\\'
584 return ''.join((QUOTE, msg.replace(QUOTE, BACKSLASH + QUOTE), QUOTE))
585
587 implements(IAction)
588
589 id = 'trap'
590 name = 'SNMP Trap'
591 actionContentInfo = ISnmpTrapActionContentInfo
592
593 _sessions = {}
594
597
598 - def execute(self, notification, signal):
609
610 - def _sendTrap(self, notification, data, event):
611 actor = getattr(event, "actor", None)
612 details = event.details
613 baseOID = '1.3.6.1.4.1.14296.1.100'
614
615 fields = {
616 'uuid' : ( 1, event),
617 'fingerprint' : ( 2, event),
618 'element_identifier' : ( 3, actor),
619 'element_sub_identifier' : ( 4, actor),
620 'event_class' : ( 5, event),
621 'event_key' : ( 6, event),
622 'summary' : ( 7, event),
623 'message' : ( 8, event),
624 'severity' : ( 9, event),
625 'status' : (10, event),
626 'event_class_key' : (11, event),
627 'event_group' : (12, event),
628 'state_change_time' : (13, event),
629 'first_seen_time' : (14, event),
630 'last_seen_time' : (15, event),
631 'count' : (16, event),
632 'zenoss.device.production_state':(17, details),
633 'agent': (20, event),
634 'zenoss.device.device_class': (21, details),
635 'zenoss.device.location' : (22, details),
636 'zenoss.device.systems' : (23, details),
637 'zenoss.device.groups' : (24, details),
638 'zenoss.device.ip_address': (25, details),
639 'syslog_facility' : (26, event),
640 'syslog_priority' : (27, event),
641 'nt_event_code' : (28, event),
642 'current_user_name' : (29, event),
643 'cleared_by_event_uuid' : (31, event),
644 'zenoss.device.priority' : (32, details),
645 'event_class_mapping_uuid': (33, event)
646 }
647
648 eventDict = self.createEventDict(fields, event)
649 self.processEventDict(eventDict, data, notification.dmd)
650 varbinds = self.makeVarBinds(baseOID, fields, eventDict)
651
652 session = self._getSession(notification.content)
653
654 for v in varbinds:
655 log.debug(v)
656 session.sendTrap(baseOID + '.0.0.1', varbinds=varbinds)
657
659 """
660 Create an event dictionary suitable for Python evaluation.
661 """
662 eventDict = {}
663 for field, oidspec in fields.items():
664 i, source = oidspec
665 if source is event.details:
666 val = source.get(field, '')
667 else:
668 val = getattr(source, field, '')
669 eventDict[field] = val
670 return eventDict
671
673 """
674 Integration hook
675 """
676 pass
677
679 """
680 Make the SNMP variable bindings in numeric order.
681 """
682 intValues = (9, 10, 26, 27)
683 varbinds = []
684 for field, oidspec in sorted(fields.items(), key=lambda x: x[1][0]):
685 i, source = oidspec
686 val = eventDict.get(field, '')
687 if isinstance(val, (list, tuple, set)):
688 val = '|'.join(val)
689
690
691 oid = "%s.%d" % (baseOID, i)
692 oidType = 's' if i not in intValues else 'i'
693
694 val = str(val)
695
696 varbinds.append( (oid, oidType, val) )
697 return varbinds
698
699 - def updateContent(self, content=None, data=None):
700 content['action_destination'] = data.get('action_destination')
701 content['community'] = data.get('community')
702 content['version'] = data.get('version')
703 content['port'] = int(data.get('port'))
704
706 traphost = content['action_destination']
707 port = content.get('port', 162)
708 destination = '%s:%s' % (traphost, port)
709
710 if not traphost or port <= 0:
711 log.error("%s: SNMP trap host information %s is incorrect ", destination)
712 return None
713
714 community = content.get('community', 'public')
715 version = content.get('version', 'v2c')
716
717 session = self._sessions.get(destination, None)
718 if session is None:
719 log.debug("Creating SNMP trap session to %s", destination)
720
721
722 try:
723 getaddrinfo(traphost, port)
724 except Exception:
725 raise ActionExecutionException("The destination %s is not resolvable." % destination)
726
727 session = netsnmp.Session((
728 '-%s' % version,
729 '-c', community,
730 destination)
731 )
732 session.open()
733 self._sessions[destination] = session
734
735 return session
736