Sommaire

- 4) Développement avec Zephir
  - 4.1) Ecrire des agents de surveillance
  - 4.2) Ecrire des applications clientes
  - 4.2.1) Généralités
  - 4.2.2) Exemple basique de client
  - 4.2.3) Authentification ldap d'un client python
  - 4.3) Etendre les fonctions du backend
  - 4.3.1) Généralités
  - 4.3.2) les fonctions xml-rpc
  - 4.3.2) accès à la base de données
  - 4.3.2.1) via la bibliothèque de Twisted Matrix
  - 4.3.2.2) via la librairie python pyPgSQL
  - 4.3.2.3) construction des requêtes et lecture des résultats
  - 4.3.3) gestion des logs

# 4) Développement avec Zephir

## 4.1) Ecrire des agents de surveillance

Vous pouvez développer assez simplement des agents de surveillance supplémantaires pour vos serveurs. Suivez ce lien pour une documentation détaillée

## 4.2) Ecrire des applications clientes

Il est possible d'écrire des applications qui se connectent au backend de zephir et utilisent ses fonctions. Les exemples donnés ici sont codés en python, mais il est à priori possible d'utiliser n'importe quel langage ayant une librairie permettant d'effectuer des requêtes XMLRPC (non testé).

Le Lien suivant propose des information utiles pour programmer des clients XMLRPC dans différents langages:

> - XMLRPC Howto

## 4.2.1) Généralités

> L'appel aux fonctions du backend impliquent les contraintes suivantes:

> - Les chaines présentant des caractères accentués ou autres (code ASCII > 127) doivent être envoyées en unicode.
> - Les chaines retournées par le backend peuvent être encodées en unicode pour les mêmes raisons.

> Pour zephir, nous avons créé des fonctions permettant de remettre automatiquement les objets unicodes sous forme de chaines (str). Voici le code de la fonction récursive convert(objet) présente dans le fichier /usr/share/zephir/web/html/erreur.py (bibliothèque de fonctions utiles de l'application web de zephir). charset est une variable précisant l'encodage à utiliser ('ISO-8859-15' pour l'europe de l'est).

>     def convert(objet):
>             """Transforme les objets unicode contenus dans un objet en chaines
>             """
>             if type(objet) == list:
>                     l = []
>                     for item in objet:
>                             l.append(convert(item))
>                     return l
>             if type(objet) == tuple:
>                     l = []
>                     for item in objet:
>                             l.append(convert(item))
>                     return l
>             if type(objet) == dict:
>                     dico={}
>                     for cle in objet.keys():
>                             dico[cle] = convert(objet[cle])
>                     return dico
>             if type(objet) == unicode:
>                     string =  objet.encode(charset)
>                     return string
>             return objet

> fonction u(objet) qui effectue le travail inverse (pour envoi de chaines unicodes au backend):

>     def u(objet):
>             """convertit récursivement les chaines en objets unicode"""
>             if type(objet) == list:
>                     l = []
>                     for item in objet:
>                             l.append(u(item))
>                     return l
>             if type(objet) == tuple:
>                     l = []
>                     for item in objet:
>                             l.append(u(item))
>                     return l
>             if type(objet) == dict:
>                     dico={}
>                     for cle in objet.keys():
>                             dico[cle] = u(objet[cle])
>                     return dico
>             if type(objet) == str:
>                     string = unicode(objet,charset)
>                     return string
>             return objet

> Note

> Toutes les fonctions du backend utilisent la convention suivante pour leur codes de retour:

> - Réussite : [1,données ou message]
> - Echec : [0,message d'erreur]

## 4.2.2) Exemple basique de client

> La première chose à faire est de créer un "proxy" qui se chargera de transmettre les appels au backend XML-RPC. La librairie standard de python permet de le faire à l'aide du module xmlrpclib.

>     import xmlrpclib,sys,getpass
>     #on demande les paramètres de connexion à l'utilisateur
>     adresse_zephir=raw_input('adresse du serveur zephir : ')
>     login=raw_input('login pour l'application zephir : ')
>     # le module getpass permet de cacher la saisie d'un mot de passe
>     password=getpass.getpass('votre mot de passe zephir : ')
>     # mise en place du proxy
>     zephir=xmlrpclib.ServerProxy("https://%s:%s@%s:7080" % (login,password,adresse_zephir))
>     # pour vérifier l'authentification, on essaye de récupérer les préférences de l'utilisateur
>     try:
>             resultat=convert(zephir.get_user(u(login))
>     except xmlrpclib.ProtocolError:
>             sys.exit("\nerreur d'authentification\n")
>     except:
>             sys.exit("\npas de droits en lecture sur zephir\n")
>     # si on arrive ici, on est bien connecté sur zephir

> Une fois le proxy en place, on peut procéder à l'appel des fonctions distantes reportez vous à l'api pour les fonctions disponibles. Par exemple les fonctions relatives aux modules sont les fonctions du type xmlrpc_nom_fonction de la classe RPCModules située dans Module zephir.backend.modules_rpc

> Exemple d'appel à la fonction de modification d'un module:

>     try:
>             libelle = raw_input("nouveau nom du module")
>             resultat = proxy_zephir.modules.edit_module(1,u({'libelle':libelle}))
>             # vérification du retour de la fonction
>             if resultat[0] == 0:
>                     print "\nEchec du backend lors de l'édition : %s\n" % resultat[1]
>             else:
>                     print "\nLibellé du module modifié\n"
>     except xmlrpclib.ProtocolError:
>             # correspond à une erreur d'authentification
>             sys.exit("""\nVous n'êtes pas autorisé à effectuer cette action\n""")
>     except :
>             sys.exit("\nerreur lors de l'appel à zephir\n")

## 4.2.3) Authentification ldap d'un client python

> Au lieu de tenter de faire un appel au backend pour vérifier si l'utilisateur est correctement authentifié, il est possible de faire directement un essai de connexion à l'annuaire LDAP utilisé par zephir dpuis le client. Pour cela, nous utilisons la librairie python-ldap.

> voici un exemple de test d'authentification.

>     import sys, getpass
>     try:
>             import ldap
>     except:
>             sys.exit("librairie ldap non disponible")

>     # paramètres de l'annuaire LDAP
>     adresse_ldap=adresse_zephir
>     base_ldap="o=gouv,c=fr"
>     filter="(uid=%s)"

>     # saisie login, mot de passe
>     login=raw_input('login pour l'application zephir : ')
>     password=getpass.getpass('votre mot de passe zephir : ')

>     # ouverture de la connexion
>     serveur=ldap.open(adresse_ldap)
>     # on récupère le dn complet de l'utilisateur
>     result=serveur.search_s(base_ldap, ldap.SCOPE_SUBTREE, filter % login )
>     user_dn = result[0][0]
>     # test d'authentification sur l'annuaire (bind)
>     try:
>             serveur.simple_bind_s(user_dn,password)
>             serveur.unbind()
>     except:
>             sys.exit('\nerreur d'authentification\n')

## 4.3) Etendre les fonctions du backend

## 4.3.1) Généralités

> Si vous désirez étendre les fonctionnalités de base de zephir, voici quelques informations utiles:

> Les définitions des serveurs, établissements, modules, services, etc. sont stockées dans une base de données postgresql. Le schéma actuel de la base est défini dans le script sql '/etc/eole/zephir.sql'. Vous pouvez manipuler directement la base sur zephir en vous connectant en tant qu'utilisateur postgres à la console d'administration de la base

>     su postgres
>     psql zephir

>     (zephir est le nom de la base de données).

> Les données de configuration, patch, et les divers fichiers sauvegardés sur les serveurs par zephir sont stockés dans le système de fichiers suivant l'arborescence suivante:

>     ::

>             (->) : lien symbolique (<) source d'un lien

>             /var/lib/zephir/conf
>                     |---etabX
>                             |---serveurX
>                                     |---dicos
>                                             |---variante (->)
>                                     |---patchs
>                                             |---variante (->)
>                                     |---fichiers_persos
>                                             |---variante (->)
>                                     |---fichiers_zephir
>                                             |---variante (->)
>                                     |---uucp
>                                     |-auth_keys
>                                     |-cle_publique
>                                     |-dictionnaire (->)
>                                     |-dico.eol (->)
>                                     |-zephir.eol
>                             |---serveurY
>                             |---serveurZ
>                     |---etabY
>                     etc.

> De même, il existe un répertoire /var/lib/zephir/modules qui contient les données des modules:

>     ::

>             /var/lib/zephir/modules/
>                     |---Module_1
>                             |-dictionnaire (<)
>                             |---variantes
>                                     |---Variante_1
>                                             |-dico.eol (<)
>                                             |---dicos (<)
>                                             |---fichiers_persos (<)
>                                             |---fichiers_zephir (<)
>                                             |---patchs (<)
>                                     |---Variante_2
>                     |---Module_2
>                     etc.

> Explication de certains répertoires et fichiers:

> - Le répertoire dicos contient des dictionnaires locaux (propres au serveur ou à une variante).
> - fichiers_persos doit contenir les templates à destination de /etc/eole ajoutés par ces dictionnaires
> - fichiers_zephir contient divers fichiers à sauvegarder sur zephir (leur destination est stockée dans un fichier de ce répertoire (fichiers_variante ou fichiers_zephir selon qu'ils sont sécifiques à une variante ou a un serveur.
> - auth_keys reçoit les clés publiques ssh des utilisateurs autorisés à se connecter au serveur (il sera concaténé à /root/.ssh/authorized_keys sur le serveur)
> - cle_publique est la clé ssh utilisée pour la connexion uucp du serveur (à travers ssh)
> - zephir.eol est une copie sur zephir du fichier /etc/eole/config.eol du serveur

## 4.3.2) les fonctions xml-rpc

> Le 'backend' de zephir est un serveur de fonctions distantes XML-RPC en langage python. Ce serveur est une version modifiée du serveur fourni dans le framework TwistedMatrix (cf. Chapitre1)

> Le principe est le suivant :

> - Le client fait appel à une fonction du backend à travers un 'proxy' de ce serveur (il 'voit' la fonction comme s'il s'agissait d'une fonction locale)

> - Le proxy transmet le nom de la fonction et ses arguments au serveur via un flux de données en XML

> - Le serveur reçoit ces données et effectue les étapes suivantes:

>   > - lecture du nom de la fonction et des paramètres envoyés
>   > - vérification de l'authentification (ldap)
>   > - vérification des droits de l'utilisateur (base de données)
>   > - exécution de la fonction si elle est autorisée

> - Lorsqu'il rencontre une instruction 'return', le serveur renvoie le résultat au proxy du client (toujours sous la forme d'un flux XML).

> - Le proxy du client décode les données reçues et les retourne comme résultat de la fonction appelée.

> Ce mode de fonctionnement implique certaines restrictions:

> - les données transmises étant transférées sous forme de XML, elles doivent pouvoir être encodées dans ce format (par exemple, les chaines de caractères accentuées doivent être encodées en unicode avant d'être transférées, et décodées de l'autre côté (d'où l'intérêt des fonctions décrites dans le paragraphe concernant l'écriture des clients). Il est également possible de transférer le contenu de fichiers en les encodant en base64.
> - Si la fonction ne retourne rien du côté du serveur, même en cas de réussite, le client retournera une erreur du type

>     xmlrpclib.Fault: <Fault 8002: "can't serialize output">

> - Sur zephir, les fonctions définies sur le serveur prennent toujours les arguments self et cred_user comme premiers arguments. self correspond au serveur xmlrpc (plus précisément l'instance en cours de la classe représentant ce serveur), et cred_user est le nom de login de l'utilisateur qui apelle la fonction.

> Exemple de fonction xmlrpc:

>         def xmlrpc_hello_world(self,cred_user):
>         """renvoie un message de bienvenue
>         """
>     hostname = os.environ['HOSTNAME']
>     message = """Bonjour %s, bienvenue sur le serveur %s (zephir)""" % (cred_user,hostname)
>     return 1, u(message)

>         exemple d'appel à cette fonction depuis python
>         >> import xmlrpclib
>         >> zephir=xmlrpclib.ServerProxy('https://user:password@adresse_zephir:7080')
>         >> zephir.local.hello_world()

> La fonction u(chaine) équivaut à unicode(chaine,'UTF-8')

> - Pour pouvoir être utilisée, la fonction doit être listée dans au moins un des groupes de droits dans la base de données, et l'utilisateur qui l'appelle doit avoir les autorisations sur ce groupe.

>   Les groupes présents actuellement sont :

>   > - lecture : consultation des données
>   > - écriture : accès en écriture à la base de données
>   > - action sur les serveurs : actions à distance sur les serveurs (configuration, sauvegarde, ...)
>   > - gestion des permissions : gestion des droits des utilisateurs, fonctions d'administration de l'application
>   > - fonction des clients : fonction utilisées par les serveurs lorsqu'ils communiquent avec zephir (vérification des md5 des archives, ...)
>   > - export de variantes : fonction permettant de récupérer une variante depuis un zephir distant

## 4.3.2) accès à la base de données

## 4.3.2.1) via la bibliothèque de Twisted Matrix

> Les objets présents dans zephir (établissements, serveurs, groupes, modules, ...) sont stockés dans une base de données postgresql. Le backend possède un certain nombre de fonctions lui permettant d'y accéder. Le serveur xmlrpc a un attribut dbpool (self.dbpool) qui est un 'proxy' sur la base de données. Il est possible d'exécuter des requètes sql de consultation (self.dbpool.runQuery(CODE_SQL)) ou d'édition (self.dbpool.runOperation(CODE_SQL)).

> exemples de requêtes pour rechercher les données d'un module et éditer un groupe de serveurs. Ces fonctions sont à insérer dans la classe RPCLocal dans le fichier /usr/share/zephir/backend/local_rpc.py

>     from zephir.backend.db_utils import db_client_failed

>     def _got_modules(self,data):
>     """formatage des données reçues depuis la base de données
>     """
>             # création d'une liste vide
>             l=[]
>             # remplissage à partir des données reçues
>             for module in data:
>                     l.append({'id':module[0],'libelle':module[1]})
>             return 1,u(l)

>     def xmlrpc_get_module(self,cred_user, id_module=None):
>     """récupère un module précis dans la base ou tous les modules
>     """
>             if id_module :
>                     # on récupère le module demandé
>                     query = """select * from modules where id = %s""" % id_module
>             else :
>                     # sinon on renvoie tous les modules
>                     query = """select * from modules"""
>             # lancement de la requête
>             deffered=self.dbpool.runQuery(query)
>             # envoi du résultat au client
>             return deffered.addCallbacks(self._got_modules,db_client_failed)

>     def xmlrpc_edit_group(self,cred_user,id_groupe,libelle,serveurs):
>     """modifie un groupe existant
>     """
>             query = """update groupes_serveurs
>                                set libelle='%s', serveurs='%s'
>                                where id=%s"""                                           % (libelle,str(serveurs),id_groupe)

>             return self.dbpool.runOperation(query).addCallbacks(lambda x : [1,'ok'],db_client_failed)

> l'appel à self.dbpool.runQuery retourne un objet différé (voir documentation de TwistedMatrix pour plus de détails). Pour obtenir le résultat de la requête, il faut attacher une fonction 'callback' et une fonction 'errback' à cet objet et retourner l'objet au client.

> Le fonctionnement de l'objet différé est le suivant:

> - exécution de la requête sql à travers une librairie compatible adbapi (pyPgSQL)
> - si l'exécution se passe bien, l'objet exécute et retourne au client la fonction 'callback' (self._got_modules) en lui donnant comme premier argument les données renvoyées par la requête.
> - si l'exécution se passe mal, l'objet retourne au client la fonction 'errback' (db_client_failed), avec eventuellement un code d'erreur en paramètre.

> Dans le cas de l'édition de groupe, le callback est

> > lambda x : [1,'ok']

> Dans le cas ou l'édition se passe bien, le client recevra donc [1,'ok'] (lambda permet de définir une mini fonction ), car on n'attend pas de données en retour de l'exécution.

> Note

> Il est possible de passer des arguments supplémentaires à la fonction callback de la façon suivante:

>     return deffered.addCallbacks(self._got_modules,
>                              db_client_failed,
>                              callbackArgs=[argument1,argument2])

> la fonction self._got_modules doit alors être définie ainsi:

>     def _got_modules(self,data,argument1,argument2):
>             code de la fonction

## 4.3.2.2) via la librairie python pyPgSQL

> Il est aussi possible d'utiliser directement la librairie pyPgSQL (qui est masquée par les fonctions de TwistedMatrix dans la première méthode). Cette technique peut être utile dans le cas suivant:

> - vous ne voulez pas utiliser le principe de callbacks
> - vous voulez accéder directement au résultat de la requête
> - vous voulez gérer vous même la transaction avec la base (rollback, commit, ...)
> - vous voulez accéder à une autre base de données

> Cependant, cette méthode a un désavantage : La fonction devient bloquante si vous n'utilisez pas le système de callbacks, et le backend zephir sera incapable de répondre à d'autre requêtes durant l'appel à la base de données (à éviter si vous manipulez un volume de données importantes).

> De plus, l'objet self.dbpool connait déjà les paramètres de connexion de la base zephir, alors qu'ici vous devrez les récupérer vous même (ils sont stockés dans zephir.backend.config)

> Exemple de code pour récupérer l'identifiant et le rne d'un serveur:

>     # construction de la requête
>     query = """select id, rne from serveurs where id = %s""" % id_serveur
>     # connexion à la base
>     cx = PgSQL.connect(database=config.DB_NAME,user=config.DB_USER,password=config.DB_PASSWD)
>     # création d'un curseur
>     cursor=cx.cursor()
>     # exécution de la requête
>     cursor.execute(query)
>     # lecture des résultats
>     data=cursor.fetchall()
>     # fermeture du curseur et de la connexion
>     cursor.close()
>     cx.close()

## 4.3.2.3) construction des requêtes et lecture des résultats

> Les requêtes sont des requêtes SQL standard (voir la documentation de postgresql). Les données au format texte doivent être entourées par des quotes simples ('). Exemple de requête de sélection (recherche des serveurs dont le module est 1 (amon) et le libellé contient 'amon':

> query="""select id,rne,variante from serveurs where module_actuel=1 and libelle like '%amon%'"""

> % dans une chaine correspond à n'importe quelle combinaison de caractère (ou rien)

> Les données renvoyées par la librairie pyPgSQL sont des listes python constituées ainsi:

>     [
>             [champ1,champ2,champ3,...]
>             [champ1,champ2,champ3,...]
>             [...]
>     ]

>     exemple:

>     requete : """select * from modules"""

>     [[1, 'amon-1.5'], [2, 'sphynx-1.1'], [3, 'scribe-1.0'], [4, 'horus-1.0']]

## 4.3.3) gestion des logs