Agents de supervision Zéphir

Installer, configurer et développer des agents

Author:Damien Pollet
Date: 2005-01-14
Version: 1.9

Sommaire

Présentation

Pour superviser l'état des machines d'un réseau, on déploie des agents sur chacune des machines à surveiller. Chaque agent va mesurer à intervalles réguliers un aspect de la machine sur laquelle il est installé. Les données locales ainsi collectées sont ensuite envoyées au serveur Zéphir qui centralise les informations provenant de tout un parc de machines.

Sur chaque machine, un service planifie et déclenche les mesures aux intervalles définis pour chaque agent. Ce service publie au fur et à mesure les résultats sur un site web dynamique local, et envoie régulièrement une archive des mesures vers le serveur Zéphir. Finalement, on peut contrôler l'état du service et des agents qu'il a chargés, ou déclencher des mesures à des moments quelconques via une interface XML-RPC.

Installer et configurer un agent

Installer les modules Python d'une classe d'agent

Un agent est un objet Python dont la classe définit le type de mesure qu'il fait (e.g. le débit d'une interface réseau). Les modules définissant la classe d'un agent doivent donc être accessibles à l'interprète Python. Ces modules peuvent être installés comme tout module Python, soit en les déposant dans l'arborescence de la distribution Python, soit en modifiant la variable d'environnement $PYTHONPATH.

Instancier et configurer des agents concrets

Chaque classe d'agent peut être instanciée autant de fois que nécessaire pour créer des agents avec différentes configurations (e.g. superviser telle ou telle interface).

Quand il démarre, le service consulte tous les fichiers *.agent qu'il trouve dans son répertoire de configurations (cf. option --config du service). Chacun de ces fichiers crée et configure un nombre quelconque d'agents que le service va charger. Les agents peuvent ainsi être regroupés thématiquement ou en fonction d'un paquet dont ils dépendent.

Ces fichiers d'instanciation *.agent contiennent les quelques déclarations en langage Python nécessaires pour instancier les agents et les ajouter à la liste dans la variable AGENTS. Un fichier d'instanciation typique a la structure suivante :

# -*- mode: Python; coding: utf-8 -*-
"""
Paragraphe de documentation de ce groupe d'agents
"""   

from agents.ifupdown import Ifupdown

agent_eth0 = Ifupdown("eth0", device="/dev/eth0")
agent_eth1 = Ifupdown("eth1", period=1,
                     device="/dev/eth1",
                     description = """Interface vers un autre réseau.""")

AGENTS = [agent_eth0, agent_eth1]

Ce fichier instancie deux agents nommés eth0 et eth1 de type Ifupdown. Le nom est le seul paramètre obligatoire des agents. D'autres paramètres tels que la période de répétition des mesures ou la description sont facultatifs mais valables pour tout agent. Finalement chaque classe d'agents peut définir des paramètres spécifiques, comme ici device qui n'est à priori reconnu que par Ifupdown.

Le nom des variables intermédiaires n'a pas d'importance, tout ce qui importe au service est la valeur de la variable AGENTS, qui doit être une liste d'agents.

Paramètres standard

Tous les agents reconnaissent les arguments génériques décrits ci-dessous. Cependant les valeurs par défaut peuvent varier d'une classe d'agent à une autre.

Le premier paramètre du constructeur est obligatoire, il s'agit du nom de l'agent.

name
Ce nom sert à identifier l'agent sur le site web, dans l'interface XML-RPC, etc ; il doit donc être unique pour une machine donnée.

Tous les autres paramètres sont nommés (syntaxe nom=valeur) ; ils peuvent donc être spécifiés dans un ordre quelconque, voire omis. Les classes d'agents reconnaissent certains de ces arguments et propagent les autres à leurs super-classes. La super-classe principale Agent reconnaît les arguments ci dessous.

period
Durée en secondes de l'intervalle entre deux mesures. Les mesures déclenchées via l'interface XML-RPC n'interfèrent pas avec le cycle des mesures programmées.
description
Chaîne de documentation de cette instance de l'agent.

Écrire un agent

Pour définir chaque nouveau type de mesure, il faut écrire une classe d'agents. Pour cela le framework fournit des classes de base : Agent et ses sous-classes RRDAgent et TableAgent (cf. Mémorisation des mesures). La classe du nouvel agent définira au moins les méthodes de mesure et de diagnostic décrites ci-dessous.

Paramétrage

L'agent est paramétré une fois pour toutes lors de son instanciation. Le constructeur __init__() sera donc redéfini au besoin. Pour que les paramètres génériques soient traités, il faut appeler le constructeur de la classe mère en propageant les arguments nommés :

from agentmanager.agent import Agent
from agentmanager import status

class ExampleAgent(Agent):

   def __init__(self, name,
                some_parameter="default value",
               **params):
      # don't forget that or superclasses won't see their params
      Agent.__init__(self, name, **params)
      do_something_with(some_parameter)

Prise de mesure

La procédure pratique de mesure doit être implémentée en définissant la méthode measure(). Cette méthode sera appelée à chaque fois que le framework décide de déclencher une mesure. La méthode measure() doit renvoyer un dictionnaire {'champ': valeur}, avec la chaîne 'value' comme champ par défaut :

def measure(self):
   return {'value': 42} # dictionary {field: value}

Certains agents ont besoin d'exécuter des commandes externes pour effectuer leur mesure. Comme le framework utilise TwistedMatrix il faut utiliser l'appel non bloquant twisted.internet.utils.getProcessOutput(), qui renvoie un objet de la classe Deferred. Dans ce cas, l'agent doit traiter le résultat de la commande dans un callback et renvoyer sans attendre l'objet deferred :

def measure(self):
   cmd = getProcessOutput('/bin/commande/externe')
   cmd.addCallback(self.process_cmd_result)
   return cmd

def process_cmd_result(self, cmd_result):
   # build the measure_dictionary from cmd_result
   return measure_dictionary

Le framework attend donc de measure() qu'elle renvoie un dictionnaire {'champ': valeur}, directement ou via un objet deferred.

Diagnostic de l'état de l'agent

L'agent doit indiquer s'il fonctionne de manière nominale ou dans un mode dégradé (e.g. impossible de prendre tout ou partie de la mesure, valeurs incohérentes). La méthode check_status() peut être redéfinie avec les heuristiques spécifiques à la classe d'agent pour renvoyer un niveau de fiabilité (voir la classe agentmanager.status.Status) :

def check_status(self):
   if self.looks_broken_heuristic():
      return status.Error("Looks like I'm broken")
   else:
      return status.OK() # or .warn() or .unknown()...

Mémorisation des mesures

Le framework mémorise automatiquement toutes les mesures programmées, pour les archiver vers le serveur Zéphir. La mémorisation dépend du format des résultats de mesures; elle est donc spécifique à chaque classe d'agents. La méthode save_measure() définit la mémorisation d'une mesure donnée. Son implémentation dans la classe Agent ne fait rien et devrait donc être redéfinie en même temps que measure(); cependant le framework fournit deux méthodes de mémorisation dans deux sous-classes d'Agent: RRDAgent et TableAgent.

RRDAgent mémorise les mesures en utilisant la suite d'outils RRDtool, adaptée aux mesures numériques et pouvant générer des graphes. TableAgent peut au contraire mémoriser des mesures d'un type quelconque, mais n'a pas les possibilités de synthèse de RRDAgent. En pratique, il suffit donc de définir la nouvelle classe d'agent en spécialisant soit RRDAgent soit TableAgent plutôt que directement Agent.

Pour mémoriser les mesures de manière spécifique, on peut redéfinir save_measure() ; par exemple pour un agent gardant seulement la dernière valeur :

def save_measure(self, measure):
    Agent.save_measure(self, measure)
    self.last_measure = measure

Archivage des mesures

Si l'agent génère des données autres que celles définies par sa super-classe, il doit d'une part archiver ces données au moment opportun, et d'autre part déclarer le format de ces données au framework.

Chaque agent dispose d'un répertoire dans lequel il peut archiver ses données et qui sera envoyé au serveur Zéphir. Ce répertoire contiendra au moins un fichier agent.xml généré automatiquement par la classe Agent. Ce fichier contient la description de l'agent: nom, statut, description, mais surtout la liste des données archivées par l'agent.

Cette liste de données est spécifiée par l'attribut data de la classe Agent, qu'il faut donc initialiser dans le constructeur. Cet attribut contient une liste d'objets des classes suivantes:

FileData
Un fichier présent dans le répertoire de données de l'agent, désigné par son chemin relatif.
ImgFileData
Sous-classe de FileData représentant une image, qui sera insérée dans la page web de l'agent au lieu d'être simplement liée.
RRDFileData
Sous-classe de FileData représentant une archive RRDtool. Pour instancier cette classe, il faut spécifier le format des archives .rrd et des graphes ; pour cela, se reporter à la documentation de RRDtool, en particulier les commandes rrdtool create et rrdtool graph.
HTMLData
Un extrait de code HTML, du niveau d'un paragraphe ou d'un bloc <div>.
MeasuresData
Une table (liste Python) de mesures, telle que générée par un agent issu de TableAgent.

L'archivage proprement dit est déclenché à la demande du framework qui appellera pour cela la méthode write_data() de l'agent. Cette méthode peut être étendue par redéfinition pour écrire des fichiers supplémentaires spéciques à l'agent; par exemple les sous-classes de RRDAgent stockent l'archive de mesures au format .rrd ainsi qu'un graphe de ces mêmes mesures au format PNG. De même, les agents générant des données HTML implémenteront write_data() de la manière suivante :

def __init__(self, name,
             **params):
   # ... initializations ...
   self.html = HTMLData("")
   self.data = [self.html]

def write_data(self):
   Agent.write_data(self)
   # only generate when the measured value is valid
   if self.last_measure is not None:
       html = "generated html code (template, string formatting...)"
       self.html.data = html