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

# Source Code for Module Products.ZenModel.MinMaxThreshold

      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
     14  __doc__= """MinMaxThreshold
     15  Make threshold comparisons dynamic by using TALES expresssions,
     16  rather than just number bounds checking.
     17  """
     18
     19  import rrdtool
     20  from AccessControl import Permissions
     21
     22  from Globals import InitializeClass
     23  from ThresholdClass import ThresholdClass
     24  from ThresholdInstance import ThresholdInstance, ThresholdContext
     25  from Products.ZenEvents import Event
     26  from Products.ZenEvents.ZenEventClasses import Perf_Snmp
     27  from Products.ZenUtils.ZenTales import talesEval, talesEvalStr
     28  from Products.ZenEvents.Exceptions import pythonThresholdException, \
     29          rpnThresholdException
     30
     31  import logging
     32  log = logging.getLogger('zen.MinMaxCheck')
     33
     34  from Products.ZenUtils.Utils import unused, nanToNone
     35  import types
     36
     37
     38 -def rpneval(value, rpn):
     39      """
     40      Simulate RPN evaluation: only handles simple arithmetic
     41      """
     42      if value is None: return value
     43      operators = ('+','-','*','/')
     44      rpn = rpn.split(',')
     45      rpn.reverse()
     46      stack = [value]
     47      while rpn:
     48          next = rpn.pop()
     49          if next in operators:
     50              first = stack.pop()
     51              second = stack.pop()
     52              try:
     53                  value = eval('%s %s %s' % (second, next, first))
     54              except ZeroDivisionError:
     55                  value = 0
     56              stack.append(value)
     57          elif next.upper() == 'ABS':
     58              stack.append(abs(float(stack.pop())))
     59          else:
     60              stack.append(float(next))
     61      return stack[0]
     62
     63
     64 -class MinMaxThreshold(ThresholdClass):
     65      """
     66      Threshold class that can evaluate RPNs and Python expressions
     67      """
     68
     69      minval = ""
     70      maxval = ""
     71      eventClass = Perf_Snmp
     72      severity = 3
     73      escalateCount = 0
     74
     75      _properties = ThresholdClass._properties + (
     76          {'id':'minval',        'type':'string',  'mode':'w'},
     77          {'id':'maxval',        'type':'string',  'mode':'w'},
     78          {'id':'eventClass',    'type':'string',  'mode':'w'},
     79          {'id':'severity',      'type':'int',     'mode':'w'},
     80          {'id':'escalateCount', 'type':'int',     'mode':'w'}
     81          )
     82
     83      factory_type_information = (
     84          {
     85          'immediate_view' : 'editRRDThreshold',
     86          'actions'        :
     87          (
     88          { 'id'            : 'edit'
     89            , 'name'          : 'Min/Max Threshold'
     90            , 'action'        : 'editRRDThreshold'
     91            , 'permissions'   : ( Permissions.view, )
     92            },
     93          )
     94          },
     95          )
     96
     97 -    def createThresholdInstance(self, context):
     98          """Return the config used by the collector to process min/max
     99          thresholds. (id, minval, maxval, severity, escalateCount)
    100          """
    101          mmt = MinMaxThresholdInstance(self.id,
    102                                        ThresholdContext(context),
    103                                        self.dsnames,
    104                                        minval=self.getMinval(context),
    105                                        maxval=self.getMaxval(context),
    106                                        eventClass=self.eventClass,
    107                                        severity=self.severity,
    108                                        escalateCount=self.escalateCount)
    109          return mmt
    110
    111 -    def getMinval(self, context):
    112          """Build the min value for this threshold.
    113          """
    114          minval = None
    115          if self.minval:
    116              try:
    117                  express = "python:%s" % self.minval
    118                  minval = talesEval(express, context)
    119              except:
    120                  msg= "User-supplied Python expression (%s) for minimum value caused error: %s" % \
    121                             ( self.minval,  self.dsnames )
    122                  log.error( msg )
    123                  raise pythonThresholdException(msg)
    124                  minval = None
    125          return nanToNone(minval)
    126
    127
    128 -    def getMaxval(self, context):
    129          """Build the max value for this threshold.
    130          """
    131          maxval = None
    132          if self.maxval:
    133              try:
    134                  express = "python:%s" % self.maxval
    135                  maxval = talesEval(express, context)
    136              except:
    137                  msg= "User-supplied Python expression (%s) for maximum value caused error: %s" % \
    138                             ( self.maxval,  self.dsnames )
    139                  log.error( msg )
    140                  raise pythonThresholdException(msg)
    141                  maxval = None
    142          return nanToNone(maxval)
    143
    144  InitializeClass(MinMaxThreshold)
    145  MinMaxThresholdClass = MinMaxThreshold
    146
    147
    148
    149 -class MinMaxThresholdInstance(ThresholdInstance):
    150      # Not strictly necessary, but helps when restoring instances from
    151      # pickle files that were not constructed with a count member.
    152      count = {}
    153
    154 -    def __init__(self, id, context, dpNames,
    155                   minval, maxval, eventClass, severity, escalateCount):
    156          self.count = {}
    157          self._context = context
    158          self.id = id
    159          self.minimum = minval
    160          self.maximum = maxval
    161          self.eventClass = eventClass
    162          self.severity = severity
    163          self.escalateCount = escalateCount
    164          self.dataPointNames = dpNames
    165          self._rrdInfoCache = {}
    166
    167 -    def name(self):
    168          "return the name of this threshold (from the ThresholdClass)"
    169          return self.id
    170
    171 -    def context(self):
    172          "Return an identifying context (device, or device and component)"
    173          return self._context
    174
    175 -    def dataPoints(self):
    176          "Returns the names of the datapoints used to compute the threshold"
    177          return self.dataPointNames
    178
    179 -    def rrdInfoCache(self, dp):
    180          if dp in self._rrdInfoCache:
    181              return self._rrdInfoCache[dp]
    182          data = rrdtool.info(self.context().path(dp))
    183          # handle both old and new style RRD versions
    184          try:
    185              # old style 1.2.x
    186              value = data['step'], data['ds']['ds0']['type']
    187          except KeyError:
    188              # new style 1.3.x
    189              value = data['step'], data['ds[ds0].type']
    190          self._rrdInfoCache[dp] = value
    191          return value
    192
    193 -    def countKey(self, dp):
    194          return(':'.join(self.context().key()) + ':' + dp)
    195
    196 -    def getCount(self, dp):
    197          countKey = self.countKey(dp)
    198          if not self.count.has_key(countKey):
    199              return None
    200          return self.count[countKey]
    201
    202 -    def incrementCount(self, dp):
    203          countKey = self.countKey(dp)
    204          if not self.count.has_key(countKey):
    205              self.resetCount(dp)
    206          self.count[countKey] += 1
    207          return self.count[countKey]
    208
    209 -    def resetCount(self, dp):
    210          self.count[self.countKey(dp)] = 0
    211
    212 -    def fetchLastValue(self, dp, cycleTime):
    213          """
    214          Fetch the most recent value for a data point from the RRD file.
    215          """
    216          startStop, names, values = rrdtool.fetch(self.context().path(dp),
    217              'AVERAGE', '-s', 'now-%d' % (cycleTime*2), '-e', 'now')
    218          values = [ v[0] for v in values if v[0] is not None ]
    219          if values: return values[-1]
    220
    221 -    def check(self, dataPoints):
    222          """The given datapoints have been updated, so re-evaluate.
    223          returns events or an empty sequence"""
    224          unused(dataPoints)
    225          result = []
    226          for dp in self.dataPointNames:
    227              cycleTime, rrdType = self.rrdInfoCache(dp)
    228              result.extend(self.checkRange(
    229                  dp, self.fetchLastValue(dp, cycleTime)))
    230          return result
    231
    232 -    def checkRaw(self, dataPoint, timeOf, value):
    233          """A new datapoint has been collected, use the given _raw_
    234          value to re-evalue the threshold."""
    235          unused(timeOf)
    236          result = []
    237          if value is None: return result
    238          try:
    239              cycleTime, rrdType = self.rrdInfoCache(dataPoint)
    240          except Exception:
    241              log.exception('Unable to read RRD file for %s' % dataPoint)
    242              return result
    243          if rrdType != 'GAUGE' and value is None:
    244              value = self.fetchLastValue(dataPoint, cycleTime)
    245          result.extend(self.checkRange(dataPoint, value))
    246          return result
    247
    248 -    def checkRange(self, dp, value):
    249          'Check the value for min/max thresholds'
    250          log.debug("Checking %s %s against min %s and max %s",
    251                    dp, value, self.minimum, self.maximum)
    252          if value is None:
    253              return []
    254          if type(value) in types.StringTypes:
    255              value = float(value)
    256          thresh = None
    257
    258          # Handle all cases where both minimum and maximum are set.
    259          if self.maximum is not None and self.minimum is not None:
    260              if self.maximum >= self.minimum and value > self.maximum:
    261                  thresh = self.maximum
    262                  how = 'exceeded'
    263              if self.maximum >= self.minimum and value < self.minimum:
    264                  thresh = self.maximum
    265                  how = 'not met'
    266              elif self.maximum < self.minimum \
    267                  and (value < self.minimum and value > self.maximum):
    268                  thresh = self.maximum
    269                  how = 'violated'
    270
    271          # Handle simple cases where only minimum or maximum is set.
    272          elif self.maximum is not None and value > self.maximum:
    273              thresh = self.maximum
    274              how = 'exceeded'
    275          elif self.minimum is not None and value < self.minimum:
    276              thresh = self.minimum
    277              how = 'not met'
    278
    279          if thresh is not None:
    280              severity = self.severity
    281              count = self.incrementCount(dp)
    282              if self.escalateCount and count >= self.escalateCount:
    283                  severity = min(severity + 1, 5)
    284              summary = 'threshold of %s %s: current value %.2f' % (
    285                  self.name(), how, float(value))
    286              return [dict(device=self.context().deviceName,
    287                           summary=summary,
    288                           eventKey=self.id,
    289                           eventClass=self.eventClass,
    290                           component=self.context().componentName,
    291                           severity=severity)]
    292          else:
    293              count = self.getCount(dp)
    294              if count is None or count > 0:
    295                  summary = 'threshold of %s restored: current value %.2f' % (
    296                      self.name(), value)
    297                  self.resetCount(dp)
    298                  return [dict(device=self.context().deviceName,
    299                               summary=summary,
    300                               eventKey=self.id,
    301                               eventClass=self.eventClass,
    302                               component=self.context().componentName,
    303                               severity=Event.Clear)]
    304          return []
    305
    306
    307 -    def raiseRPNExc( self ):
    308          """
    309          Raise an RPN exception, taking care to log all details.
    310          """
    311          msg= "The following RPN exception is from user-supplied code."
    312          log.exception( msg )
    313          raise rpnThresholdException(msg)
    314
    315
    316 -    def getGraphElements(self, template, context, gopts, namespace, color,
    317                           legend, relatedGps):
    318          """Produce a visual indication on the graph of where the
    319          threshold applies."""
    320          unused(template, namespace)
    321          if not color.startswith('#'):
    322              color = '#%s' % color
    323          minval = self.minimum
    324          maxval = self.maximum
    325          if not self.dataPointNames:
    326              return gopts
    327          gp = relatedGps[self.dataPointNames[0]]
    328
    329          # Attempt any RPN expressions
    330          rpn = getattr(gp, 'rpn', None)
    331          if rpn:
    332              try:
    333                  rpn = talesEvalStr(rpn, context)
    334              except:
    335                  self.raiseRPNExc()
    336                  return gopts
    337
    338              try:
    339                  minval = rpneval(minval, rpn)
    340              except:
    341                  minval= 0
    342                  self.raiseRPNExc()
    343
    344              try:
    345                  maxval = rpneval(maxval, rpn)
    346              except:
    347                  maxval= 0
    348                  self.raiseRPNExc()
    349
    350          minstr = self.setPower(minval)
    351          maxstr = self.setPower(maxval)
    352
    353          if legend:
    354              gopts.append(
    355                  "HRULE:%s%s:%s\\j" % (minval or maxval, color, legend))
    356          elif minval is not None and maxval is not None:
    357              if minval == maxval:
    358                  gopts.append(
    359                      "HRULE:%s%s:%s not equal to %s\\j" % (minval, color,
    360                          self.getNames(relatedGps), minstr))
    361              elif minval < maxval:
    362                  gopts.append(
    363                      "HRULE:%s%s:%s not within %s and %s\\j" % (minval, color,
    364                          self.getNames(relatedGps), minstr, maxstr))
    365                  gopts.append("HRULE:%s%s" % (maxval, color))
    366              elif minval > maxval:
    367                  gopts.append(
    368                      "HRULE:%s%s:%s between %s and %s\\j" % (minval, color,
    369                          self.getNames(relatedGps), maxstr, minstr))
    370                  gopts.append("HRULE:%s%s" % (maxval, color))
    371          elif minval is not None :
    372              gopts.append(
    373                  "HRULE:%s%s:%s less than %s\\j" % (minval, color,
    374                      self.getNames(relatedGps), minstr))
    375          elif maxval is not None:
    376              gopts.append(
    377                  "HRULE:%s%s:%s greater than %s\\j" % (maxval, color,
    378                      self.getNames(relatedGps), maxstr))
    379
    380          return gopts
    381
    382
    383 -    def getNames(self, relatedGps):
    384          names = list(set([x.split('_', 1)[1] for x in self.dataPointNames]))
    385          names.sort()
    386          return ', '.join(names)
    387
    388 -    def setPower(self, number):
    389          powers = ("k", "M", "G")
    390          if number < 1000: return number
    391          for power in powers:
    392              number = number / 1000.0
    393              if number < 1000:
    394                  return "%0.2f%s" % (number, power)
    395          return "%.2f%s" % (number, powers[-1])
    396
    397  from twisted.spread import pb
    398  pb.setUnjellyableForClass(MinMaxThresholdInstance, MinMaxThresholdInstance)
    399