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                  minval = talesEval("python:"+self.minval, context)
    118              except:
    119                  msg= "User-supplied Python expression (%s) for minimum value caused error: %s" % \
    120                             ( self.minval,  self.dsnames )
    121                  log.error( msg )
    122                  raise pythonThresholdException(msg)
    123                  minval = None
    124          return nanToNone(minval)
    125
    126
    127 -    def getMaxval(self, context):
    128          """Build the max value for this threshold.
    129          """
    130          maxval = None
    131          if self.maxval:
    132              try:
    133                  maxval = talesEval("python:"+self.maxval, context)
    134              except:
    135                  msg= "User-supplied Python expression (%s) for maximum value caused error: %s" % \
    136                             ( self.maxval,  self.dsnames )
    137                  log.error( msg )
    138                  raise pythonThresholdException(msg)
    139                  maxval = None
    140          return nanToNone(maxval)
    141
    142  InitializeClass(MinMaxThreshold)
    143  MinMaxThresholdClass = MinMaxThreshold
    144
    145
    146
    147 -class MinMaxThresholdInstance(ThresholdInstance):
    148      # Not strictly necessary, but helps when restoring instances from
    149      # pickle files that were not constructed with a count member.
    150      count = {}
    151
    152 -    def __init__(self, id, context, dpNames,
    153                   minval, maxval, eventClass, severity, escalateCount):
    154          self.count = {}
    155          self._context = context
    156          self.id = id
    157          self.minimum = minval
    158          self.maximum = maxval
    159          self.eventClass = eventClass
    160          self.severity = severity
    161          self.escalateCount = escalateCount
    162          self.dataPointNames = dpNames
    163          self._rrdInfoCache = {}
    164
    165 -    def name(self):
    166          "return the name of this threshold (from the ThresholdClass)"
    167          return self.id
    168
    169 -    def context(self):
    170          "Return an identifying context (device, or device and component)"
    171          return self._context
    172
    173 -    def dataPoints(self):
    174          "Returns the names of the datapoints used to compute the threshold"
    175          return self.dataPointNames
    176
    177 -    def rrdInfoCache(self, dp):
    178          if dp in self._rrdInfoCache:
    179              return self._rrdInfoCache[dp]
    180          data = rrdtool.info(self.context().path(dp))
    181          # handle both old and new style RRD versions
    182          try:
    183              # old style 1.2.x
    184              value = data['step'], data['ds']['ds0']['type']
    185          except KeyError:
    186              # new style 1.3.x
    187              value = data['step'], data['ds[ds0].type']
    188          self._rrdInfoCache[dp] = value
    189          return value
    190
    191 -    def countKey(self, dp):
    192          return(':'.join(self.context().key()) + ':' + dp)
    193
    194 -    def getCount(self, dp):
    195          countKey = self.countKey(dp)
    196          if not self.count.has_key(countKey):
    197              return None
    198          return self.count[countKey]
    199
    200 -    def incrementCount(self, dp):
    201          countKey = self.countKey(dp)
    202          if not self.count.has_key(countKey):
    203              self.resetCount(dp)
    204          self.count[countKey] += 1
    205          return self.count[countKey]
    206
    207 -    def resetCount(self, dp):
    208          self.count[self.countKey(dp)] = 0
    209
    210 -    def fetchLastValue(self, dp, cycleTime):
    211          """
    212          Fetch the most recent value for a data point from the RRD file.
    213          """
    214          startStop, names, values = rrdtool.fetch(self.context().path(dp),
    215              'AVERAGE', '-s', 'now-%d' % (cycleTime*2), '-e', 'now')
    216          values = [ v[0] for v in values if v[0] is not None ]
    217          if values: return values[-1]
    218
    219 -    def check(self, dataPoints):
    220          """The given datapoints have been updated, so re-evaluate.
    221          returns events or an empty sequence"""
    222          unused(dataPoints)
    223          result = []
    224          for dp in self.dataPointNames:
    225              cycleTime, rrdType = self.rrdInfoCache(dp)
    226              result.extend(self.checkRange(
    227                  dp, self.fetchLastValue(dp, cycleTime)))
    228          return result
    229
    230 -    def checkRaw(self, dataPoint, timeOf, value):
    231          """A new datapoint has been collected, use the given _raw_
    232          value to re-evalue the threshold."""
    233          unused(timeOf)
    234          result = []
    235          if value is None: return result
    236          try:
    237              cycleTime, rrdType = self.rrdInfoCache(dataPoint)
    238          except Exception:
    239              log.exception('Unable to read RRD file for %s' % dataPoint)
    240              return result
    241          if rrdType != 'GAUGE' and value is None:
    242              value = self.fetchLastValue(dataPoint, cycleTime)
    243          result.extend(self.checkRange(dataPoint, value))
    244          return result
    245
    246 -    def checkRange(self, dp, value):
    247          'Check the value for min/max thresholds'
    248          log.debug("Checking %s %s against min %s and max %s",
    249                    dp, value, self.minimum, self.maximum)
    250          if value is None:
    251              return []
    252          if type(value) in types.StringTypes:
    253              value = float(value)
    254          thresh = None
    255
    256          # Handle all cases where both minimum and maximum are set.
    257          if self.maximum is not None and self.minimum is not None:
    258              if self.maximum >= self.minimum and value > self.maximum:
    259                  thresh = self.maximum
    260                  how = 'exceeded'
    261              if self.maximum >= self.minimum and value < self.minimum:
    262                  thresh = self.maximum
    263                  how = 'not met'
    264              elif self.maximum < self.minimum \
    265                  and (value < self.minimum and value > self.maximum):
    266                  thresh = self.maximum
    267                  how = 'violated'
    268
    269          # Handle simple cases where only minimum or maximum is set.
    270          elif self.maximum is not None and value > self.maximum:
    271              thresh = self.maximum
    272              how = 'exceeded'
    273          elif self.minimum is not None and value < self.minimum:
    274              thresh = self.minimum
    275              how = 'not met'
    276
    277          if thresh is not None:
    278              severity = self.severity
    279              count = self.incrementCount(dp)
    280              if self.escalateCount and count >= self.escalateCount:
    281                  severity = min(severity + 1, 5)
    282              summary = 'threshold of %s %s: current value %.2f' % (
    283                  self.name(), how, float(value))
    284              return [dict(device=self.context().deviceName,
    285                           summary=summary,
    286                           eventKey=self.id,
    287                           eventClass=self.eventClass,
    288                           component=self.context().componentName,
    289                           severity=severity)]
    290          else:
    291              count = self.getCount(dp)
    292              if count is None or count > 0:
    293                  summary = 'threshold of %s restored: current value %.2f' % (
    294                      self.name(), value)
    295                  self.resetCount(dp)
    296                  return [dict(device=self.context().deviceName,
    297                               summary=summary,
    298                               eventKey=self.id,
    299                               eventClass=self.eventClass,
    300                               component=self.context().componentName,
    301                               severity=Event.Clear)]
    302          return []
    303
    304
    305 -    def raiseRPNExc( self ):
    306          """
    307          Raise an RPN exception, taking care to log all details.
    308          """
    309          msg= "The following RPN exception is from user-supplied code."
    310          log.exception( msg )
    311          raise rpnThresholdException(msg)
    312
    313
    314 -    def getGraphElements(self, template, context, gopts, namespace, color,
    315                           legend, relatedGps):
    316          """Produce a visual indication on the graph of where the
    317          threshold applies."""
    318          unused(template, namespace)
    319          if not color.startswith('#'):
    320              color = '#%s' % color
    321          minval = self.minimum
    322          maxval = self.maximum
    323          if not self.dataPointNames:
    324              return gopts
    325          gp = relatedGps[self.dataPointNames[0]]
    326
    327          # Attempt any RPN expressions
    328          rpn = getattr(gp, 'rpn', None)
    329          if rpn:
    330              try:
    331                  rpn = talesEvalStr(rpn, context)
    332              except:
    333                  self.raiseRPNExc()
    334                  return gopts
    335
    336              try:
    337                  minval = rpneval(minval, rpn)
    338              except:
    339                  minval= 0
    340                  self.raiseRPNExc()
    341
    342              try:
    343                  maxval = rpneval(maxval, rpn)
    344              except:
    345                  maxval= 0
    346                  self.raiseRPNExc()
    347
    348          minstr = self.setPower(minval)
    349          maxstr = self.setPower(maxval)
    350
    351          if legend:
    352              gopts.append(
    353                  "HRULE:%s%s:%s\\j" % (minval or maxval, color, legend))
    354          elif minval is not None and maxval is not None:
    355              if minval == maxval:
    356                  gopts.append(
    357                      "HRULE:%s%s:%s not equal to %s\\j" % (minval, color,
    358                          self.getNames(relatedGps), minstr))
    359              elif minval < maxval:
    360                  gopts.append(
    361                      "HRULE:%s%s:%s not within %s and %s\\j" % (minval, color,
    362                          self.getNames(relatedGps), minstr, maxstr))
    363                  gopts.append("HRULE:%s%s" % (maxval, color))
    364              elif minval > maxval:
    365                  gopts.append(
    366                      "HRULE:%s%s:%s between %s and %s\\j" % (minval, color,
    367                          self.getNames(relatedGps), maxstr, minstr))
    368                  gopts.append("HRULE:%s%s" % (maxval, color))
    369          elif minval is not None :
    370              gopts.append(
    371                  "HRULE:%s%s:%s less than %s\\j" % (minval, color,
    372                      self.getNames(relatedGps), minstr))
    373          elif maxval is not None:
    374              gopts.append(
    375                  "HRULE:%s%s:%s greater than %s\\j" % (maxval, color,
    376                      self.getNames(relatedGps), maxstr))
    377
    378          return gopts
    379
    380
    381 -    def getNames(self, relatedGps):
    382          names = list(set([x.split('_', 1)[1] for x in self.dataPointNames]))
    383          names.sort()
    384          return ', '.join(names)
    385
    386 -    def setPower(self, number):
    387          powers = ("k", "M", "G")
    388          if number < 1000: return number
    389          for power in powers:
    390              number = number / 1000.0
    391              if number < 1000:
    392                  return "%0.2f%s" % (number, power)
    393          return "%.2f%s" % (number, powers[-1])
    394
    395  from twisted.spread import pb
    396  pb.setUnjellyableForClass(MinMaxThresholdInstance, MinMaxThresholdInstance)
    397