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