Package Products :: Package ZenModel :: Module actions
[hide private]
[frames] | no frames]

Source Code for Module Products.ZenModel.actions

  1  ########################################################################### 
  2  # 
  3  # This program is part of Zenoss Core, an open source monitoring platform. 
  4  # Copyright (C) 2007, Zenoss Inc. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify it 
  7  # under the terms of the GNU General Public License version 2 or (at your 
  8  # option) any later version as published by the Free Software Foundation. 
  9  # 
 10  # For complete information please visit: http://www.zenoss.com/oss/ 
 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   
47 -class ActionExecutionException(Exception): pass
48 49
50 -class ActionMissingException(Exception): pass
51 52
53 -class TargetableActionException(ActionExecutionException):
54 - def __init__(self, action, notification, exceptionTargets):
55 self.action = action 56 self.notificationId = notification.id 57 self.exceptionTargets = exceptionTargets
58 - def __str__(self):
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
65 -def processTalSource(source, **kwargs):
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 # build basic event context wrapper for notifications 79 if signal.clear: 80 data = NotificationEventContextWrapper(summary, signal.clear_event) 81 else: 82 data = NotificationEventContextWrapper(summary) 83 84 # add urls to event context 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 # now process all custom processors that might be registered to enhance 94 # the event context 95 for key, processor in getUtilitiesFor(IProcessSignal): 96 data[key] = processor.process(signal) 97 98 # Add trigger and notification info 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
109 -def _getBaseUrl(zopeurl):
110 if not zopeurl: 111 zopeurl = Utils.getDefaultZopeUrl() 112 return '%s/zport/dmd' % zopeurl
113 114
115 -def _getBaseEventUrl(zopeurl):
116 return '%s/Events' % _getBaseUrl(zopeurl)
117 118
119 -def _getBaseDeviceUrl(zopeurl, device_class, device_name):
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
127 -def getEventUrl(zopeurl, evid):
128 return "%s/viewDetail?evid=%s" % (_getBaseEventUrl(zopeurl), evid)
129 130
131 -def getEventsUrl(zopeurl, device_class=None, device_name=None):
132 if device_class and device_name: 133 # events for a specific device 134 return "%s#deviceDetailNav:device_events" % _getBaseDeviceUrl(zopeurl, device_class, device_name) 135 else: 136 #events on all devices 137 return "%s/viewEvents" % _getBaseUrl(zopeurl)
138 139
140 -def getAckUrl(zopeurl, evid):
141 return "%s/manage_ackEvents?evids=%s&zenScreenName=viewEvents" %\ 142 (_getBaseEventUrl(zopeurl), evid)
143 144
145 -def getCloseUrl(zopeurl, evid):
146 return "%s/manage_deleteEvents?evids=%s&zenScreenName=viewHistoryEvents" %\ 147 (_getBaseEventUrl(zopeurl), evid)
148 149
150 -def getReopenUrl(zopeurl, evid):
151 return "%s/manage_undeleteEvents?evids=%s&zenScreenName=viewEvents" %\ 152 (_getBaseEventUrl(zopeurl), evid)
153 154
155 -class IActionBase(object):
156 """ 157 Mixin class for provided some common, necessary, methods. 158 """ 159
160 - def configure(self, options):
161 self.options = options
162
163 - def getInfo(self, notification):
164 return self.actionContentInfo(notification)
165
166 - def generateJavascriptContent(self, notification):
167 content = self.getInfo(notification) 168 return IFormBuilder(content).render(fieldsets=False)
169 170
171 -class TargetableAction(object):
172 - def setupAction(self, dmd):
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
179 - def getTargets(self, notification):
180 targets = set() 181 for recipient in notification.recipients: 182 if recipient['type'] in ['group', 'user']: 183 guid = recipient['value'] 184 target_obj = self.guidManager.getObject(guid) 185 for target in self.getActionableTargets(target_obj): 186 targets.add(target) 187 else: 188 targets.add(recipient['value']) 189 return targets
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 # If there is an error executing this action on a target, 201 # we need to handle it, but we don't want to prevent other 202 # actions from executing on any other targets that may be 203 # about to be acted on. 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
222 -class EmailAction(IActionBase, TargetableAction):
223 implements(IAction) 224 id = 'email' 225 name = 'Email' 226 actionContentInfo = IEmailActionContentInfo 227
228 - def __init__(self):
229 super(EmailAction, self).__init__()
230
231 - def setupAction(self, dmd):
232 self.guidManager = GUIDManager(dmd) 233 self.email_from = dmd.getEmailFrom() 234 self.host = dmd.smtpHost 235 self.port = dmd.smtpPort 236 self.useTls = dmd.smtpUseTLS 237 self.user = dmd.smtpUser 238 self.password = dmd.smtpPass
239
240 - def executeOnTarget(self, notification, signal, target):
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
292 - def getActionableTargets(self, target):
293 """ 294 @param target: This is an object that implements the IProvidesEmailAddresses 295 interface. 296 @type target: UserSettings or GroupSettings. 297 """ 298 if IProvidesEmailAddresses.providedBy(target): 299 return target.getEmailAddresses()
300
301 - def _stripTags(self, data):
302 """A quick html => plaintext converter 303 that retains and displays anchor hrefs 304 305 stolen from the old zenactions. 306 @todo: needs to be updated for the new data structure? 307 """ 308 tags = re.compile(r'<(.|\n)+?>', re.I | re.M) 309 aattrs = re.compile(r'<a(.|\n)+?href=["\']([^"\']*)[^>]*?>([^<>]*?)</a>', re.I | re.M) 310 anchors = re.finditer(aattrs, data) 311 for x in anchors: data = data.replace(x.group(), "%s: %s" % (x.groups()[2], x.groups()[1])) 312 data = re.sub(tags, '', data) 313 return data
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 #deferred=self.options.cycle) 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
383 -class EventCommandProtocol(ProcessProtocol):
384 - def __init__(self, cmd):
385 self.cmd = cmd 386 self.data = '' 387 self.error = ''
388
389 - def timedOut(self, value):
390 log.error("Command '%s' timed out" % self.cmd.id) 391 # FIXME: send an event or something? 392 return value
393
394 - def processEnded(self, reason):
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 # FIXME: send an event or something? 405 else: 406 cmdError = self.error or "<command produced no output>"
407 # FIXME: send an event or something? 408
409 - def outReceived(self, text):
410 self.data += text
411
412 - def errReceived(self, text):
413 self.error += text
414 415
416 -class CommandAction(IActionBase):
417 implements(IAction) 418 419 id = 'command' 420 name = 'Command' 421 actionContentInfo = ICommandActionContentInfo 422
423 - def configure(self, options):
424 super(CommandAction, self).configure(options) 425 self.processQueue = ProcessQueue(options.get('maxCommands', 10)) 426 self.processQueue.start()
427
428 - def setupAction(self, dmd):
429 self.guidManager = GUIDManager(dmd)
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
477 - def _escapeEvent(self, evt):
478 """ 479 Escapes the relavent fields of an event context for event commands. 480 """ 481 if evt.message: 482 evt.message = self._wrapInQuotes(evt.message) 483 if evt.summary: 484 evt.summary = self._wrapInQuotes(evt.summary) 485 return evt
486
487 - def _wrapInQuotes(self, msg):
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
498 - def getActionableTargets(self, target):
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
514 -class SNMPTrapAction(IActionBase):
515 implements(IAction) 516 517 id = 'trap' 518 name = 'SNMP Trap' 519 actionContentInfo = ISnmpTrapActionContentInfo 520 521 _sessions = {} 522
523 - def _getSession(self, destination):
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
532 - def setupAction(self, dmd):
533 self.guidManager = GUIDManager(dmd)
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