Date: 2005-01-14

# 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
- Installer et configurer un agent
  - Installer les modules Python d'une classe d'agent
  - Instancier et configurer des agents concrets
  - Paramètres standard
- Écrire un agent
  - Paramétrage
  - Prise de mesure
  - Diagnostic de l'état de l'agent
  - Mémorisation des mesures
  - Archivage des mesures

# 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