1
2
3
4
5
6
7
8
9 try: _
10 except NameError: _ = str
11
12 import os, traceback
13 from datetime import datetime, timedelta
14 from twisted.python import log
15 from twisted.internet import defer
16 from twisted.web.microdom import *
17
18 from twisted.persisted import aot
19
20 from zephir.monitor.agentmanager import config as cfg, rrd, status, util
21 from zephir.monitor.agentmanager.data import *
22
23
24
26 """Persistance et accès aux données d'un agent.
27
28 Cette classe contient la « charge utile » d'un agent ; avant envoi
29 au serveur Zephir tous les agents sont convertis en une instance
30 de C{L{AgentData}}.
31
32 Attributs :
33
34 - C{name} : nom de l'agent
35
36 - C{period} : intervalle de mesures en secondes ; C{period=0}
37 désactive les mesures automatiques.
38
39 - C{description} : description de cette instance d'agent,
40 apparaissant sur la page de l'agent.
41
42 - C{max_status} : L{status<zephir.monitor.agentmanager.status>} le plus grave ; réinitialisé à chaque
43 envoi au serveur Zephir ou sur demande via XML-RPC.
44
45 - C{last_measure_date} : date de la dernière mesure.
46
47 - C{data} : liste d'objets représentant les données associées à
48 l'agent (cf. C{L{zephir.monitor.agentmanager.data}}).
49 """
50
51 - def __init__(self, name, period, description, section,
52 max_status, max_status_date, last_status, last_measure_date, data, measure_data={}):
53 self.name = name
54 self.period = period
55 self.description = description
56 self.section = section
57 self.max_status = max_status
58 self.last_status = last_status
59 self.max_status_date = max_status_date
60 self.last_measure_date = last_measure_date
61 self.data = data
62 self.measure_data = measure_data
63
64
66 """I{Factory Method}
67
68 @param agent: un agent concret
69
70 @return: une copie de C{agent} ne contenant plus que les
71 données utiles au serveur Zephir (instance de C{AgentData}
72 """
73 result = self(agent.name, agent.period, agent.description, agent.section,
74 agent.max_status, agent.max_status_date, agent.last_status, agent.last_measure_date, agent.data, agent.measure_data)
75 return result
76 from_agent = classmethod(from_agent)
77
78
80 """I{Factory Method}
81
82 @param archive_dir: le chemin d'un répertoire contenant les
83 données d'un agent
84
85 @return: une instance de C{AgentData} chargée depuis le
86 système de fichiers
87 """
88 xml_filename = os.path.join(archive_dir, 'agent.xml')
89 xml_file = file(xml_filename, 'r')
90 try:
91
92 try:
93 result = aot.unjellyFromSource(xml_file)
94 except:
95 xml_file.seek(0)
96 from twisted.persisted import marmalade
97 result = marmalade.unjellyFromXML(xml_file)
98 except Exception, e:
99 error_agent = LoadErrorAgent(
100 os.path.basename(archive_dir),
101 title = _("Loading archive %s failed") % archive_dir,
102 message = _("Loading agent from archive %s failed:\n%s") % (archive_dir, e))
103 result = AgentData.from_agent(error_agent)
104 xml_file.close()
105 return result
106 from_archive = classmethod(from_archive)
107
108
110 """Sérialise l'agent sur disque, cf. L{from_archive}"""
111 xml_filename = os.path.join(archive_dir, 'agent.xml')
112 xml_file = file(xml_filename, 'w')
113
114 aot.jellyToSource(self, xml_file)
115
116 xml_file.close()
117
119 """Met à jour les données de l'agent sur disque"""
120 pass
121
122
123
124
125 STATUS_GRAPH_OPTIONS = [
126 "-send-7days", "-l0", "-u1", "-g",
127 "-w112", "-h10", "-xHOUR:6:DAY:1:DAY:1:0:%d",
128 "CDEF:unknown=status,0,EQ",
129 "CDEF:ok=status,1,EQ",
130 "CDEF:warn=status,2,EQ",
131 "CDEF:error=status,3,EQ",
132 "AREA:unknown#666666",
133 "AREA:ok#33BB33",
134 "AREA:warn#CC6600",
135 "AREA:error#BB3333",
136 ]
137
138 STATUS_GRAPH_OPTIONS_MONTHLY = [
139 "-send-1month", "-l0", "-u1", "-g",
140 "-w300", "-h10", "-xDAY:1:WEEK:1:DAY:7:0:%d",
141 "CDEF:unknown=status,0,EQ",
142 "CDEF:ok=status,1,EQ",
143 "CDEF:warn=status,2,EQ",
144 "CDEF:error=status,3,EQ",
145 "AREA:unknown#666666",
146 "AREA:ok#33BB33",
147 "AREA:warn#CC6600",
148 "AREA:error#BB3333",
149 ]
150
151
153 """Classe abstraite des agents.
154
155 Un agent concret est une sous-classe d'C{L{Agent}} implémentant
156 (en particulier) la méthode C{L{measure()}}.
157 """
158
159 - def __init__(self, name,
160 period = 60,
161 fields = ['value'],
162 description = None,
163 section = None,
164 modules = None,
165 requires = [],
166 **params):
167 if description is None:
168 description = self.__class__.__doc__
169 AgentData.__init__(self, name, period, description, section,
170 status.Unknown(), "", status.Unknown(), "", [], {})
171 self.fields = fields
172 self.data_needs_update = False
173
174 self.archive_dir = None
175 self.status_rrd = None
176 self.manager = None
177
178 self.requires = requires
179 self.modules = modules
180 self.last_measure = None
181
183 """Mémorise et initialise le répertoire d'archivage
184
185 Cette méthode sera appelée par le framework après chargement
186 de l'agent, afin de terminer les initialisations pour
187 lesquelles l'agent a besoin de connaître l'emplacement de ses
188 données sur disque.
189 """
190 self.archive_dir = archive_dir
191 self.ensure_datadirs()
192 if self.period != 0:
193 status_period, xff = self.period, 0.75
194 else:
195 status_period, xff = 60, 1
196
197
198 xml_file = os.path.join(self.archive_dir, 'agent.xml')
199 if os.path.exists(xml_file):
200 try:
201 a = AgentData("temp", 60, "agent temporaire",status.Unknown(), "", status.Unknown(), "", [], {})
202 a = a.from_archive(self.archive_dir)
203 self.max_status = a.max_status
204 self.max_status_date = a.max_status_date
205 del a
206 except:
207 pass
208
209 statusname = os.path.join(self.archive_dir, 'status')
210 self.status_rrd = rrd.Database(statusname + '.rrd',
211 step = status_period)
212 self.status_rrd.new_datasource(
213 name = "status",
214 min_bound = 0, max_bound = len(status.STATUS_ORDER))
215 self.status_rrd.new_archive(
216 rows = 24*7, steps = 3600/status_period,
217 consolidation = 'MAX', xfiles_factor = 0.75)
218 self.status_rrd.new_archive(
219 rows = 24*3600/status_period, steps = 1,
220 consolidation = 'MAX', xfiles_factor = 0.75)
221 self.status_rrd.new_graph(pngname = statusname + '.png',
222 vnamedefs = { "status": ("status", 'MAX') },
223 options = STATUS_GRAPH_OPTIONS)
224 self.status_rrd.new_graph(pngname = statusname + '_long.png',
225 vnamedefs = { "status": ("status", 'MAX') },
226 options = STATUS_GRAPH_OPTIONS_MONTHLY)
227 self.status_rrd.create()
228
229
230
231
233 """Prend concrètement une mesure.
234
235 Pour implémenter un agent, il faut implémenter au moins cette
236 méthode.
237
238 @return: Résultat de la mesure, un dictionnaire C{{champ:
239 valeur}} ou un objet C{L{twisted.internet.defer.Deferred}}
240 renvoyant ce dictionnaire.
241 """
242 raise NotImplementedError
243
244
246 """Renvoie le diagnostic de fonctionnement de l'agent.
247
248 L'implémentation par défaut dans C{L{Agent}} renvoie un statut
249 neutre. Les agents concrets doivent donc redéfinir cette
250 méthode pour annoncer un diagnostic utile.
251 """
252 log.msg(_("Warning: agent class %s does not redefine method check_status()")
253 % self.__class__.__name__)
254 return status.Unknown()
255
257 new_status = None
258
259 if self.manager is not None:
260 for agent in self.requires:
261 try:
262 if self.manager.agents[agent].last_status == status.Error():
263 new_status = status.Dependant()
264 except:
265
266 pass
267 if new_status is None:
268 new_status = self.check_status()
269
270 self.set_status(new_status)
271
273 """Mémorise le statut et met à jour C{statut_max}
274
275 @param s: statut actuel
276 @param reset: réinitialise C{max_status} à C{s} si C{reset==True}
277 """
278
279 if s > self.last_status and s not in [status.OK(),status.Dependant()]:
280 self.max_status = s
281 self.max_status_date = self.last_measure_date
282 self.last_status = s
283
285 """Réinitialise C{max_status} à la valeur courante du status
286 """
287 self.set_status(self.check_status(), reset = True)
288
289
290
292 """Déclenche une mesure programmée.
293
294 Prend une mesure et mémorise le résultat et l'heure.
295 """
296 now = util.utcnow()
297 try:
298 m = self.measure()
299 except Exception, e:
300 print "erreur mesure (%s)" % self.name
301 traceback.print_exc()
302 self.handle_measure_exception(e)
303 else:
304 if isinstance(m, defer.Deferred):
305 m.addCallbacks(
306 lambda m: self.save_measure(Measure(now, m)),
307 lambda f: self.handle_measure_exception(f.trap(Exception)))
308 else:
309 self.save_measure(Measure(now, m))
310
311
313 """Mémorise une mesure donnée.
314
315 Méthode à redéfinir dans les sous-classes concrètes de C{L{Agent}}.
316 (callback de succès pour C{L{scheduled_measure()}})
317 """
318
319
320 self.last_measure_date = measure.get_strdate()
321 self.data_needs_update = True
322 self.last_measure = measure
323 self.status_rrd.update({'status': self.last_status.num_level()},
324 util.utcnow())
325
326
328 """Callback d'erreur pour C{L{scheduled_measure()}}
329 """
330 log.msg(_("/!\ Agent %s, exception during measure: %s")
331 % (self.name, str(exc)))
332 self.set_status(status.Error(str(exc)))
333
334
340
341
352
354 """Écrit les données générées par l'agent sur disque
355
356 Méthode à redéfinir si nécessaire dans les sous-classes.
357 """
358 additional_args = []
359 this_morning = util.utcnow().replace(hour = 0, minute = 0, second = 0, microsecond = 0)
360 for i in range(7):
361 t = this_morning - i*timedelta(days = 1)
362 additional_args += "VRULE:%s#000000" % rrd.rrd_date_from_datetime(t)
363 self.status_rrd.graph_all()
364
365
367 """Méthode de convenance, cf C{L{zephir.monitor.agentmanager.util.ensure_dir}}
368 """
369 assert self.archive_dir is not None
370 util.ensure_dirs(self.archive_dir)
371
372
374 """Agent concret mémorisant ses mesures dans une table.
375
376 Les valeurs mesurées peuvent être non-numériques.
377 """
378
379 - def __init__(self, name,
380 max_measures=100,
381 **params):
382 """
383 @param max_measures: nombre maximal de mesures mémorisées
384 """
385 Agent.__init__(self, name, **params)
386 assert max_measures >= 1
387 self.max_measures = max_measures
388 self.measures = []
389 self.data = [MeasuresData(self.measures, self.fields)]
390
391
393 """Maintient la table de mesures triée et en dessous de la taille
394 maximale (cf. C{Agent.save_measure}).
395 """
396 Agent.save_measure(self, measure)
397
398
399 for x in range(max(0, len(self.measures) - self.max_measures +1)):
400 self.measures.pop(0)
401 self.measures.append(measure)
402 self.measures.sort()
403
404
405
406
408 """Classe abstraite pour les agents utilisant RRDtool.
409
410 Les valeurs mesurées étant visualisées sous forme de graphes,
411 elles doivent être numériques.
412
413 Les agents de cette classe maintiennent plusieurs bases de données RRD et
414 génèrent des graphes au format PNG de leurs données.
415 """
416
417 - def __init__(self, name,
418 datasources, archives, graphs,
419 **params):
420 """
421 Les paramètres C{datasources}, C{archives} et C{graphs} sont
422 des listes de paramètres pour la configuration d'une L{base
423 RRD<agentmanager.rrd.Database>}.
424 """
425 Agent.__init__(self, name, **params)
426 self.datasources = datasources
427 self.archives = archives
428 self.graphs = graphs
429 self.data = []
430
431
433 """Crée et initialise la base RRD dans C{archive_dir}.
434 """
435 Agent.init_data(self, archive_dir)
436 self.rrd = {}
437 arch_names = self.archives.keys()
438 arch_names.sort()
439 for name in arch_names:
440 rrdname = name + '.rrd'
441 self.rrd[name] = rrd.Database(os.path.join(self.archive_dir, rrdname),
442 step = self.period)
443 self.data.append(RRDFileData(rrdname))
444 for ds in self.datasources[name]: self.rrd[name].new_datasource(**ds)
445 for rra in self.archives[name]: self.rrd[name].new_archive(**rra)
446 for g in self.graphs[name]:
447 self.data.append(ImgFileData(g['pngname']))
448 self.data.append(HTMLData('<br>'))
449 g['pngname'] = os.path.join(self.archive_dir, g['pngname'])
450 self.rrd[name].new_graph(**g)
451 self.rrd[name].create()
452
463
468
469
471 """Classe abstraite pour les agents utilisant RRDtool.
472
473 Les valeurs mesurées étant visualisées sous forme de graphes,
474 elles doivent être numériques.
475
476 Les agents de cette classe maintiennent une base de données RRD et
477 génèrent des graphes au format PNG de leurs données.
478 """
479
480 - def __init__(self, name,
481 datasources, archives, graphs,
482 **params):
483 """
484 Les paramètres C{datasources}, C{archives} et C{graphs} sont
485 des listes de paramètres pour la configuration d'une L{base
486 RRD<agentmanager.rrd.Database>}.
487 """
488 Agent.__init__(self, name, **params)
489 self.datasources = datasources
490 self.archives = archives
491 self.graphs = graphs
492 self.data = []
493
494
496 """Crée et initialise la base RRD dans C{archive_dir}.
497 """
498 Agent.init_data(self, archive_dir)
499 rrdname = self.name + '.rrd'
500 self.rrd = rrd.Database(os.path.join(self.archive_dir, rrdname),
501 step = self.period)
502 self.data.append(RRDFileData(rrdname))
503 for ds in self.datasources: self.rrd.new_datasource(**ds)
504 for rra in self.archives: self.rrd.new_archive(**rra)
505 for g in self.graphs:
506 self.data.append(ImgFileData(g['pngname']))
507 self.data.append(HTMLData('<br>'))
508 g['pngname'] = os.path.join(self.archive_dir, g['pngname'])
509 self.rrd.new_graph(**g)
510 self.rrd.create()
511
512
521
522
526
527
528
529
531 """Pseudo-agent représentant une erreur de chargement d'un fichier
532 de configuration.
533 """
534
535 HTML = """<p class="error">%s</p>"""
536
537 - def __init__(self, name,
538 title=_("Error while loading agent"),
539 message="",
540 **params):
546
548 return self.max_status()
549
550
551
552
553
554
555
556
557