1
2
3
4
5
6
7
8
9
10
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:
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
66
67
68 addRenderServer = DTMLFile('dtml/addRenderServer',globals())
69
70
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
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
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
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
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
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
271 params = urllib.urlencode({'tarfilename': tarfilename,
272 'tarfile':tarfilebody})
273
274
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')
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')
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')
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
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
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
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