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