Zephir : Description technique - développement

1) le backend XML-RPC

La base de zephir est constituée d'un serveur XML-RPC basé sur la technologie TwistedMatrix (Twisted) et dialoguant avec une base de données postgresql (sur zephir). L'accès aux fonctions du serveur sont protégées par une authentification sur un serveur LDAP (annuaire local fourni avec Zephir ou tout autre annuaire accessible). Accès à l'api du serveur xml-rpc.

1.1) Organisation des méthodes du serveur

Le backend est réparti sur plusieurs fichiers. chacun de ces fichiers correspond à une classe qui comporte un certain nombre de fonctions du backend. Chaque classe donnera une 'branche dans le serveur'.

Ces classes dérivent toutes de la classe XMLRPCEole, qui est une version authentifiée de la classe XMLRPC de TwistedMatrix (voir le paragraphe suivant pour les modifications approtées).

Les fonctions ont été réparties de la façon suivante:

  • users_rpc.py: classe racine du serveur à laquelle sont rattachées toutes les autres. Gère les fonctions telles que la gestion des préférences utilisateurs, la gestion des droits, ...
  • etabs_rpc.py: fonctions de gestion des établissements
  • modules_rpc.py: fonctions de gestion des modules et des variantes
  • serveurs_rpc.py: fonctions de gestion des serveurs et groupes de serveurs
  • services_rpc.py: fonctions de gestion des services (non utilisé actuellement)
  • uucp_rpc.py: fonctions diverses de communication entre zephir et les clients
  • local_rpc.py: classe vide pour l'ajout de fonctions (contributions)

Pour le détail des fonctions présentes dans chaque classe, se reporter à l'api du backend

méthode d'appel distant au backend (avec python). Exemple d'appel de la fonction get_serveur (définie dans etab_rpc.py):

>>> import xmlrpclib
>>> zephir=xmlrpclib.ServerProxy('https://user:passwd@adresse_zephir:7080')
>>> retour=zephir.serveurs.get_serveur(1)
>>> print retour
[1, [{'libelle': 'amon-1.5', 'id': 1}]]

1.2) Authentification LDAP pour le serveur XML-RPC

La méthode retenue pour sécuriser xml-rpc est d'authentifier l'utilisateur par l'intermédiaire des requêtes http. Par exemple, la connexion au serveur se fera par l'appel suivant :

https://toto:password@192.168.230.63:7080/

Dans cet exemple, la librairie http va se charger de crypter le login et le mot de passe. du côté du serveur, les valeurs corespondantes peuvent être récupérées à travers l'objet requête comme suit :

cred_user = request.getUser()
cred_password = request.getPassword()

La solution actuelle consiste à redéfinir les fonctions render() et __init__() de la classe XMLRPC de twisted (fichier /usr/lib/pythonX.X/site-package/twisted/web/xmlrpc). Dans la fonction __init__, on va définir les utilisateurs permis et leurs mot de passe (la version actuelle va chercher les mots de passes dans un annuaire ldap).

On définit également des groupes de fonctions autorisés ou non (stockés dans la base de données) qui définissent quelles fonctions l'utilisateur a le droit d'exécuter sur le serveur.

1.3) Vérification de l'authentification et des autorisations

Tous les utilisateurs présents dans l'annuaire peuvent être utilisés, mais il faut trouver un moyen de les vérifier et d'empêcher l'exécution de code non autorisé.

l'implémentation de ces fonctions se fait au niveau de la fonction render() du serveur, qui est la fonction qui exécute la fonction demandée et retourne le résultat à l'utilisateur. Comme la requête HTTP est accessible dans cette fonction, il suffit de récupérer le login et le mot de passe envoyés dans cette requête et d'effectuer une connexion authentifiée (bind) à l'annuaire.

Si la fonction échoue c'est que le mot de passe est mauvais. Dans le cas ou le nom de login et le mot de passe fournis sont corrects, on peut récupérer le nom de la fonction demandée par l'utilisateur via la fonction suivante:

arguments, nom_fonction = xmlrpclib.loads(request.content.read())

Une fois le nom de la fonction récupérée, on regarde si la fonction est dans les groupes autorisés pour cet utilisateur

# on vérifie si l'utilisateur a le droit d'utiliser cette fonction
# on récupère les groupes de droits de l'utilisateur
cx = PgSQL.connect(database=config.DB_NAME,user=config.DB_USER,password=config.DB_PASSWD)
cursor=cx.cursor()
cursor.execute("""select droits from users where login='%s'""" % cred_user)
rs = cursor.fetchone()
cursor.close()
droits = []
# on rassemble toutes les fonctions auxquelles on a droit
if rs == []:
        groupe = []
else:
        for groupe in eval(rs[0]):
                droits.extend(self.groupes[groupe][1])
try:
        # on regarde si on a le droit d'executer la fonction
        if functionPath not in droits:
                # fonction interdite
                print "\nutilisation de la fonction %s interdite pour %s (%s)"
                                 % (functionPath,cred_user,request.getHost()[1])
                errpage = error.ErrorPage(http.UNAUTHORIZED,
                                          "Unauthorized",
                                          "erreur,ressource %s non autorisée !" % (request.uri))
                return errpage.render(request)
except:
        print "\n pas d'autorisations pour " + cred_user + " !" 
        errpage = error.ErrorPage(http.UNAUTHORIZED,
                                  "Unauthorized",
                                  "erreur, ressource %s non autorisée !" % (request.uri))
        return errpage.render(request)
# fonction autorisée
try:
        function = self._getFunction(functionPath)
except xmlrpc.NoSuchFunction:
        self._cbRender(
                xmlrpclib.Fault(self.NOT_FOUND, "no such function %s" % functionPath),
                request
        )
else:
        request.setHeader("content-type", "text/xml")
        defer.maybeDeferred(function, cred_user, *args).addErrback(
                self._ebRender
        ).addCallback(
                self._cbRender, request
        )
return server.NOT_DONE_YET

Les droits sont stockés dans la base de données postgresql. Chaque utilisateur de l'application est rattaché à un ou plusieurs groupes de droits qui sont affectés par l'administrateur de zephir. Ces groupes contiennent la liste des noms de fonctions du backend qui leur appartiennent (par exemple, 'serveur.get_serveur' et 'get_user' font partie des droits en lecture). les groupes de droits originaux sont dans le fichier '/etc/eole/zephir.sql', et sont insérés dans la base à l'instanciation de zephir (création des données de base).

1.4) Outils divers du backend

  • Les codes d'erreurs

    XML-RPC ne gère qu'une exception générique. De manière à récupérer les différents types d'exception, l'api de communiquation avec le bakend renvoie en premier des codes d'erreur, en général : .. pycode:

    [0,[exception],[msg]]: en cas d'erreur 
    [1, [resultat]]: si tout va bien
    

    Contrairement au status codes unix, bien remarquer qu'ici la convention est 0 pour un code d'erreur, et 1 pour un code retour normal. Ce choix est dû au fait qu'en python, 0 est aussi un booléen équivalent à False. Exception est ici un message du type 'libpq.OperationalError' ou 'libpq.IntegrityError', qui est ensuite convertit dans le frontend en DatabaseError. Si rien n'est renvoyé, le frontend génère une BackendError. msg est le message renvoyé par la base de donnée, de manière, côté frontend, à récupérer le nom de la table qui a provoqué l'erreur.

  • Conversions Unicode/UTF-8

    Le backend encode les paramètres des méthodes invoquées, ainsi que le retour, le résultat des méthodes, en UTF8. Pour pouvoir localiser les données (en encoding UTF-8), il y a des deux côtés des convertisseurs. Côté frontend, pour envoyer des paramètres de méthode xml-rpc, il faut toujours les faire passer par u(), qui convertit en UTF8. La fonction convert() a l'effet inverse, elle permet donc de convertir les données transitant via xml-rpc en encoding utf-8.

  • Gestion du spool Uucp

    Une librairie est disponible pour gérer la file d'attente Uucp. Elle permet de: - connaitre la liste de commandes et de fichiers en attente de transfert pour un serveur (list_files,list_cmd) - ajouter des commandes ou fichiers à la file d'attente (add_cmd,add_file) - supprimer des commandes ou fichiers en attente (remove_cmd) - purger entièrement la file d'attente d'une liste de serveurs (flush)

    cette librairie est /usr/share/zephir/backend/uucp_utils.py

    exemple d'utilisation (Télécharger):

    from zephir.backend.uucp_utils import uucp_pool, UUCPError, COMMANDS
    import sys,time
    
    # id_uucp correspond à l'identifiant du serveur dans la conf uucp
    # (dans zephir : numero_rne-identifiant_serveur)
    id_uucp = "0210001-1"
    
    try:
            # ajout d'un fichier
            print "ajout fichier ..."
            id_file = uucp_pool.add_file(id_uucp,"/tmp/config.rpt")
    
            # ajout d'une commande
            print "ajout de 2 commandes ..."
            id_cmd1 = uucp_pool.add_cmd(id_uucp,"zephir_client configure")
            id_cmd2 = uucp_pool.add_cmd(id_uucp,"zephir_client reconfigure")
            
            # affichage de la file d'attente de ce serveur (on met un petit temps d'attente
            # pour être sur que la commande est bien ajoutée avant l'affichage)
            time.sleep(0.5)
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
    
            # on supprime le fichier de la file
            print "suppression commande 1 ..."
            uucp_pool.remove_cmd(id_uucp,id_cmd2)
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
    
            # on purge toutes les commandes
            print "vidage complet ..."
            uucp_pool.flush([id_uucp])
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
            
    except UUCPError,e:
            sys.exit(("Erreur UUCP %s" % str(e)))
    

2) les transferts et appels distants par uucp

2.1) Les transfert de fichiers

La plupart des échanges de fichiers entre zephir et les serveurs clients se fait au travers du protocole UUCP (Taylor-UUCP : Uucp ), encapsulé dans un tunnel SSH. Le principe est le suivant:

  • mise en attente d'une commande à exécuter sur le serveur client.

  • si il y a des fichier à transférer au serveur:

    • création d'une archive contenant les fichiers à envoyer.
    • calcul et stockage de la somme md5 de l'archive
  • préparation du transfert par uucp (le fichier est mis en file d'attente)

  • connexion UUCP du client (à intervalle régulier):

    • Les fichier en attente sont transférés automatiquement

    • Les commande en attente sont exécutées sur le client:

      • si le transfert est de zephir vers le serveur (ex. configuration du serveur): la commande vérifie la somme md5 auprès de zephir à travers appel xmlrpc, décompresse l'archive, et met en place les fichiers sur le serveur
      • si le transfert est du serveur vers zephir (ex. sauvegarde de la configuration): elle va créer une archive des données à transférer et calculer sa somme md5. Puis elle va lancer immédiatement une nouvelle connexion UUCP vers zephir pour envoyer l'archive. Une fois le fichier transferé, le serveur apelle sur zephir une fonction XML-RPC en lui passant le résultat du md5. La fonction apellée va alors vérifier l'archive, la décompresser et la mettre en place (coté zephir).

2.2) Les appels de commandes

Comme vu dans le paragraphe précédent, zephir peut demander l'exécution de commandes sur les serveurs.

les commandes actuellement disponibles sont les suivantes:

  • configure (mise en place de la configuration du client)
  • reconfigure (lance la reconfiguration du serveur)
  • maj_auto (lance la procédure Maj-Auto)
  • maj_client (met à jour zephir-client si une version plus récente est présente sur zephir)
  • save_files (sauvegarde de la configuration du client sur zephir)
  • call (lance la connexion UUCP à zephir avec remontée des statistiques)
  • del_lock (permet de supprimer les verrous mis en place en cas d'échec d'une fonction zephir)

Note

les fonctions call et del_lock ne sont pas apellées par zephir. call est apellée par le script cron-zephir.sh, lancé toutes les 10 minutes par cron. del_lock peut être apellée manuellement par l'administrateur du serveur.

Les fonctions disponibles peuvent être définies de 2 façons différentes:

  • comme une fonction python définie dans le fichier /usr/share/eole/zephir/zephir_client.py
  • comme un script exécutable portant pour nom commande.zephir et situé dans /usr/share/eole/zephir

L'appel à ces fonctions se fait en demandant l'exécution par uucp du script zephir_client.

ex : mise en attente de la commande 'reconfigure' sur le serveur 1 (établissement 0210001A) depuis zephir

/usr/bin/uux2 -r '0210001A-1!zephir_client reconfigure'

Note

une librairie python créant une interface sur UUCP a été développée sur zephir (voir le paragraphe outils divers du backend). Par exemple, l'appel précédent dans l'application zephir est fait ainsi:

from zephir.backend.uucp_utils import uucp_pool, UUCPError
try:
        res = uucp_pool.add_cmd("0210001A-1","zephir_client reconfigure")
except UUCPError,e:
        return 0, u("Erreur UUCP %s" % str(e))

3) Les scripts clients

Les deux principaux scripts de la partie client sont:

Tous les scripts concernant Zephir se situent dans le répertoire /usr/share/eole/zephir/

3.1) La fonction d'enregistrement

Cette procédure permet de configurer un serveur Eole pour fonctionner avec Zephir. Les différentes étapes de cette procédures sont les suivantes:

  • mise en place d'une configuration réseau minimale si besoin

  • saisie de l'adresse de zephir

  • saisie d'un login et mot de passe pour la connexion à zephir

  • selon la méthode de déploiement retenue:

    • choix du n° de serveur existant dans la base zephir
    • création d'un nouveau serveur dans la base
  • mise en place de la conf uucp (et échange de clé rsa avec zephir)

  • proposition des choix suivants pour la mise en place de la configuration:

    • sauvegarde de la configuration existante (si déjà configuré) sur zephir
    • utilisation de la configuration stockée sur zephir
    • ne rien faire (configuration manuelle du serveur)
  • sauvegarde du n° de serveur et de l'adresse de zephir dans le fichier /usr/share/eole/zephir/zephir_conf/zephir_conf.py (si ce fichier est présent, le serveur considère qu'il est en mode zephir).

3.2) Les fonctions accessibles à zephir

Ces procédures sont soit des fonctions python déclarées dans zephir_client.py, soit des scripts exécutables du type /usr/share/eole/zephir/nom_script.zephir.

Dans le cas des scripts, si l'utilisateur uucp n'a pas les droits nécessaires à l'utilisation de ce script, la solution la plus simple consiste à écrire une fonction dans zephir_client.py qui porte le même nom que le script et qui apelle clui-ci avec une commande sudo.

Exemple de la fonction de sauvegarde de la configuration :

Cette fonction lance un script nommé save_files.zephir sur le client

def save_files(zephir):
        """lance la procédure d'envoi de la configuration du serveur sur zephir"""
        res = os.system('sudo /usr/share/eole/zephir/save_files.zephir 2>&1>> /tmp/rapport.zephir')
        if res == 0:
                return "ok"
        else:
                log('SAUVEGARDE',1,'erreur lors de la sauvegarde sur zephir')
                return "erreur save_files.zephir"

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 XML-RPC (non testé).

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.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.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']]