Package Products :: Package ZenRRD :: Module RenderServer |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenRRD.RenderServer

      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__="""RenderServer
     15
     16  Frontend that passes RRD graph options to rrdtool to render,
     17  and then returns an URL to access the rendered graphic file.
     18  """
     19
     20  import os
     21  import time
     22  import logging
     23  import urllib
     24  import zlib
     25  import mimetypes
     26
     27  from AccessControl import ClassSecurityInfo
     28  from Globals import InitializeClass
     29  from Globals import DTMLFile
     30  from sets import Set
     31
     32  try:
     33      import rrdtool
     34  except ImportError:
     35      pass
     36
     37  try:
     38      from base64 import urlsafe_b64decode
     39      raise ImportError
     40  except ImportError:
     41 -    def urlsafe_b64decode(s):
     42          import base64
     43          return base64.decodestring(s.replace('-','+').replace('_','/'))
     44
     45  from Products.ZenRRD.RRDUtil import fixMissingRRDs
     46  from Products.ZenUtils.PObjectCache import PObjectCache
     47  from Products.ZenUtils.Utils import zenPath
     48
     49  from RRDToolItem import RRDToolItem
     50
     51  from Products.ZenModel.PerformanceConf import performancePath
     52  import glob
     53  import tarfile
     54
     55  log = logging.getLogger("RenderServer")
     56
     57
     58 -def manage_addRenderServer(context, id, REQUEST = None):
     59      """
     60      Make a RenderServer
     61      """
     62      rs = RenderServer(id)
     63      context._setObject(id, rs)
     64      if REQUEST is not None:
     65          REQUEST['RESPONSE'].redirect(context.absolute_url()+'/manage_main')
     66
     67
     68  addRenderServer = DTMLFile('dtml/addRenderServer',globals())
     69
     70
     71 -class RenderServer(RRDToolItem):
     72      """
     73      Base class for turning graph requests into graphics.
     74      NB: Any log messages get logged into the event.log file.
     75      """
     76
     77      meta_type = "RenderServer"
     78
     79      cacheName = 'RRDRenderCache'
     80
     81      security = ClassSecurityInfo()
     82
     83 -    def __init__(self, id, tmpdir = '/tmp/renderserver', cachetimeout=300):
     84          self.id = id
     85          self.tmpdir = tmpdir
     86          self.cachetimeout = cachetimeout
     87
     88
     89      security.declareProtected('View', 'render')
     90 -    def render(self, gopts=None, start=None, end=None, drange=None,
     91                 remoteUrl=None, width=None, ftype='PNG', getImage=True,
     92                 graphid='', comment=None, ms=None, REQUEST=None):
     93          """
     94          Render a graph and return it
     95
     96          @param gopts: RRD graph creation options
     97          @param start: requested start of data to graph
     98          @param end: requested start of data to graph
     99          @param drange: min/max values of the graph
    100          @param remoteUrl: if the RRD is not here, where it lives
    101          @param width: size of graphic to create
    102          @param ftype: file type of graphic (eg PNG)
    103          @param getImage: return the graph or a script location
    104          @param graphid: (hopefully) unique identifier of a graph
    105          @param comment: RRD graph comment
    106          @param ms: a timestamp used to force IE to reload images
    107          @param REQUEST: URL-marshalled object containg URL options
    108          @return: graph or script location
    109          """
    110          gopts = zlib.decompress(urlsafe_b64decode(gopts))
    111          gopts = gopts.split('|')
    112          gopts = fixMissingRRDs(gopts)
    113          gopts.append('HRULE:INF#00000000')
    114          gopts.append('--width=%s' % width)
    115          if start:
    116              gopts.append('--start=%s' % start)
    117          if end:
    118              gopts.append('--end=%s' % end)
    119          drange = int(drange)
    120          id = self.graphId(gopts, drange, ftype)
    121          graph = self.getGraph(id, ftype, REQUEST)
    122          if not graph:
    123              if not os.path.exists(self.tmpdir):
    124                  os.makedirs(self.tmpdir, 0750)
    125              filename = "%s/graph-%s" % (self.tmpdir,id)
    126              if remoteUrl:
    127                  f = open(filename, "w")
    128                  f.write(urllib.urlopen(remoteUrl).read())
    129                  f.close()
    130              else:
    131                  if ftype.lower()=='html':
    132                      imgtype = 'PNG'
    133                  else:
    134                      imgtype = ftype
    135                  gopts.insert(0, "--imgformat=%s" % imgtype)
    136                  #gopts.insert(0, "--lazy")
    137                  end = int(time.time())-300
    138                  start = end - drange
    139                  if comment is not None:
    140                      gopts.insert(0, 'COMMENT:%s\\c' % comment)
    141                  gopts.insert(0, '--end=%d' % end)
    142                  gopts.insert(0, '--start=%d' % start)
    143                  gopts.insert(0, filename)
    144                  log.debug("RRD graphing options: %r", (gopts,))
    145                  try:
    146                      rrdtool.graph(*gopts)
    147                  except Exception, ex:
    148                      if ex.args[0].find('No such file or directory') > -1:
    149                          return None
    150                      log.exception("Failed to generate a graph")
    151                      log.warn(" ".join(gopts))
    152                      return None
    153
    154              self.addGraph(id, filename)
    155              graph = self.getGraph(id, ftype, REQUEST)
    156
    157          if getImage:
    158              return graph
    159          else:
    160              return """
    161              <script>
    162                  parent.location.hash = '%s:%s;';
    163              </script>
    164              """ % (graphid, str(bool(graph)))
    165
    166
    167 -    def deleteRRDFiles(self, device,
    168                          datasource=None, datapoint=None,
    169                          remoteUrl=None, REQUEST=None):
    170          """
    171          Delete RRD files associated with the given device id.
    172          If datapoint is not None then delete the file corresponding to that dp.
    173          Else if datasource is not None then delete the files corresponding to
    174            all datapoints in the datasource.
    175          Else delete all RRD files associated with the given device.
    176
    177          @param device: device name
    178          @param datasource: RRD datasource (DS) name
    179          @param datapoint: RRD datapoint name (lives in a DS)
    180          @param remoteUrl: if the RRD is not here, where it lives
    181          @param REQUEST: URL-marshalled object containg URL options
    182          """
    183          devDir = performancePath('/Devices/%s' % device)
    184          if not os.path.isdir(devDir):
    185              return
    186          fileNames = []
    187          dirNames = []
    188          if datapoint:
    189              fileNames = [
    190                  performancePath('/Devices/%s/%s.rrd' % (device, datapoint))]
    191          elif datasource:
    192              rrdPath = '/Devices/%s/%s_*.rrd' % (device, datasource)
    193              fileNames = glob.glob(performancePath(rrdPath))
    194          else:
    195              for dPath, dNames, dFiles in os.walk(devDir, topdown=False):
    196                  fileNames += [os.path.join(dPath, f) for f in dFiles]
    197                  dirNames += [os.path.join(dPath, d) for d in dNames]
    198              dirNames.append(devDir)
    199          for fileName in fileNames:
    200              try:
    201                  os.remove(fileName)
    202              except OSError:
    203                  log.warn("File %s does not exist" % fileName)
    204          for dirName in dirNames:
    205              try:
    206                  os.rmdir(dirName)
    207              except OSError:
    208                  log.warn('Directory %s could not be removed' % dirName)
    209          if remoteUrl:
    210              urllib.urlopen(remoteUrl)
    211
    212
    213 -    def packageRRDFiles(self, device, REQUEST=None):
    214          """
    215          Tar up RRD files into a nice, neat package
    216
    217          @param device: device name
    218          @param REQUEST: URL-marshalled object containg URL options
    219          """
    220          srcdir = performancePath('/Devices/%s' % device)
    221          tarfilename = '%s/%s.tgz' % (self.tmpdir, device)
    222          log.debug( "tarring up %s into %s" % ( srcdir, tarfilename ))
    223          tar = tarfile.open(tarfilename, "w:gz")
    224          for file in os.listdir(srcdir):
    225              tar.add('%s/%s' % (srcdir, file), '/%s' % os.path.basename(file))
    226          tar.close()
    227
    228 -    def unpackageRRDFiles(self, device, REQUEST=None):
    229          """
    230          Untar a package of RRDFiles
    231
    232          @param device: device name
    233          @param REQUEST: URL-marshalled object containg URL options
    234          """
    235          destdir = performancePath('/Devices/%s' % device)
    236          tarfilename = '%s/%s.tgz' % (self.tmpdir, device)
    237          log.debug( "Untarring %s into %s" % ( tarfilename, destdir ))
    238          tar = tarfile.open(tarfilename, "r:gz")
    239          for file in tar.getmembers():
    240              tar.extract(file, destdir)
    241          tar.close()
    242
    243 -    def receiveRRDFiles(self, REQUEST=None):
    244          """
    245          Receive a device's RRD Files from another server
    246          This function is called by sendRRDFiles()
    247
    248          @param REQUEST: 'tarfile', 'tarfilename'
    249          @type REQUEST: URL-marshalled parameters
    250          """
    251          tarfile = REQUEST.get('tarfile')
    252          tarfilename = REQUEST.get('tarfilename')
    253          log.debug( "Receiving %s ..." % ( tarfilename ))
    254          f=open('%s/%s' % (self.tmpdir, tarfilename), 'wb')
    255          f.write(urllib.unquote(tarfile))
    256          f.close()
    257
    258 -    def sendRRDFiles(self, device, server, REQUEST=None):
    259          """
    260          Move a package of RRDFiles
    261
    262          @param device: device name
    263          @param server: another RenderServer instance
    264          @param REQUEST: URL-marshalled object containg URL options
    265          """
    266          tarfilename = '%s.tgz' % device
    267          f=open('%s/%s' % (self.tmpdir, tarfilename), 'rb')
    268          tarfilebody=f.read()
    269          f.close()
    270          # urlencode the id, title and file
    271          params = urllib.urlencode({'tarfilename': tarfilename,
    272              'tarfile':tarfilebody})
    273
    274          # send the file to Zope
    275          perfMon = self.dmd.getDmdRoot("Monitors").getPerformanceMonitor(server)
    276          if perfMon.renderurl.startswith('http'):
    277              remoteUrl = '%s/receiveRRDFiles' % (perfMon.renderurl)
    278              log.debug( "Sending %s to %s ..." % ( tarfilename, remoteUrl ))
    279              urllib.urlopen(remoteUrl, params)
    280
    281
    282 -    def moveRRDFiles(self, device, destServer, srcServer=None, REQUEST=None):
    283          """
    284          Send a device's RRD files to another server
    285
    286          @param device: device name
    287          @param destServer: another RenderServer instance
    288          @param srcServer: another RenderServer instance
    289          @param REQUEST: URL-marshalled object containg URL options
    290          """
    291          monitors = self.dmd.getDmdRoot("Monitors")
    292          destPerfMon = monitors.getPerformanceMonitor(destServer)
    293          if srcServer:
    294              srcPerfMon = monitors.getPerformanceMonitor(srcServer)
    295              remoteUrl = '%s/moveRRDFiles?device=%s&destServer=%s' % (srcPerfMon.renderurl, device, destServer)
    296              urllib.urlopen(remoteUrl)
    297
    298          else:
    299              self.packageRRDFiles(device, REQUEST)
    300              self.sendRRDFiles(device, destServer, REQUEST)
    301              if destPerfMon.renderurl.startswith('http'):
    302                  remoteUrl = '%s/unpackageRRDFiles?device=%s' % (destPerfMon.renderurl, device)
    303                  urllib.urlopen(remoteUrl)
    304              else:
    305                  self.unpackageRRDFiles(device, REQUEST)
    306
    307      security.declareProtected('View', 'plugin')
    308 -    def plugin(self, name, REQUEST=None):
    309          """
    310          Render a custom graph and return it
    311
    312          @param name: plugin name from Products/ZenRRD/plugins
    313          @return: graph or None
    314          """
    315          try:
    316              m = zenPath('Products/ZenRRD/plugins/%s.py' % name)
    317              log.debug( "Trying plugin %s to generate a graph..." % m )
    318              graph = None
    319              exec open(m)
    320              return graph
    321          except Exception, ex:
    322              log.exception("Failed generating graph from plugin %s" % name)
    323              raise
    324
    325
    326      security.declareProtected('GenSummary', 'summary')
    327 -    def summary(self, gopts):
    328          """
    329          Return summary information as a list but no graph
    330
    331          @param gopts: RRD graph options
    332          @return: values from the graph
    333          """
    334          gopts = fixMissingRRDs(gopts)
    335          gopts.insert(0, '/dev/null') #no graph generated
    336          try:
    337              values = rrdtool.graph(*gopts)[2]
    338          except Exception, ex:
    339              if ex.args[0].find('No such file or directory') > -1:
    340                  return None
    341              log.exception("Failed while generating summary")
    342              log.warn(" ".join(gopts))
    343              raise
    344          return values
    345
    346
    347      security.declareProtected('GenSummary', 'fetchValues')
    348 -    def fetchValues(self, paths, cf, resolution, start, end=""):
    349          if not end:
    350              end = "now"
    351          values = []
    352          try:
    353              for path in paths:
    354                  values.append(rrdtool.fetch(path, cf, "-r %d" % resolution,
    355                      "-s %s" % start,"-e %s" % end))
    356              return values
    357          except NameError:
    358              log.exception("It appears that the rrdtool bindings are not installed properly.")
    359          except Exception, ex:
    360              if ex.args[0].find('No such file or directory') > -1:
    361                  return None
    362              log.exception("Failed while generating current values")
    363              raise
    364
    365
    366      security.declareProtected('GenSummary', 'currentValues')
    367 -    def currentValues(self, paths):
    368          """
    369          Return the latest values recorded in the RRD file
    370          """
    371          try:
    372              def value(p):
    373                  v = None
    374                  info = None
    375                  try:
    376                      info = rrdtool.info(p)
    377                  except:
    378                      log.debug('%s not found' % p)
    379                  if info:
    380                      last = info['last_update']
    381                      step = info['step']
    382                      v = rrdtool.graph('/dev/null',
    383                                        'DEF:x=%s:ds0:AVERAGE' % p,
    384                                        'VDEF:v=x,LAST',
    385                                        'PRINT:v:%.2lf',
    386                                        '--start=%d'%(last-step),
    387                                        '--end=%d'%last)
    388                      v = float(v[2][0])
    389                      if str(v) == 'nan': v = None
    390                  return v
    391              return map(value, paths)
    392
    393          except NameError:
    394              log.exception("It appears that the rrdtool bindings are not installed properly.")
    395
    396          except Exception, ex:
    397              if ex.args[0].find('No such file or directory') > -1:
    398                  return None
    399              log.exception("Failed while generating current values")
    400              raise
    401
    402
    403 -    def rrdcmd(self, gopts, ftype='PNG'):
    404          """
    405          Generate the RRD command using the graphing options specified.
    406
    407          @param gopts: RRD graphing options
    408          @param ftype: graphic file type (eg PNG)
    409          @return: RRD command usable on the command-line
    410          @rtype: string
    411          """
    412          filename, gopts = self._setfile(gopts, ftype)
    413          return "rrdtool graph " + " ".join(gopts)
    414
    415
    416 -    def graphId(self, gopts, drange, ftype):
    417          """
    418          Generate a graph id based on a hash of values
    419
    420          @param gopts: RRD graphing options
    421          @param drange: min/max values of the graph
    422          @param ftype: graphic file's type (eg PNG)
    423          @return: An id for this graph usable in URLs
    424          @rtype: string
    425          """
    426          import md5
    427          id = md5.new(''.join(gopts)).hexdigest()
    428          id += str(drange) + '.' + ftype.lower()
    429          return id
    430
    431 -    def _loadfile(self, filename):
    432          try:
    433              f = open(filename)
    434              graph = f.read()
    435              f.close()
    436              return graph
    437          except IOError:
    438              log.info("File: %s not created yet." % filename);
    439              return None
    440
    441
    442 -    def setupCache(self):
    443          """
    444          Make a new cache if we need one
    445          """
    446          if not hasattr(self, '_v_cache') or not self._v_cache:
    447              tmpfolder = self.getPhysicalRoot().temp_folder
    448              if not hasattr(tmpfolder, self.cacheName):
    449                  cache = PObjectCache(self.cacheName, self.cachetimeout)
    450                  tmpfolder._setObject(self.cacheName, cache)
    451              self._v_cache = tmpfolder._getOb(self.cacheName)
    452          return self._v_cache
    453
    454
    455 -    def addGraph(self, id, filename):
    456          """
    457          Add a graph to temporary folder
    458
    459          @param id: graph id
    460          @param filename: cacheable graphic file
    461          """
    462          cache = self.setupCache()
    463          graph = self._loadfile(filename)
    464          if graph:
    465              cache.addToCache(id, graph)
    466              try:
    467                  os.remove(filename)
    468              except OSError, e:
    469                  if e.errno == 2:
    470                      log.debug("Unable to remove cached graph %s: %s" \
    471                          % (e.strerror, e.filename))
    472                  else:
    473                      raise e
    474          cache.cleanCache()
    475
    476
    477 -    def getGraph(self, id, ftype, REQUEST):
    478          """
    479          Get a previously generated graph
    480
    481          @param id: graph id
    482          @param ftype: file type of graphic (eg PNG)
    483          @param REQUEST: graph id
    484          """
    485          cache = self.setupCache()
    486          ftype = ftype.lower()
    487
    488          if REQUEST:
    489              mimetype = mimetypes.guess_type('%s.%s' % (id, ftype))[0]
    490              if mimetype is None:
    491                  mimetype = 'image/%s' % ftype
    492              response = REQUEST.RESPONSE
    493              response.setHeader('Content-Type', mimetype)
    494
    495          return cache.checkCache(id)
    496
    497
    498  InitializeClass(RenderServer)
    499