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