Package Products :: Package ZenEvents :: Module zenactions |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenEvents.zenactions

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2007, 2009 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 as published by
      8  # the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13  #! /usr/bin/env python
     14
     15  __doc__='''zenactions
     16
     17  Turn events into notifications (pages, emails).
     18
     19  '''
     20
     21  import re
     22  import socket
     23  import time
     24  from sets import Set
     25  import Globals
     26
     27  from ZODB.POSException import POSError
     28  from _mysql_exceptions import OperationalError, ProgrammingError
     29
     30  from Products.ZenUtils.ProcessQueue import ProcessQueue
     31  from Products.ZenUtils.ZCmdBase import ZCmdBase
     32  from Products.ZenUtils.ZenTales import talesCompile, getEngine
     33  from Products.ZenEvents.Exceptions import ZenEventNotFound
     34  from ZenEventClasses import App_Start, App_Stop, Status_Heartbeat
     35  from ZenEventClasses import Cmd_Fail
     36  import Event
     37  from Schedule import Schedule
     38  from UpdateCheck import UpdateCheck
     39  from Products.ZenUtils import Utils
     40  from twisted.internet import reactor
     41  from twisted.internet.protocol import ProcessProtocol
     42  from email.Utils import formatdate
     43
     44  DEFAULT_MONITOR = "localhost"
     45
     46  deviceFilterRegex = re.compile("Device\s(.*?)'(.*?)'", re.IGNORECASE)
     47
     48
     49 -def _capitalize(s):
     50      return s[0:1].upper() + s[1:]
     51
     52 -class EventCommandProtocol(ProcessProtocol):
     53
     54 -    def __init__(self, cmd, server):
     55          self.cmd = cmd
     56          self.server = server
     57          self.data = ''
     58          self.error = ''
     59
     60 -    def timedOut(self, value):
     61          self.server.log.error("Command %s timed out" % self.cmd.id)
     62          self.server.sendEvent(Event.Event(
     63              device=self.server.options.monitor,
     64              eventClass=Cmd_Fail,
     65              severity=Event.Error,
     66              component="zenactions",
     67              eventKey=self.cmd.id,
     68              summary="Timeout running %s" % (self.cmd.id,),
     69              ))
     70          return value
     71
     72 -    def processEnded(self, reason):
     73          self.server.log.debug("Command finished: %s" % reason.getErrorMessage())
     74          code = 1
     75          try:
     76              code = reason.value.exitCode
     77          except AttributeError:
     78              pass
     79
     80          if code == 0:
     81              cmdData = self.data or "<command produced no output>"
     82              self.server.log.debug("Command %s says: %s", self.cmd.id, cmdData)
     83              self.server.sendEvent(Event.Event(
     84                  device=self.server.options.monitor,
     85                  eventClass=Cmd_Fail,
     86                  severity=Event.Clear,
     87                  component="zenactions",
     88                  eventKey=self.cmd.id,
     89                  summary="Command succeeded: %s: %s" % (
     90                      self.cmd.id, cmdData),
     91                  ))
     92          else:
     93              cmdError = self.error or "<command produced no output>"
     94              self.server.log.error("Command %s says %s", self.cmd.id, cmdError)
     95              self.server.sendEvent(Event.Event(
     96                  device=self.server.options.monitor,
     97                  eventClass=Cmd_Fail,
     98                  severity=Event.Error,
     99                  component="zenactions",
    100                  eventKey=self.cmd.id,
    101                  summary="Error running: %s: %s" % (
    102                      self.cmd.id, cmdError),
    103                  ))
    104
    105 -    def outReceived(self, text):
    106          self.data += text
    107
    108 -    def errReceived(self, text):
    109          self.error += text
    110
    111
    112 -class BaseZenActions(object):
    113      """
    114      Take actions based on events in the event manager.
    115      Start off by sending emails and pages.
    116      """
    117
    118      lastCommand = None
    119
    120      addstate = ("INSERT INTO alert_state "
    121                  "VALUES ('%s', '%s', '%s', NULL) "
    122                  "ON DUPLICATE KEY UPDATE lastSent = now()")
    123
    124
    125      clearstate = ("DELETE FROM alert_state "
    126                    " WHERE evid='%s' "
    127                    "   AND userid='%s' "
    128                    "   AND rule='%s'")
    129
    130  #FIXME attempt to convert subquery to left join that doesn't work
    131  #    newsel = """select %s, evid from status s left join alert_state a
    132  #                on s.evid=a.evid where a.evid is null and
    133  #                a.userid='%s' and a.rule='%s'"""
    134
    135      newsel = ("SELECT %s, evid FROM status WHERE "
    136                "%s AND evid NOT IN "
    137                " (SELECT evid FROM alert_state "
    138                "  WHERE userid='%s' AND rule='%s' %s)")
    139
    140      clearsel = ("SELECT %s, h.evid FROM history h, alert_state a "
    141                  " WHERE h.evid=a.evid AND a.userid='%s' AND a.rule='%s'")
    142
    143      clearEventSelect = ("SELECT %s "
    144                          "  FROM history clear, history event "
    145                          " WHERE clear.evid = event.clearid "
    146                          "   AND event.evid = '%s'")
    147
    148
    149 -    def loadActionRules(self):
    150          """Load the ActionRules into the system.
    151          """
    152          self.actions = []
    153          for ar in self.dmd.ZenUsers.getAllActionRules():
    154              if not ar.enabled: continue
    155              userid = ar.getUser().id
    156              self.actions.append(ar)
    157              self.log.debug("action:%s for:%s loaded", ar.getId(), userid)
    158
    159
    160 -    def _getCursor(self, stmt, callback):
    161          """
    162          Get a cursor for the ZenEventManager connection.  Execute statement
    163          and call the callback with the cursor and number of row affected.
    164          """
    165          result = None
    166          self.lastCommand = stmt
    167          self.log.debug(stmt)
    168          zem = self.dmd.ZenEventManager
    169          conn = zem.connect()
    170          try:
    171              curs = conn.cursor()
    172              rowsAffected = curs.execute(stmt)
    173              result = callback(cursor=curs, rowsAffected=rowsAffected)
    174          finally:
    175              zem.close(conn)
    176          return result
    177
    178
    179 -    def execute(self, stmt):
    180          """
    181          Execute stmt against ZenEventManager connection and return the number
    182          of rows that were affected.
    183          """
    184          def callback(rowsAffected, **unused):
    185              return rowsAffected
    186          return self._getCursor(stmt, callback)
    187
    188
    189 -    def query(self, stmt):
    190          """
    191          Execute stmt against ZenEventManager connection and fetch all results.
    192          """
    193          def callback(cursor, **unused):
    194              return cursor.fetchall()
    195          return self._getCursor(stmt, callback)
    196
    197
    198 -    def _describe(self, stmt):
    199          """
    200          Execute stmt against ZenEventManager connection and return the cursor
    201          description.
    202          """
    203          def callback(cursor, **unused):
    204              return cursor.description
    205          return self._getCursor(stmt, callback)
    206
    207
    208 -    def _columnNames(self, table):
    209          """
    210          Returns the column names for the table using a ZenEventManager
    211          connection.
    212          """
    213          description = self._describe("SELECT * FROM %s LIMIT 0" % table)
    214          return [d[0] for d in description]
    215
    216
    217 -    def getBaseUrl(self, device=None):
    218          url = self.options.zopeurl
    219          if device:
    220              return "%s%s" % (url, device.getPrimaryUrlPath())
    221          else:
    222              return "%s/zport/dmd/Events" % (url)
    223
    224
    225 -    def getEventUrl(self, evid, device=None):
    226          return "%s/viewDetail?evid=%s" % (self.getBaseUrl(device), evid)
    227
    228
    229 -    def getEventsUrl(self, device=None):
    230          return "%s/viewEvents" % self.getBaseUrl(device)
    231
    232
    233 -    def getAckUrl(self, evid, device=None):
    234          return "%s/manage_ackEvents?evids=%s&zenScreenName=viewEvents" % (
    235              self.getBaseUrl(device), evid)
    236
    237
    238 -    def getDeleteUrl(self, evid, device=None):
    239          return "%s/manage_deleteEvents?evids=%s" % (
    240              self.getBaseUrl(device), evid) + \
    241              "&zenScreenName=viewHistoryEvents"
    242
    243
    244 -    def getUndeleteUrl(self, evid, device=None):
    245          return "%s/manage_undeleteEvents?evids=%s" % (
    246              self.getBaseUrl(device), evid) + \
    247              "&zenScreenName=viewEvents"
    248
    249
    250 -    def processRules(self, zem):
    251          """Run through all rules matching them against events.
    252          """
    253          for ar in self.actions:
    254              try:
    255                  self.lastCommand = None
    256                  # call sendPage or sendEmail
    257                  actfunc = getattr(self, "send"+ar.action.title())
    258                  self.processEvent(zem, ar, actfunc)
    259              except (SystemExit, KeyboardInterrupt, OperationalError, POSError):
    260                  raise
    261              except:
    262                  if self.lastCommand:
    263                      self.log.warning(self.lastCommand)
    264                  self.log.exception("action:%s",ar.getId())
    265
    266 -    def checkVersion(self, zem):
    267          self.updateCheck.check(self.dmd, zem)
    268          import transaction
    269          transaction.commit()
    270
    271 -    def filterDeviceName(self, zem, whereClause):
    272          """This is somewhat janky but we only store the device id in the mysql
    273          database but allow people to search based on the device "title".
    274          This method resolves the disparity by searching the catalog first and using
    275          those results.
    276          """
    277          # they are not searching based on device at all
    278          if not 'device' in whereClause:
    279              return whereClause
    280          matches = deviceFilterRegex.findall(whereClause)
    281
    282          # incase our awesome regex fails
    283          if not matches:
    284              return whereClause
    285
    286          # get the devices from the event manager
    287          deviceids = []
    288          include = 'IN'
    289
    290          # should be of the form "LIKE '%bar%'" or "NOT LIKE '%bar%'"
    291          for match in matches:
    292              operator = match[0]
    293              searchTerm = match[-1]
    294              originalDeviceFilter = operator + "'" + searchTerm + "'"
    295
    296              # take care of begins with and ends with
    297              if searchTerm.startswith('%'):
    298                  searchTerm = '.*' + searchTerm
    299              if searchTerm.endswith('%'):
    300                  searchTerm = searchTerm + '.*'
    301
    302              # search the catalog
    303              deviceids = zem._getDeviceIdsMatching(searchTerm.replace("%", ""), globSearch=False)
    304
    305              # if we didn't find anything in the catalog just search the mysql
    306              if not deviceids:
    307                  continue
    308
    309              if not operator.lower().strip() in ('like', '='):
    310                  include = 'NOT IN'
    311              deviceFilter = " %s ('%s') " % (include, "','".join(deviceids))
    312              whereClause = whereClause.replace(originalDeviceFilter, deviceFilter)
    313
    314          return whereClause
    315
    316
    317 -    def processEvent(self, zem, context, action):
    318          userFields = context.getEventFields()
    319          columnNames = self._columnNames('status')
    320          fields = [f for f in userFields if f in columnNames]
    321          userid = context.getUserid()
    322          # get new events
    323          nwhere = context.where.strip() or '1 = 1'
    324          nwhere = self.filterDeviceName(zem, nwhere)
    325          if context.delay > 0:
    326              nwhere += " and firstTime + %s < UNIX_TIMESTAMP()" % context.delay
    327          awhere = ''
    328          if context.repeatTime:
    329              awhere += ' and DATE_ADD(lastSent, INTERVAL %d SECOND) > now() ' % (
    330                  context.repeatTime,)
    331          q = self.newsel % (",".join(fields), nwhere, userid, context.getId(),
    332                             awhere)
    333          for result in self.query(q):
    334              evid = result[-1]
    335              data = dict(zip(fields, map(zem.convert, fields, result[:-1])))
    336
    337              # Make details available to event commands.  zem.getEventDetail
    338              # uses the status table (which is where this event came from
    339              try:
    340                  details = dict( zem.getEventDetail(evid).getEventDetails() )
    341                  data.update( details )
    342              except ZenEventNotFound:
    343                  pass
    344
    345              device = self.dmd.Devices.findDevice(data.get('device', None))
    346              data['eventUrl'] = self.getEventUrl(evid, device)
    347              if device:
    348                  data['eventsUrl'] = self.getEventsUrl(device)
    349              else:
    350                  data['eventsUrl'] = 'n/a'
    351                  data['device'] = data.get('device', None) or ''
    352              data['ackUrl'] = self.getAckUrl(evid, device)
    353              data['deleteUrl'] = self.getDeleteUrl(evid, device)
    354              severity = data.get('severity', -1)
    355              data['severityString'] = zem.getSeverityString(severity)
    356              if action(context, data, False):
    357                  addcmd = self.addstate % (evid, userid, context.getId())
    358                  self.execute(addcmd)
    359
    360          # get clear events
    361          historyFields = [("h.%s" % f) for f in fields]
    362          historyFields = ','.join(historyFields)
    363          q = self.clearsel % (historyFields, userid, context.getId())
    364          for result in self.query(q):
    365              evid = result[-1]
    366              data = dict(zip(fields, map(zem.convert, fields, result[:-1])))
    367
    368              # For clear events we are using the history table, so get the event details
    369              # using the history table.
    370              try:
    371                  details = dict( zem.getEventDetailFromStatusOrHistory(evid).getEventDetails() )
    372                  data.update( details )
    373              except ZenEventNotFound:
    374                  pass
    375
    376              # get clear columns
    377              cfields = [('clear.%s' % x) for x in fields]
    378              q = self.clearEventSelect % (",".join(cfields), evid)
    379
    380              # convert clear columns to clear names
    381              cfields = [('clear%s' % _capitalize(x)) for x in fields]
    382
    383              # there might not be a clear event, so set empty defaults
    384              data.update({}.fromkeys(cfields, ""))
    385
    386              # pull in the clear event data
    387              for values in self.query(q):
    388                  values = map(zem.convert, fields, values)
    389                  data.update(dict(zip(cfields, values)))
    390
    391              # If our event has a clearid, but we have no clear data it means
    392              # that we're in a small delay before it is inserted. We'll wait
    393              # until next time to deal with the clear.
    394              if data.get('clearid', None) and not data.get('clearEvid', None):
    395                  continue
    396
    397              data['clearOrEventSummary'] = (
    398                  data['clearSummary'] or data['summary'])
    399
    400              # We want to insert the ownerid and stateChange fields into the
    401              # clearSummary and clearFirstTime fields in the case where an
    402              # event was manually cleared by an operator.
    403              if not data.get('clearSummary', False) \
    404                  and data.get('ownerid', False):
    405                  data['clearSummary'] = data['ownerid']
    406                  data['clearFirstTime'] = data.get('stateChange', '')
    407
    408              # add in the link to the url
    409              device = self.dmd.Devices.findDevice(data.get('device', None))
    410              data['eventUrl'] = self.getEventUrl(evid, device)
    411              data['undeleteUrl'] = self.getUndeleteUrl(evid, device)
    412              severity = data.get('severity', -1)
    413              data['severityString'] = zem.getSeverityString(severity)
    414              delcmd = self.clearstate % (evid, userid, context.getId())
    415              if getattr(context, 'sendClear', True):
    416                  if action(context, data, True):
    417                      self.execute(delcmd)
    418              else:
    419                  self.execute(delcmd)
    420
    421
    422 -    def maintenance(self, zem):
    423          """Run stored procedures that maintain the events database.
    424          """
    425          sql = 'call age_events(%s, %s);' % (
    426                  zem.eventAgingHours, zem.eventAgingSeverity)
    427          try:
    428              self.execute(sql)
    429          except ProgrammingError:
    430              self.log.exception("problem with proc: '%s'" % sql)
    431
    432
    433 -    def deleteHistoricalEvents(self, deferred=False, force=False):
    434          """
    435          Once per day delete events from history table.
    436          If force then run the deletion statement regardless of when it was
    437          last run (the deletion will still not run if the historyMaxAgeDays
    438          setting in the event manager is not greater than zero.)
    439          If deferred then we are running in a twisted reactor.  Run the
    440          deletion script in a non-blocking manner (if it is to be run) and
    441          return a deferred (if the deletion script is run.)
    442          In all cases return None if the deletion script is not run.
    443          """
    444          import datetime
    445          import os
    446          import twisted.internet.utils
    447          import Products.ZenUtils.Utils as Utils
    448          import transaction
    449          import subprocess
    450
    451          def onSuccess(unused, startTime):
    452              self.log.info('Done deleting historical events in %.2f seconds' %
    453                              (time.time() - startTime))
    454              return None
    455          def onError(error, startTime):
    456              self.log.error('Error deleting historical events after '
    457                              '%s seconds: %s' % (time.time()-startTime,
    458                              error))
    459              return None
    460
    461          # d is the return value.  It is a deferred if the deferred argument
    462          # is true and if we run the deletion script.  Otherwise it is None
    463          d = None
    464
    465          # Unless the event manager has a positive number of days for its
    466          # historyMaxAgeDays setting then there is never any point in
    467          # performing the deletion.
    468          try:
    469              maxDays = int(self.dmd.ZenEventManager.historyMaxAgeDays)
    470          except ValueError:
    471              maxDays = 0
    472          if maxDays > 0:
    473              # lastDeleteHistoricalEvents_datetime is when the deletion
    474              # script was last run
    475              lastRun = getattr(self.dmd,
    476                                  'lastDeleteHistoricalEvents_datetime', None)
    477              # lastDeleteHistoricalEvents_days is the value of historyMaxAgeDays
    478              # the last time the deletion script was run.  If this value has
    479              # changed then we run the script again regardless of when it was
    480              # last run.
    481              lastAge = getattr(self.dmd,
    482                                  'lastDeleteHistoricalEvents_days', None)
    483              now = datetime.datetime.now()
    484              if not lastRun \
    485                      or now - lastRun > datetime.timedelta(1) \
    486                      or lastAge != maxDays \
    487                      or force:
    488                  self.log.info('Deleting historical events older than %s days' %
    489                                  maxDays)
    490                  startTime = time.time()
    491                  cmd = Utils.zenPath('Products', 'ZenUtils',
    492                                          'ZenDeleteHistory.py')
    493                  args = ['--numDays=%s' % maxDays]
    494                  if deferred:
    495                      # We're in a twisted reactor, so make a twisty call
    496                      d = twisted.internet.utils.getProcessOutput(
    497                              cmd, args, os.environ, errortoo=True)
    498                      d.addCallback(onSuccess, startTime)
    499                      d.addErrback(onError, startTime)
    500                  else:
    501                      # Not in a reactor, so do this in a blocking manner
    502                      proc = subprocess.Popen(
    503                              [cmd]+args, stdout=subprocess.PIPE,
    504                              stderr=subprocess.STDOUT, env=os.environ)
    505                      # Trying to mimic how twisted returns results to us
    506                      # sort of.
    507                      output, _ = proc.communicate()
    508                      code = proc.wait()
    509                      if code:
    510                          onError(output, startTime)
    511                      else:
    512                          onSuccess(output, startTime)
    513                  # Record circumstances of this run
    514                  self.dmd.lastDeleteHistoricalEvents_datetime = now
    515                  self.dmd.lastDeleteHistoricalEvents_days = maxDays
    516                  transaction.commit()
    517          return d
    518
    519
    520 -    def fetchMonitorHostname(self, monitor='localhost'):
    521          if monitor in self.monitorToHost:
    522              return self.monitorToHost[monitor]
    523
    524          all_monitors = self.dmd.Monitors.getPerformanceMonitorNames()
    525          if monitor in all_monitors:
    526              perfMonitor = self.dmd.Monitors.getPerformanceMonitor(monitor)
    527              hostname = getattr(perfMonitor, 'hostname', monitor)
    528          else:
    529              # Someone's put in something that we don't expect
    530              hostname = monitor
    531
    532          if hostname == 'localhost':
    533              hostname = self.daemonHostname
    534
    535          self.monitorToHost[monitor] = hostname
    536          return hostname
    537
    538 -    def heartbeatEvents(self):
    539          """Create events for failed heartbeats.
    540          """
    541          # build cache of existing heartbeat issues
    542          q = ("SELECT monitor, component "
    543               "FROM status WHERE eventClass = '%s'" % Status_Heartbeat)
    544          heartbeatState = Set(self.query(q))
    545
    546          # Find current heartbeat failures
    547          # Note: 'device' in the heartbeat table is actually filled with the
    548          #        collector name
    549          sel = "SELECT device, component FROM heartbeat "
    550          sel += "WHERE DATE_ADD(lastTime, INTERVAL timeout SECOND) <= NOW();"
    551          for monitor, comp in self.query(sel):
    552              hostname = self.fetchMonitorHostname(monitor)
    553              self.sendEvent(
    554                  Event.Event(device=hostname, component=comp,
    555                              eventClass=Status_Heartbeat,
    556                              summary="%s %s heartbeat failure" % (monitor, comp),
    557                              prodState=self.prodState,
    558                              severity=Event.Error))
    559              heartbeatState.discard((monitor, comp))
    560
    561          # clear heartbeats
    562          for monitor, comp in heartbeatState:
    563              hostname = self.fetchMonitorHostname(monitor)
    564              self.sendEvent(
    565                  Event.Event(device=hostname, component=comp,
    566                              eventClass=Status_Heartbeat,
    567                              summary="%s %s heartbeat clear" % (monitor, comp),
    568                              prodState=self.prodState,
    569                              severity=Event.Clear))
    570
    571 -    def runEventCommand(self, cmd, data, clear = None):
    572          try:
    573              command = cmd.command
    574              if clear:
    575                  command = cmd.clearCommand
    576              if not command:
    577                  return True;
    578              device = self.dmd.Devices.findDevice(data.get('device', ''))
    579              component = None
    580              if device:
    581                  componentName = data.get('component')
    582                  for c in device.getMonitoredComponents():
    583                      if c.id == componentName:
    584                          component = c
    585                          break
    586              compiled = talesCompile('string:' + command)
    587              environ = {'dev':device, 'component':component, 'evt':data }
    588              res = compiled(getEngine().getContext(environ))
    589              if isinstance(res, Exception):
    590                  raise res
    591              prot = EventCommandProtocol(cmd, self)
    592              if res:
    593                  self.log.info('Queueing %s' % res)
    594                  self._processQ.queueProcess('/bin/sh', ('/bin/sh', '-c', res),
    595                                          env=None, processProtocol=prot,
    596                                          timeout=cmd.defaultTimeout,
    597                                          timeout_callback=prot.timedOut)
    598          except Exception:
    599              self.log.exception('Error running command %s', cmd.id)
    600          return True
    601
    602
    603 -    def eventCommands(self, zem):
    604          now = time.time()
    605          count = 0
    606          for command in zem.commands():
    607              if command.enabled:
    608                  count += 1
    609                  self.processEvent(zem, command, self.runEventCommand)
    610          self.log.info("Processed %d commands in %f", count, time.time() - now)
    611
    612
    613 -    def mainbody(self):
    614          """main loop to run actions.
    615          """
    616          from twisted.internet.process import reapAllProcesses
    617          reapAllProcesses()
    618          zem = self.dmd.ZenEventManager
    619          self.loadActionRules()
    620          self.eventCommands(zem)
    621          self.processRules(zem)
    622          self.checkVersion(zem)
    623          self.maintenance(zem)
    624          self.deleteHistoricalEvents(deferred=self.options.cycle)
    625          self.heartbeatEvents()
    626
    627
    628 -    def runCycle(self):
    629          try:
    630              start = time.time()
    631              self.syncdb()
    632              self.mainbody()
    633              self.log.info("processed %s rules in %.2f secs",
    634                             len(self.actions), time.time()-start)
    635              self.sendHeartbeat()
    636          except:
    637              self.log.exception("unexpected exception")
    638          if self.options.cycle:
    639              reactor.callLater(self.options.cycletime, self.runCycle)
    640          else:
    641              def shutdown(value):
    642                  reactor.stop()
    643                  return value
    644              self.log.info("waiting for outstanding process to end")
    645              d = self._processQ.stop()
    646              d.addBoth(shutdown)
    647
    648
    649 -    def run(self):
    650          self.prodState = filter(lambda x: x.split(':')[0] == 'Production',
    651                                  self.dmd.prodStateConversions)
    652          import socket
    653          self.daemonHostname = socket.getfqdn()
    654          self.monitorToHost = {}
    655          try:
    656              # eg ['Production:1000']
    657              self.prodState = int(self.prodState[0].split(':')[1])
    658          except:
    659              self.prodState = 1000
    660
    661          self._processQ = ProcessQueue(self.options.parallel)
    662          def startup():
    663              self._processQ.start()
    664              self.schedule.start()
    665              self.runCycle()
    666          reactor.callWhenRunning(startup)
    667          reactor.run()
    668
    669
    670 -    def sendEvent(self, evt):
    671          """Send event to the system.
    672          """
    673          self.dmd.ZenEventManager.sendEvent(evt)
    674
    675
    676 -    def sendHeartbeat(self):
    677          """Send a heartbeat event for this monitor.
    678          """
    679          timeout = self.options.cycletime*3
    680          evt = Event.EventHeartbeat(self.options.monitor, "zenactions", timeout)
    681          self.sendEvent(evt)
    682          self.niceDoggie(self.options.cycletime)
    683
    684
    685 -    def stop(self):
    686          self.running = False
    687          self.log.info("stopping")
    688          self.sendEvent(Event.Event(device=self.options.monitor,
    689                          eventClass=App_Stop,
    690                          summary="zenactions stopped",
    691                          severity=3, component="zenactions"))
    692
    693 -    def format(self, action, data, clear):
    694          fmt = action.format
    695          body = action.body
    696          if clear:
    697              fmt = action.clearFormat
    698              body = action.clearBody
    699          try:
    700              fmt = fmt % data
    701          except Exception, ex:
    702              fmt = "Error formatting event: %s" % (str(ex),)
    703          try:
    704              body = body % data
    705          except Exception, ex:
    706              body = "Error formatting event body: %s" % (str(ex),)
    707          return fmt, body
    708
    709 -    def stripTags(self, data):
    710          """A quick html => plaintext converter
    711             that retains and displays anchor hrefs
    712          """
    713          import re
    714          tags = re.compile(r'<(.|\n)+?>', re.I|re.M)
    715          aattrs = re.compile(r'<a(.|\n)+?href=["\']([^"\']*)[^>]*?>([^<>]*?)</a>', re.I|re.M)
    716          anchors = re.finditer(aattrs, data)
    717          for x in anchors: data = data.replace(x.group(), "%s: %s" % (x.groups()[2], x.groups()[1]))
    718          data = re.sub(tags, '', data)
    719          return data
    720
    721 -    def sendPage(self, action, data, clear = None):
    722          """Send and event to a pager.  Return True if we think page was sent,
    723          False otherwise.
    724          """
    725          if self.options.cycle and not reactor.running:
    726             # Give the reactor time to startup if necessary.
    727             return False
    728
    729          fmt, body = self.format(action, data, clear)
    730          recipients = action.getAddresses()
    731          if not recipients:
    732              self.log.warning('failed to page %s on rule %s: %s',
    733                               action.getUser().id, action.id,
    734                               'Unspecified address.')
    735              return True
    736
    737          result = False
    738          for recipient in recipients:
    739              success, errorMsg = Utils.sendPage(
    740                  recipient, fmt, self.dmd.pageCommand,
    741                  deferred=self.options.cycle)
    742
    743              if success:
    744                  self.log.info('sent page to %s: %s', recipient, fmt)
    745                  # return True if anyone got the page
    746                  result = result or success
    747              else:
    748                  self.log.info('failed to send page to %s: %s %s',
    749                                recipient,
    750                                fmt,
    751                                errorMsg)
    752          return result
    753
    754
    755
    756 -    def sendEmail(self, action, data, clear = None):
    757          """Send an event to an email address.
    758          Return True if we think the email was sent, False otherwise.
    759          """
    760          from email.MIMEText import MIMEText
    761          from email.MIMEMultipart import MIMEMultipart
    762          addr = action.getAddresses()
    763          if not addr:
    764              self.log.warning('failed to email %s on rule %s: %s',
    765                  action.getUser().id, action.id, 'Unspecified address.')
    766              return True
    767
    768          fmt, htmlbody = self.format(action, data, clear)
    769          htmlbody = htmlbody.replace('\n','<br/>\n')
    770          body = self.stripTags(htmlbody)
    771          plaintext = MIMEText(body)
    772
    773          emsg = None
    774          if action.plainText:
    775              emsg = plaintext
    776          else:
    777              emsg = MIMEMultipart('related')
    778              emsgAlternative = MIMEMultipart('alternative')
    779              emsg.attach( emsgAlternative )
    780              html = MIMEText(htmlbody)
    781              html.set_type('text/html')
    782              emsgAlternative.attach(plaintext)
    783              emsgAlternative.attach(html)
    784
    785          emsg['Subject'] = fmt
    786          emsg['From'] = self.dmd.getEmailFrom()
    787          emsg['To'] = ', '.join(addr)
    788          emsg['Date'] = formatdate(None, True)
    789          result, errorMsg = Utils.sendEmail(emsg, self.dmd.smtpHost,
    790                      self.dmd.smtpPort, self.dmd.smtpUseTLS, self.dmd.smtpUser,
    791                      self.dmd.smtpPass)
    792          if result:
    793              self.log.info("rule '%s' sent email:%s to:%s",
    794                  action.id, fmt, addr)
    795          else:
    796              self.log.info("rule '%s' failed to send email to %s: %s %s",
    797                  action.id, ','.join(addr), fmt, errorMsg)
    798          return result
    799
    800
    801 -    def buildOptions(self):
    802          ZCmdBase.buildOptions(self)
    803          self.parser.add_option('--cycletime',
    804              dest='cycletime', default=60, type="int",
    805              help="check events every cycletime seconds")
    806          self.parser.add_option(
    807              '--zopeurl', dest='zopeurl',
    808              default='http://%s:%d' % (socket.getfqdn(), 8080),
    809              help="http path to the root of the zope server")
    810          self.parser.add_option("--monitor", dest="monitor",
    811              default=DEFAULT_MONITOR,
    812              help="Name of monitor instance to use for heartbeat "
    813                  " events. Default is %s." % DEFAULT_MONITOR)
    814          self.parser.add_option("--parallel", dest="parallel",
    815              default=10, type='int',
    816              help="Number of event commands to run concurrently")
    817
    818
    819 -    def sigTerm(self, signum=None, frame=None):
    820          'controlled shutdown of main loop on interrupt'
    821          try:
    822              ZCmdBase.sigTerm(self, signum, frame)
    823          except SystemExit:
    824              reactor.stop()
    825
    826
    827 -class ZenActions(BaseZenActions, ZCmdBase):
    828
    829 -    def __init__(self):
    830          ZCmdBase.__init__(self)
    831          self.schedule = Schedule(self.options, self.dmd)
    832          self.schedule.sendEvent = self.dmd.ZenEventManager.sendEvent
    833          self.schedule.monitor = self.options.monitor
    834
    835          self.actions = []
    836          self.loadActionRules()
    837          self.updateCheck = UpdateCheck()
    838          self.sendEvent(Event.Event(device=self.options.monitor,
    839                          eventClass=App_Start,
    840                          summary="zenactions started",
    841                          severity=0, component="zenactions"))
    842
    843
    844  if __name__ == "__main__":
    845      za = ZenActions()
    846      import logging
    847      logging.getLogger('zen.Events').setLevel(20)
    848      za.run()
    849