1
2
3
4
5
6
7
8
9
10
11
12
13
14 import re
15 from traceback import format_exc
16 from zope.interface import implements
17 from zope.component import getUtilitiesFor
18
19 from pynetsnmp import netsnmp
20
21 from twisted.internet.protocol import ProcessProtocol
22
23 from email.MIMEText import MIMEText
24 from email.MIMEMultipart import MIMEMultipart
25 from email.Utils import formatdate
26 from Products.ZenEvents.events2.proxy import EventSummaryProxy
27
28 from Products.Zuul.interfaces.actions import IEmailActionContentInfo, IPageActionContentInfo, ICommandActionContentInfo, ISnmpTrapActionContentInfo
29 from Products.Zuul.form.interfaces import IFormBuilder
30
31 from Products.ZenModel.interfaces import IAction, IProvidesEmailAddresses, IProvidesPagerAddresses, IProcessSignal
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 data = NotificationEventContextWrapper(summary, signal.clear_event)
81 else:
82 data = NotificationEventContextWrapper(summary)
83
84
85 data['urls']['eventUrl'] = getEventUrl(zopeurl, summary.uuid)
86 data['urls']['ackUrl'] = getAckUrl(zopeurl, summary.uuid)
87 data['urls']['closeUrl'] = getCloseUrl(zopeurl, summary.uuid)
88 proxy = EventSummaryProxy(summary)
89 data['urls']['deviceUrl'] = _getBaseDeviceUrl(zopeurl, proxy.DeviceClass, proxy.device)
90 data['urls']['eventsUrl'] = getEventsUrl(zopeurl, proxy.DeviceClass, proxy.device)
91 data['urls']['reopenUrl'] = getReopenUrl(zopeurl, summary.uuid)
92 data['urls']['baseUrl'] = zopeurl
93
94
95 for key, processor in getUtilitiesFor(IProcessSignal):
96 data[key] = processor.process(signal)
97
98
99 if notification:
100 data['notification']['name'] = notification.titleOrId()
101 if guidManager:
102 trigger = guidManager.getObject(signal.trigger_uuid)
103 if trigger:
104 data['trigger']['name'] = trigger.titleOrId()
105
106 return data
107
108
113
114
117
118
120 """
121 Builds the URL for a device.
122 Example: "http://.../Devices/Server/Linux/devices/localhost/devicedetail"
123 """
124 return '%s/Devices%s/devices/%s/devicedetail' % (_getBaseUrl(zopeurl), device_class, device_name)
125
126
129
130
131 -def getEventsUrl(zopeurl, device_class=None, device_name=None):
132 if device_class and device_name:
133
134 return "%s#deviceDetailNav:device_events" % _getBaseDeviceUrl(zopeurl, device_class, device_name)
135 else:
136
137 return "%s/viewEvents" % _getBaseUrl(zopeurl)
138
139
141 return "%s/manage_ackEvents?evids=%s&zenScreenName=viewEvents" %\
142 (_getBaseEventUrl(zopeurl), evid)
143
144
146 return "%s/manage_deleteEvents?evids=%s&zenScreenName=viewHistoryEvents" %\
147 (_getBaseEventUrl(zopeurl), evid)
148
149
151 return "%s/manage_undeleteEvents?evids=%s&zenScreenName=viewEvents" %\
152 (_getBaseEventUrl(zopeurl), evid)
153
154
156 """
157 Mixin class for provided some common, necessary, methods.
158 """
159
162
165
166 - def generateJavascriptContent(self, notification):
167 content = self.getInfo(notification)
168 return IFormBuilder(content).render(fieldsets=False)
169
170
173 """
174 Some actions need to configure themselves with properties from the dmd.
175 This is their opportunity to do so.
176 """
177 pass
178
190
191 - def execute(self, notification, signal):
192 self.setupAction(notification.dmd)
193
194 exceptionTargets = []
195 for target in self.getTargets(notification):
196 try:
197 self.executeOnTarget(notification, signal, target)
198 log.debug('Done executing action for target: %s' % target)
199 except Exception, e:
200
201
202
203
204 msg = 'Error executing action {notification} on {target}'.format(
205 notification=notification.id,
206 target=target,
207 )
208 log.error(e)
209 log.error(msg)
210 traceback = format_exc()
211 event = Event(device="localhost",
212 eventClass="/App/Failed",
213 summary=msg,
214 message=traceback,
215 severity=SEV_WARNING, component="zenactiond")
216 notification.dmd.ZenEventManager.sendEvent(event)
217 exceptionTargets.append(target)
218
219 if exceptionTargets:
220 raise TargetableActionException(self, notification, exceptionTargets)
221
223 implements(IAction)
224 id = 'email'
225 name = 'Email'
226 actionContentInfo = IEmailActionContentInfo
227
230
239
241 log.debug('Executing action: Email')
242
243 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
244 if signal.clear:
245 log.debug('This is a clearing signal.')
246 subject = processTalSource(notification.content['clear_subject_format'], **data)
247 body = processTalSource(notification.content['clear_body_format'], **data)
248 else:
249 subject = processTalSource(notification.content['subject_format'], **data)
250 body = processTalSource(notification.content['body_format'], **data)
251
252 log.debug('Sending this subject: %s' % subject)
253 log.debug('Sending this body: %s' % body)
254
255 plain_body = MIMEText(self._stripTags(body))
256 email_message = plain_body
257
258 if notification.content['body_content_type'] == 'html':
259 email_message = MIMEMultipart('related')
260 email_message_alternative = MIMEMultipart('alternative')
261 email_message_alternative.attach(plain_body)
262
263 html_body = MIMEText(body.replace('\n', '<br />\n'))
264 html_body.set_type('text/html')
265 email_message_alternative.attach(html_body)
266
267 email_message.attach(email_message_alternative)
268
269 email_message['Subject'] = subject
270 email_message['From'] = self.email_from
271 email_message['To'] = target
272 email_message['Date'] = formatdate(None, True)
273
274 result, errorMsg = Utils.sendEmail(
275 email_message,
276 self.host,
277 self.port,
278 self.useTls,
279 self.user,
280 self.password
281 )
282
283 if result:
284 log.debug("Notification '%s' sent email to: %s",
285 notification.id, target)
286 else:
287 raise ActionExecutionException(
288 "Notification '%s' failed to send email to %s: %s" %
289 (notification.id, target, errorMsg)
290 )
291
300
314
315 - def updateContent(self, content=None, data=None):
316 updates = dict()
317 updates['body_content_type'] = data.get('body_content_type', 'html')
318
319 properties = ['subject_format', 'body_format', 'clear_subject_format', 'clear_body_format', ]
320 for k in properties:
321 updates[k] = data.get(k)
322
323 content.update(updates)
324
325
326 -class PageAction(IActionBase, TargetableAction):
327 implements(IAction)
328
329 id = 'page'
330 name = 'Page'
331 actionContentInfo = IPageActionContentInfo
332
333 - def __init__(self):
334 super(PageAction, self).__init__()
335
336 - def setupAction(self, dmd):
337 self.guidManager = GUIDManager(dmd)
338 self.page_command = dmd.pageCommand
339
340 - def executeOnTarget(self, notification, signal, target):
341 """
342 @TODO: handle the deferred parameter on the sendPage call.
343 """
344 log.debug('Executing action: Page')
345
346 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
347 if signal.clear:
348 log.debug('This is a clearing signal.')
349 subject = processTalSource(notification.content['clear_subject_format'], **data)
350 else:
351 subject = processTalSource(notification.content['subject_format'], **data)
352
353 success, errorMsg = Utils.sendPage(
354 target, subject, self.page_command,
355
356 deferred=False)
357
358 if success:
359 log.debug("Notification '%s' sent page to %s." % (notification, target))
360 else:
361 raise ActionExecutionException(
362 "Notification '%s' failed to send page to %s. (%s)" % (notification, target, errorMsg))
363
364 - def getActionableTargets(self, target):
365 """
366 @param target: This is an object that implements the IProvidesPagerAddresses
367 interface.
368 @type target: UserSettings or GroupSettings.
369 """
370 if IProvidesPagerAddresses.providedBy(target):
371 return target.getPagerAddresses()
372
373 - def updateContent(self, content=None, data=None):
374 updates = dict()
375
376 properties = ['subject_format', 'clear_subject_format', ]
377 for k in properties:
378 updates[k] = data.get(k)
379
380 content.update(updates)
381
382
385 self.cmd = cmd
386 self.data = ''
387 self.error = ''
388
393
395 log.debug("Command finished: '%s'" % reason.getErrorMessage())
396 code = 1
397 try:
398 code = reason.value.exitCode
399 except AttributeError:
400 pass
401
402 if code == 0:
403 cmdData = self.data or "<command produced no output>"
404
405 else:
406 cmdError = self.error or "<command produced no output>"
407
408
411
414
415
417 implements(IAction)
418
419 id = 'command'
420 name = 'Command'
421 actionContentInfo = ICommandActionContentInfo
422
427
430
431 - def execute(self, notification, signal):
432 self.setupAction(notification.dmd)
433
434 log.debug('Executing action: Command')
435
436 if signal.clear:
437 command = notification.content['clear_body_format']
438 else:
439 command = notification.content['body_format']
440
441 log.debug('Executing this command: %s' % command)
442
443 actor = signal.event.occurrence[0].actor
444 device = None
445 if actor.element_uuid:
446 device = self.guidManager.getObject(actor.element_uuid)
447
448 component = None
449 if actor.element_sub_uuid:
450 component = self.guidManager.getObject(actor.element_sub_uuid)
451
452 environ = {'dev': device, 'component': component, 'dmd': notification.dmd}
453 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
454 environ.update(data)
455
456 if environ.get('evt', None):
457 environ['evt'] = self._escapeEvent(environ['evt'])
458
459 if environ.get('clearEvt', None):
460 environ['clearEvt'] = self._escapeEvent(environ['clearEvt'])
461
462 command = processTalSource(command, **environ)
463 log.debug('Executing this compiled command: "%s"' % command)
464
465 _protocol = EventCommandProtocol(command)
466
467 log.debug('Queueing up command action process.')
468 self.processQueue.queueProcess(
469 '/bin/sh',
470 ('/bin/sh', '-c', command),
471 env=None,
472 processProtocol=_protocol,
473 timeout=int(notification.content['action_timeout']),
474 timeout_callback=_protocol.timedOut
475 )
476
486
488 """
489 Wraps the message in quotes, escaping any existing quote.
490
491 Before: How do you pronounce "Zenoss"?
492 After: "How do you pronounce \"Zenoss\"?"
493 """
494 QUOTE = '"'
495 BACKSLASH = '\\'
496 return ''.join((QUOTE, msg.replace(QUOTE, BACKSLASH + QUOTE), QUOTE))
497
499 """
500 Commands do not act _on_ targets, they are only executed.
501 """
502 pass
503
504 - def updateContent(self, content=None, data=None):
505 updates = dict()
506
507 properties = ['body_format', 'clear_body_format', 'action_timeout']
508 for k in properties:
509 updates[k] = data.get(k)
510
511 content.update(updates)
512
513
515 implements(IAction)
516
517 id = 'trap'
518 name = 'SNMP Trap'
519 actionContentInfo = ISnmpTrapActionContentInfo
520
521 _sessions = {}
522
524 if destination not in self._sessions:
525 log.debug("Creating SNMP trap session to %s", destination)
526 self._sessions[destination] = netsnmp.Session((
527 '-v2c', '-c', 'public', '%s:162' % destination))
528 self._sessions[destination].open()
529
530 return self._sessions.get(destination)
531
534
535 - def execute(self, notification, signal):
536 """
537 Send out an SNMP trap according to the definition in ZENOSS-MIB.
538 """
539 log.debug('Processing SNMP Trap action.')
540 self.setupAction(notification.dmd)
541
542 data = _signalToContextDict(signal, self.options.get('zopeurl'), notification, self.guidManager)
543 event = data['eventSummary']
544 actor = event.actor
545 details = event.details
546
547 baseOID = '1.3.6.1.4.1.14296.1.100'
548
549 fields = {
550 'uuid' : ( 1, event),
551 'fingerprint' : ( 2, event),
552 'element_identifier' : ( 3, actor),
553 'element_sub_identifier' : ( 4, actor),
554 'event_class' : ( 5, event),
555 'event_key' : ( 6, event),
556 'summary' : ( 7, event),
557 'severity' : ( 9, event),
558 'status' : (10, event),
559 'event_class_key' : (11, event),
560 'event_group' : (12, event),
561 'state_change_time' : (13, event),
562 'first_seen_time' : (14, event),
563 'last_seen_time' : (15, event),
564 'count' : (16, event),
565 'zenoss.device.production_state':(17, details),
566 'agent': (20, event),
567 'zenoss.device.device_class': (21, details),
568 'zenoss.device.location' : (22, details),
569 'zenoss.device.systems' : (23, details),
570 'zenoss.device.groups' : (24, details),
571 'zenoss.device.ip_address': (25, details),
572 'syslog_facility' : (26, event),
573 'syslog_priority' : (27, event),
574 'nt_event_code' : (28, event),
575 'current_user_name' : (29, event),
576 'cleared_by_event_uuid' : (31, event),
577 'zenoss.device.priority' : (32, details),
578 'event_class_mapping_uuid': (33, event)
579 }
580
581 varbinds = []
582
583 for field, oidspec in sorted(fields.items(), key=lambda x: x[1][0]):
584 i, source = oidspec
585 if source == event.details:
586 val = source.get(field, '')
587 else:
588 val = getattr(source, field, '')
589 if isinstance(val, (list, tuple, set)):
590 val = '|'.join(val)
591 varbinds.append(("%s.%d.0" % (baseOID,i), 's', str(val)))
592
593 self._getSession(notification.content['action_destination']).sendTrap(
594 baseOID + '.0.0.1', varbinds=varbinds)
595
596 - def updateContent(self, content=None, data=None):
597 content['action_destination'] = data.get('action_destination')
598