# -*- coding: UTF-8 -*-
###########################################################################
# Eole NG - 2007
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# modules_rpc.py
#
# fonctions xmlrpc pour la gestion des modules dans Zephir
#
###########################################################################
"""module de gestion des modules Eole
"""
from zephir.backend.db_utils import *
from zephir.backend import config
from zephir.backend.config import u, log
from zephir.backend.xmlrpceole import XMLRPCEole as XMLRPC
from zephir.backend.lib_backend import ResourceAuthError, istextfile, cx_pool
from zephir.utils.creolewrap import ZephirDict, get_upgrade_infos
from zephir.utils.convert import to_bytes

# import relatifs aux tables
import os,shutil,time,base64,traceback,json
from hashlib import md5
from zephir.eolerpclib import xmlrpclib, EoleProxy
from twisted.python import syslog, failure
from twisted.internet import defer
# importé au cas où une configuration serait envoyée sous forme d'OrderedDict
from collections import OrderedDict
from xmlrpc.client import Binary
from pathlib import Path

BLANK_MD5 = md5(b'').hexdigest()

class RPCModules(XMLRPC):
    """serveur XMLRPC zephir pour la gestion des modules Eole
    """

    def __init__(self,parent):
        self.dbpool = db_connect()
        self.dbpool.noisy = 0
        self.parent = parent
        # mise à jour des modules si besoin
        XMLRPC.__init__(self)

    def startup(self):
        old_obs = log.theLogPublisher.observers
        if old_obs:
            self.old_obs = log.theLogPublisher.observers[0]
        else:
            self.old_obs = None
        new_obs = syslog.SyslogObserver('zephir_backend', options=syslog.DEFAULT_OPTIONS, facility=syslog.DEFAULT_FACILITY)
        log.addObserver(new_obs.emit)
        if self.old_obs:
            log.removeObserver(self.old_obs)
        self.xmlrpc_update_modules()

    def stop(self):
        if self.old_obs:
            log.removeObserver(log.theLogPublisher.observers[0])
            log.addObserver(self.old_obs)

    def xmlrpc_update_modules(self,cred_user=''):
        """vérification de la liste des modules"""
        cursor = cx_pool.create()
        try:
            cursor.execute("""select id, libelle, version from modules""")
            modules_actuels = cursor.fetchall()
            libelle_modules = [module[1] for module in modules_actuels]
        except:
            cx_pool.close(cursor)
            # Erreur de lecture des modules
            log.msg("\nErreur de lecture de la liste des modules\n")
            return 0, u("Erreur de lecture de la liste des modules")
        cx_pool.close(cursor)
        d_list = []
        new_modules = []
        for version, modules in list(config.liste_modules.items()):
            for module in modules:
                # on recrée automatiquement les modules manquants, en dehors des modules Eole 1.X
                if module not in libelle_modules and version > 1:
                    try:
                        new_modules.append(module)
                        d_list.append(self.xmlrpc_add_module("", module, version, True))
                    except:
                        return 0, u("erreur d'ajout du module %s" % module)
        d = defer.DeferredList(d_list, consumeErrors=True)
        d.addBoth(self._update_module, new_modules)

    def _update_module(self, result, new_modules):
        # log de la création des modules
        for index, libelle in enumerate(new_modules):
            if result[index][0] == True:
                log.msg("\nNouveau module créé: %s (%s)" % (libelle, result[index][1]))
            else:
                log.msg("\nErreur de création du module %s : %s" % (libelle, result[index][1]))
        if len(new_modules) > 0:
            # mise à jour des données du pool de dictionnaires
            self.parent.dictpool.update_data()
            self.parent.dictpool.reset_modules()
            return 1, u("Nouveaux modules : %s" % ", ".join(new_modules))
        else:
            return 1, u("OK")

    def _got_modules(self,modules,cred_user):
        """Récupération de la table module depuis le backend
        """
        l=[]
        for module in modules:
            try:
                self.parent.s_pool.check_mod_credential(cred_user, module[0])
            except ResourceAuthError:
                # non autorisé : module suivant
                continue
            if module[2] == None:
                version = 1
            else:
                version = module[2]
            l.append({'id':module[0],'libelle':module[1],'version':version})
        return 1,u(l)

    ##############################
    ## gestion de la table modules
    ##############################


    def xmlrpc_add_module(self, cred_user, libelle, version=3, auto=False):
        """ajoute un module dans la base de données et crée l'arborescence correspondante
        @param auto: si True, on ne met pas à jour le pool de dictionnaires.
                     Limite les traitements au démarrage (update_modules)
        """
        # on vérifie que les données obligatoires sont remplies
        if libelle:
            query = """insert into modules (libelle, version) values (%s,%s)"""
            params = (libelle, int(version))
            # on effectue l'insertion (l'existence est à tester dans l'application cliente)
            return self.dbpool.runOperation(query, params).addCallbacks(self._add_module1,db_client_failed,callbackArgs=[libelle, version, auto])
        else:
            # des attributs manquent
            return 0, u("""donnez un libellé""")

    def _add_module1(self, resultat, libelle, version, auto):
        """récupère l'id du module créé"""
        # requête pour récupérer l'id
        query="""select id from modules where libelle ilike %s"""
        return self.dbpool.runQuery(query, (libelle,)).addCallbacks(self._add_module2,db_client_failed,callbackArgs=[libelle,version,auto])

    def _add_module2(self, row, libelle, version, auto):
        """crée la variante standard d'un nouveau module"""
        if row ==[]:
            return 0,u('erreur de création du module dans la base')
        else:
            id_module=row[0][0]
            # on crée la variante standard du module
            query = """insert into variantes (module,libelle) values (%s, 'standard')"""
            return self.dbpool.runOperation(query, (int(id_module),)).addCallbacks(self._add_module3,db_client_failed,callbackArgs=[id_module,libelle,version,auto])

    def _add_module3(self, resultat, id_module, libelle, version, auto):
        """récupère l'id de la variante standard"""
        query = """select id,module from variantes where module = %s and libelle = 'standard'"""
        return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._add_module4,db_client_failed,callbackArgs=[libelle,version,auto])


    def _add_module4(self,row,libelle,version,auto):
        """met en place l'arborescence du nouveau module et de ses variantes"""
        if row == []:
            return 0,u('erreur de creation de la variante par défaut dans la base')
        else:
            id_variante = row[0][0]
            id_module = row[0][1]
            # on créé l'arborescence pour le module
            module_dir = os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)
            try:
                if not os.path.isdir(module_dir):
                    os.makedirs(module_dir)
                os.makedirs(os.path.join(module_dir,'dicos'))
                # on crée la variante par défaut (module standard)
                os.makedirs(module_dir+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'dicos')
                os.makedirs(module_dir+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'patchs')
                os.makedirs(module_dir+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'fichiers_perso')
                os.makedirs(module_dir+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'fichiers_zephir')
            except:
                traceback.print_exc()
                try:
                    shutil.rmtree(module_dir)
                except:
                    # aucun répertoire créé
                    pass
                query = """delete from variantes where module = %s"""
                self.dbpool.runOperation(query, (int(id_module),))
                query = """delete from modules where id = %s"""
                self.dbpool.runOperation(query, (int(id_module),))
                return 0, u("""erreur de creation de l'arborescence du module %s""" % libelle)
            # mise à jour des statistiques
            self.parent.s_pool.stats['serv_modules'][str(id_module)] = 0
            self.parent.s_pool.stats['serv_variantes'][str(id_variante)] = 0
            # mise à jour du pool de dictionnaires si lancé par un utilisateur
            if not auto and version in self.parent.dictpool.eole_versions:
                self.parent.dictpool.update_data()
                self.parent.dictpool.update_module(id_module)
            return 1, u(id_module)

    def xmlrpc_del_module(self,cred_user,id_module):
        """supprime un module et ses variantes"""
        if int(id_module) >= 0:
            self.parent.s_pool.check_mod_credential(cred_user, id_module)
            # suppression des variantes dans la base de données
            query = "delete from variantes where module = %s"
            return self.dbpool.runOperation(query, (int(id_module),)).addCallbacks(self._del_module,db_client_failed,callbackArgs=[id_module])
        else:
            return 0, u("""donnez l'id d'un module (nombre entier)""")

    def _del_module(self,retour,id_module):
        """supprime de la base les services liés au module"""
        query = "delete from services where module = %s"
        return self.dbpool.runOperation(query, (int(id_module),)).addCallbacks(self._del_module1,db_client_failed,callbackArgs=[id_module])

    def _del_module1(self,retour,id_module):
        """récupère les informations en base avant suppression"""
        query = "select version, libelle from modules where id = %s"
        return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._del_module2,db_client_failed,callbackArgs=[id_module])

    def _del_module2(self,retour,id_module):
        """réinitialise le champ module_initial pour les serveurs concernés"""
        version, libelle = retour[0]
        query = "update serveurs set module_initial = module_actuel where module_initial = %s"
        return self.dbpool.runOperation(query, (int(id_module),)).addCallbacks(self._del_module3,db_client_failed,callbackArgs=[id_module, version, libelle])

    def _del_module3(self,retour,id_module,version,libelle):
        """supprime le module dans la base de données"""
        query = "delete from modules where id = %s"
        return self.dbpool.runOperation(query, (int(id_module),)).addCallbacks(self._del_module4,db_client_failed,callbackArgs=[id_module, version, libelle])

    def _del_module4(self,retour,id_module,version,libelle):
        """supprime l'arborescence du module dans l'arborescence zephir"""
        # mise à jour des statistiques
        if str(id_module) in self.parent.s_pool.stats['serv_modules']:
            del(self.parent.s_pool.stats['serv_modules'][str(id_module)])
        module_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)
        try:
            shutil.rmtree(module_dir)
        except:
            return 1,u("""erreur de supression du répertoire du module""")
        # suppression de l'entrée dans default_module/version pour les modules
        # utilisant le pool de dictionnaires
        if int(id_module) in self.parent.dictpool.modules:
            try:
                self.parent.dictpool.del_module_defaults(int(id_module))
            except:
                return 1,u("""erreur de supression du fichier de description du module""")
            try:
                del(self.parent.dictpool.modules[int(id_module)])
                del(self.parent.dictpool.variantes[int(id_module)])
            except:
                traceback.print_exc()
                # module inconnu de dictpool, ne devrait pas arriver
        else:
            # suppression du lien sur default_module/version si existant
            def_path = os.path.join(config.PATH_DEF_MODULES, str(version), libelle)
            if os.path.islink(def_path):
                print("Suppression du lien %s" % def_path)
                os.unlink(def_path)
        return 1,u('ok')

    def xmlrpc_edit_module(self,cred_user,id_module,dico_modifs):
        """modification d'un module
        cette fonction prend en compte un dictionnaire qui indique les
        champs à modifier et leur nouvelle valeur. l'application cliente
        doit s'assurer que ces champs existent dans la base"""
        self.parent.s_pool.check_mod_credential(cred_user, id_module)
        if dico_modifs == {}:
            return 1,u("""aucune modification demandée""")
        # on vérifie que l'identifiant n'est pas modifié
        if 'id' in list(dico_modifs.keys()):
            return 0,u("""l'identifiant ne peut pas être modifié""")
        # construction de la requête SQL de modification
        requete = ["update modules set "]
        params = []
        for cle in list(dico_modifs.keys()):
            requete.append(str(cle))
            if cle == 'version':
                requete.append("=%s, ")
                params.append(int(dico_modifs[cle]))
            else:
                requete.append("=%s, ")
                params.append(dico_modifs[cle])
        string_fin=""" where id=%s"""
        params.append(int(id_module))
        query="".join(requete)[:-2]
        query += string_fin
        return self.dbpool.runOperation(query, params).addCallbacks(lambda x:(1,'ok'), db_client_failed)

    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 id, libelle, version from modules where id = %s"""
            return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._got_modules,db_client_failed,callbackArgs=[cred_user])
        else:
            # sinon on renvoie tous les modules
            query = """select id, libelle, version from modules order by version desc, libelle"""
            return self.dbpool.runQuery(query).addCallbacks(self._got_modules,db_client_failed,callbackArgs=[cred_user])

    def xmlrpc_supported_versions(self, cred_user):
        """retourne la liste des version Zéphir supportées par le serveur
        """
        supported_versions = [el['id'] for el in config.TABLE_SUPPORT]
        return 1, supported_versions

    def xmlrpc_modules_distrib(self, cred_user, eole_version):
        """retourne la version de module Zéphir associée à une version de la distribution
        @param eole_version: version de la distribution (2.3, 2.4, ...)
        """
        # modules d'une version de distribution eole
        for mod_version, infos in list(config.DISTRIBS.items()):
            if infos[1] == eole_version:
                query = """select id, libelle, version from modules where version = %s order by libelle"""
                return self.dbpool.runQuery(query, (int(mod_version),)).addCallbacks(self._got_modules,db_client_failed,callbackArgs=[cred_user])
        # version demandée non trouvée
        return 0, u("""Version de distribution inconnue: %s""" % eole_version)

    def xmlrpc_get_dico(self,cred_user,id_module,id_variante=None):
        """récupération du dictionnaire d'un module (valeurs par défaut)"""
        # recherche du répertoire ou le dictionnaire est stocké
        try:
            query = "select version from modules where id = %s"
        except:
            return 0, u("""donnez l'id d'un module (nombre entier)""")
        return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._get_dico,db_client_failed,callbackArgs=[id_module, id_variante])

    def _get_dico(self,data,id_module,id_variante=None,errors=True):
        # détection version de créole
        try:
            version = int(data[0][0])
        except:
            return 0, u("""Module non trouvé""")
        try:
            # chargement de configuration
            path_module = os.path.join(os.path.abspath(config.PATH_ZEPHIR), 'modules', str(id_module))
            path_dico = os.path.join(path_module, 'dicos')
            # définition des dictionnaires à charger
            dict_dirs = [path_dico]
            if id_variante is not None:
                # dictionnaires de la variante si besoin
                dict_dirs.append(os.path.join(path_module,'variantes',str(id_variante),'dicos'))
                if os.path.isdir(os.path.join(path_module,'variantes',str(id_variante),'package')):
                    dict_dirs.append(os.path.join(path_module,'variantes',str(id_variante),'package'))
            # création de l'objet ZephirDict et lecture des valeurs existantes
            if version not in config.CREOLE_VERSIONS:
                return 1, u([])
            creole_version = config.CREOLE_VERSIONS[version]

            dict_zeph = ZephirDict(dicos=dict_dirs,
                                   confdir=path_dico,
                                   mode='config',
                                   version=creole_version,
                                   no_auto_store=True,
                                   eole_version=version)
            # on initialise un 'store' JSON avec les valeurs définies au niveau module/variante
            default_values = {}
            path_mod = os.path.join(path_module, 'module.eol')
            if os.path.isfile(path_mod):
                try:
                    store = json.loads(open(path_mod).read())
                except json.decoder.JSONDecodeError:
                    store = {}
                default_values.update(store)
            # traitement de la variante si demandé
            if id_variante is not None:
                path_var = os.path.join(path_module, 'variantes', str(id_variante), 'dico.eol')
                if os.path.isfile(path_var):
                    try:
                        store = json.loads(open(path_var, 'rb').read())
                    except json.decoder.JSONDecodeError:
                        store = {}
                    default_values.update(store)
            if default_values:
                # chargement des valeurs
                dict_zeph.init_from_zephir([str(default_values)])
            dicos = dict_zeph.get_dict()
            if dicos == None:
                dicos = []
            # ajout des erreur de chargement
            upgrade_infos = get_upgrade_infos(dict_zeph.dico)
            if upgrade_infos['load_errors']:
                dicos.append(upgrade_infos)
        except Exception as e:
            traceback.print_exc()
            msg = "Erreur lors du chargement du module {} (variante {}) : {}".format(id_module, id_variante, e)
            print(msg)
            return 0, u(str(msg))

        return 1,u(dicos)

    def xmlrpc_get_vars(self,cred_user,id_module):
        """récupération des libellés/familles des variables eole pour un module"""
        # recherche du répertoire ou le dictionnaire est stocké
        try:
            query = "select id,version from modules where id = %s"
        except:
            return 0, u("""donnez l'id d'un module (nombre entier)""")
        return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._get_vars,db_client_failed)

    def _get_vars(self,data):
        # détection version de créole
        try:
            id_module = int(data[0][0])
            version = int(data[0][1])
        except:
            return 0, u("""Module non trouvé""")
        # dictionnaire de base du module
        path_module = os.path.abspath(config.PATH_ZEPHIR)+os.sep+'modules'+os.sep+str(id_module)
        path_dico = path_module+os.sep+'dicos'
        # définition des dictionnaires à charger
        dict_dirs = [path_dico]
        # création de l'objet ZephirDict et lecture des valeurs existantes
        creole_version = config.CREOLE_VERSIONS[version]
        dict_zeph = ZephirDict(dicos=dict_dirs,
                               confdir=path_dico,
                               mode='config',
                               version=creole_version,
                               no_auto_store=True,
                               eole_version=version)
        families = {}
        menu = dict_zeph.get_menu(True)
        for family in menu:
            for var in family[2]:
                families[var] = family[0]

        # dictionnaire variable, libelle
        vars_dict = {}
        try:
            var_data = dict_zeph.get_first()
            while 1:
                if var_data[0] in families:
                    family = families[var_data[0]]
                else:
                    family = ''
                vars_dict[var_data[0]] = [var_data[2],family]
                var_data = dict_zeph.get_next()
        except:
            # on arrive en fin de dictionnaire
            pass

        return 1,u(vars_dict)

    def xmlrpc_save_dico(self,cred_user,dico_b64,id_module,id_variante=None,pass_var=""):
        """mise à jour du dictionnaire d'un module (valeurs par défaut)"""
        # recherche du libellé du module
        self.parent.s_pool.check_mod_credential(cred_user, id_module)
        if id_variante is not None:
            self.parent.s_pool.check_var_credential(cred_user, id_variante)
            query = "select modules.id, modules.libelle, modules.version, variantes.owner, variantes.passmd5 \
                    from modules,variantes where modules.id=%s and modules.id=variantes.module and variantes.id=%s"
            return self.dbpool.runQuery(query, (int(id_module), int(id_variante))).addCallbacks(self._save_dico, db_client_failed, callbackArgs=[cred_user,dico_b64,id_variante,pass_var])
        else:
            query = """select id, libelle, version from modules where id=%s"""
            return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._save_dico, db_client_failed, callbackArgs=[cred_user,dico_b64,id_variante,pass_var])

    def _save_dico(self,data,cred_user,dico_b64,id_variante=None,pass_var=""):
        if data == []:
            return 0,u("""module non retrouvé""")
        # récupération id et libellé module
        id_module = data[0][0]
        libelle = data[0][1]
        version = int(data[0][2])
        if id_variante is not None:
            # vérification owner/mot de passe
            owner = data[0][3]
            passmd5 = data[0][4]
            if owner != cred_user:
                # vérification du mot de passe
                if passmd5 != pass_var and passmd5 not in [None,'', BLANK_MD5]:
                    # mauvais mot de passe
                    return 0,u('mot de passe incorrect pour cette variante')
        if id_variante is None:
            path_dico = os.path.abspath(config.PATH_ZEPHIR)+os.sep+'modules'+os.sep+str(id_module)+os.sep+'module.eol'
        else:
            path_dico = os.path.abspath(config.PATH_ZEPHIR)+os.sep+'modules'+os.sep+str(id_module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'dico.eol'
        creole_version = config.CREOLE_VERSIONS[version]
        try:
            values = eval(dico_b64[-1])
            fic_zephir = open(path_dico, 'w')
            fic_zephir.write(json.dumps(values))
            fic_zephir.close()
        except:
            return 0,u("""erreur de sauvegarde du dictionnaire""")

        return 1,u('ok')

    def xmlrpc_get_mod_dict(self,cred_user,id_module):
        """renvoie la liste des dictionnaires d'un module.
        utile pour les modules eole2 (fichier fixé pour les modules eole1)
        """
        self.parent.s_pool.check_mod_credential(cred_user, id_module)
        try:
            dest_dir=os.path.join(os.path.abspath(config.PATH_MODULES),str(id_module),"dicos")
            content = []
            # cas d'un répertoire
            for dict in os.listdir(dest_dir):
                if dict.endswith('.xml'):
                    content.append(dict)
            return 1, u(content)
        except:
            return 0,u("""erreur de parcours du répertoire des dictionnaires""")

    def xmlrpc_del_mod_dict(self,cred_user,id_module,dico_name):
        """renvoie la liste des dictionnaires d'un module.
        utile pour les modules eole2 (fichier fixé pour les modules eole1)
        """
        # définition du chemin vers les dictionnaires
        self.parent.s_pool.check_mod_credential(cred_user, id_module)
        try:
            fic_dest = os.path.join(os.path.abspath(config.PATH_MODULES),str(id_module),"dicos",dico_name)
            assert os.path.isfile(fic_dest)
        except:
            return 0,u("""dictionnaire inexistant %s""" % dico_name)
        # on supprime le fichier
        try:
            os.unlink(fic_dest)
            return 1, "OK"
        except:
            return 0,u("""erreur lors de la suppression du dictionnaire""")

    ####################################
    ## gestion des variantes des modules
    ####################################

    def _got_variantes(self,variantes,cred_user):
        """formate les données lues dans la table variantes pour
        l'envoyer à une application zephir (liste de dictionnaires)"""
        l=[]
        for variante in variantes:
            owner = variante[3]
            if owner == None:
                owner = ""
            try:
                self.parent.s_pool.check_var_credential(cred_user, variante[0])
            except ResourceAuthError:
                # non autorisé : module suivant
                continue
            l.append({'id':variante[0],'module':variante[1],'libelle':variante[2],'owner':owner})
        return 1,u(l)

    def xmlrpc_copy_variante(self,cred_user,module,id_var_src="",libelle="",passmd5="",keep_perms=False,id_var=""):
        """Copie une variante existante sur une nouvelle variante"""
        # recherche des données de l'ancienne variante
        try:
            var_src = int(id_var_src)
            module = int(module)
            if id_var == "":
                assert libelle != ""
            else:
                # écrasement d'une variante existante
                id_var = int(id_var)
        except:
            return 0, u("""paramètres invalides""")

        # garde-fou pour ne pas écraser une variante avec elle-même
        if id_var == var_src:
            return 0, u("ids de variante source et destination identiques")

        """Vérification de la présence des dictionnaires"""
        query = """select m.version from modules as m  where m.id=%s"""
        return self.dbpool.runQuery(query, (int(module),)).addCallbacks(self._copy_variante_src_local_resources, db_client_failed,callbackArgs=[cred_user,var_src,libelle,module,passmd5,keep_perms,id_var])

    def _copy_variante_src_local_resources(self, data, cred_user, var_src, libelle, module, passmd5, keep_perms, id_var):
        version = config.DISTRIBS[data[0][0]][1]
        query = """select dict_name from dict_variante where id_resource=%s and dict_type='local'"""
        return self.dbpool.runQuery(query, (int(var_src),)).addCallbacks(self._copy_variante_dict_avalaible, db_client_failed,callbackArgs=[cred_user,var_src,libelle,module,passmd5,keep_perms,id_var,version])

    def _copy_variante_dict_avalaible(self, data, cred_user, var_src, libelle, module, passmd5, keep_perms, id_var, version):
        """Vérification de la présence des dictionnaires"""
        try:
            local_resources_dst_module = os.listdir(os.path.join(config.ROOT_DIR, 'dictionnaires', version, 'local'))
            local_resources_src_variante = [d[0] for d in data]
        except:
            return 0, u("""erreur lors de la récupération des listes de dictionnaires personnalisés""")
        if set(local_resources_src_variante).issubset(set(local_resources_dst_module)):
            log.msg('on continue')
            # lecture du mot de passe et du module de l'ancienne variante
            query = """select module,owner,passmd5 from variantes where id=%s"""
            return self.dbpool.runQuery(query, (int(var_src),)).addCallbacks(self._copy_variante, db_client_failed,callbackArgs=[cred_user,var_src,libelle,module,passmd5,keep_perms,id_var])
        else:
            missing_dict = set(local_resources_src_variante).difference(set(local_resources_dst_module))
            return 0, u("""dictionnaires personnalisés nécessaires pour la version cible du module ({})""".format(', '.join(missing_dict)))

    def _copy_variante(self,data,cred_user,var_src,libelle,module,passmd5,keep_perms,id_var):
        """insertion de la nouvelle variante"""
        try:
            module_src = data[0][0]
            if keep_perms and id_var == "":
                # on veut conserver le propriétaire/mot de passe
                cred_user = data[0][1]
                passmd5 = data[0][2]
        except:
            # variante créée non trouvée dans la base
            return 0,u("""module d'origine non retrouvé dans la base""")

        if id_var != "":
            # copie sur une variante existante
            query = """select * from variantes where id=%s"""
            return self.dbpool.runQuery(query, (int(id_var),)).addCallbacks(self._copy_variante3, db_client_failed,callbackArgs=[var_src,module_src,id_var,cred_user,passmd5])
        else:
            # création d'une copie de variante
            query = """insert into variantes (module,libelle,owner,passmd5) values (%s,%s,%s,%s)"""
            params = (int(module), libelle, cred_user, passmd5)
            # id assigné automatiquement dans postgresql
            # on effectue l'insertion
            return self.dbpool.runOperation(query, params).addCallbacks(self._copy_variante2, db_client_failed,callbackArgs=[var_src,module_src,libelle,module,id_var,cred_user,passmd5])

    def _copy_variante2(self,data,var_src,module_src,libelle,module,id_var,cred_user,passmd5):
        """recherche de l'id de la variante créée"""

        query = """select * from variantes where libelle=%s and module=%s"""
        params = (libelle, int(module))
        return self.dbpool.runQuery(query, params).addCallbacks(self._copy_variante3, db_client_failed,callbackArgs=[var_src,module_src,id_var,cred_user,passmd5])

    def _copy_variante3(self,data,var_src,module_src,id_var,cred_user,passmd5):
        """copie l'arborescence de la variante source"""
        try:
            var_dest = data[0][0]
            module = data[0][1]
            libelle = data[0][2]
            owner = data[0][3]
            pass_var = data[0][4]
        except:
            # variante créée non trouvée dans la base
            return 0,u("""nouvelle variante non retrouvée dans la base""")
        else:
            # on recopie l'arborescence de la variante source
            rep_src=os.path.abspath(config.PATH_MODULES)+os.sep+str(module_src)+os.sep+'variantes'+os.sep+str(var_src)
            rep_dest=os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(var_dest)
            try:
                if id_var == "":
                    if str(var_dest) not in self.parent.s_pool.stats['serv_variantes']:
                        self.parent.s_pool.stats['serv_variantes'][str(var_dest)] = 0
                else:
                    # modification d'une variante existante
                    if owner != cred_user:
                        # vérification du mot de passe
                        if passmd5 != pass_var and pass_var not in [None,'', BLANK_MD5]:
                            # mauvais mot de passe
                            return 0, u("""Mauvais mot de passe pour la variante de destination""")
                    shutil.rmtree(rep_dest)
                shutil.copytree(rep_src,rep_dest)
            except:
                traceback.print_exc()
                return 0,u("""erreur de copie de l'arborescence de la variante""")
            if int(module) in self.parent.dictpool.modules:
                try:
                    # mise à jour des variantes/modules connus du pool de dictionnaires
                    self.parent.dictpool.update_data()
                    if int(module_src) in self.parent.dictpool.modules:
                        # dans le cas de 2 variantes 'creole3': copie des dictionnaires activés
                        if not self.parent.dictpool.copy_variante(var_src, var_dest):
                            log.msg("Erreur lors de la reprise des dictionnaires activés de la variante %s vers la variante %s" % (str(var_src), str(var_dest)))
                    else:
                        # sinon: recherche d'éventuels paquets de dictionnaires déclarés
                        self.parent.dictpool.sync_variante_paqs(var_dest)
                except:
                    traceback.print_exc()
                    return 0,u("""erreur lors de la copie des dictionnaires activés""")
        query = """select id, libelle, version from modules where id = %s or id = %s"""
        params = (int(module_src), int(module))
        return self.dbpool.runQuery(query, params).addCallbacks(self._copy_variante4, db_client_failed,callbackArgs=[var_src, var_dest, module_src, module, id_var])

    def _copy_variante4(self, data, var_src, var_dest, module_src, module, id_var):
        """stocke la concordance entre les variantes si nécessaire"""
        if id_var == "":
            version_src = version_dest = 0
            libelle_src = libelle_dest = ""
            try:
                for mod in data:
                    if mod[0] == module_src:
                        version_src = int(mod[2])
                        libelle_src = mod[1][:mod[1].rindex("-")]
                    elif mod[0] == module:
                        version_dest = int(mod[2])
                        libelle_dest = mod[1][:mod[1].rindex("-")]
            except:
                log.msg('copie de variante : erreur lors de la recherche des version des modules sources et destination')
            if libelle_src == libelle_dest:
                # stockage de la liaison entre les 2 variantes si copie pour upgrade entre 2 version d'un module
                # possible entre 2 versions successives, ou si autorisée explicitement (config.allowed_upgrades)
                next_dist = config.get_next_dist(version_src)
                if next_dist != -1:
                    allowed_dests = [next_dist]
                else:
                    allowed_dests = []
                if libelle_src in config.allowed_upgrades:
                    allowed_dests.extend(config.allowed_upgrades[libelle_src].get(version_src,[]))
                if version_dest in allowed_dests:
                    query = """insert into migration_variantes (id_source, id_dest) values (%s,%s)"""
                    params = (int(var_src), int(var_dest))
                    return self.dbpool.runOperation(query, params).addCallbacks(lambda x:(1, var_dest), db_client_failed)
        return 1, var_dest

    def get_variante_query(self):
        return """select * from variantes where libelle ilike %s and module = %s"""

    def xmlrpc_add_variante(self,cred_user, module, libelle, pass_var=""):
        """ajoute une variante à un module"""
        self.parent.s_pool.check_mod_credential(cred_user, module)
        # on vérifie qu'on a bien récupéré un module
        if module and libelle:
            params = (libelle, int(module))
            query = self.get_variante_query()
            return self.dbpool.runQuery(query, params).addCallbacks(self._valid_variante, db_client_failed,callbackArgs=[libelle, module, cred_user, pass_var])
        else:
            # des attributs manquent
            return 0, u("""donnez un id de module et un libellé""")

    def _valid_variante(self, data, libelle, module, cred_user, pass_var):
        if data:
            return 0, u('''la variante "%s" existe déjà pour ce module''' % libelle)
        query = """insert into variantes (module,libelle,owner,passmd5) values (%s,%s,%s,%s)"""
        params = (int(module), libelle, cred_user, pass_var)
        # id assigné automatiquement dans postgresql
        # on effectue l'insertion
        return self.dbpool.runOperation(query, params).addCallbacks(self._add_variante1, db_client_failed,callbackArgs=[libelle,module])

    def _add_variante1(self,data,libelle,module):
        """recherche de l'id de la variante créée"""
        # on récupère l'id de la variante créée
        query = self.get_variante_query()
        params = (libelle, int(module))
        return self.dbpool.runQuery(query, params).addCallbacks(self._add_variante2,db_client_failed)

    def _add_variante2(self,variante):
        """met en place l'arborescence d'une nouvelle variante"""
        try:
            id_variante = variante[0][0]
            module = variante[0][1]
            libelle = variante[0][2]
        except:
            # variante créée non trouvée dans la base
            return 0,u("""nouvelle variante non retrouvée dans la base""")
        else:
            # on créé l'arborescence de la variante dans les modules
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante))
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'patchs')
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'dicos')
            # Création d'un répertoire pour les paquets de dictionnaires
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'package')
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'fichiers_perso')
            os.makedirs(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+os.sep+'variantes'+os.sep+str(id_variante)+os.sep+'fichiers_zephir')
            # on met à jour les statistiques
            self.parent.s_pool.stats['serv_variantes'][str(id_variante)] = 0
            self.parent.dictpool.update_data()
            return 1,id_variante

    def xmlrpc_del_variante(self,cred_user,id_variante):
        """suppression d'une variante de la base zephir"""
        self.parent.s_pool.check_var_credential(cred_user, id_variante)
        if id_variante:
            query = """select * from variantes where id = %s"""
            # on récupère les données de la variante
            return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._del_variante, db_client_failed)
        else:
            return 0,u("""donnez un identifiant de variante""")

    def _del_variante(self,variante):
        """supprime la variante de la base de données"""
        try:
            id_variante = variante[0][0]
            id_module = variante[0][1]
            libelle = variante[0][2]
        except:
            # variante non trouvée dans la base
            return 0,u("""variante non trouvée dans la base""")
        else:
            # on interdit la supression de la variante standard
            if libelle == 'standard':
                return 0, u("""la variante standard ne peut pas être supprimée""")
            # suppression variante dans la base
            query = """delete from variantes where id = %s"""
            return self.dbpool.runOperation(query, (int(id_variante),)).addCallbacks(self._del_variante2, db_client_failed,callbackArgs=[id_module,id_variante])

    def _del_variante2(self,result,id_module,id_variante):
        """supression de l'arborescence de la variante"""
        # purge des statistiques de la variante
        del(self.parent.s_pool.stats['serv_variantes'][str(id_variante)])
        # purge des infos de variante dans 'dictpool' si besoin
        if id_variante in self.parent.dictpool.variantes[id_module]:
            self.parent.dictpool.variantes[id_module].remove(id_variante)
        # ok, on supprime l'arborescence dans zephir/modules/variantes/id_variante
        variante_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        try:
            shutil.rmtree(variante_dir)
        except:
            # remonter code -1 si supression de répertoire impossible ?
            return 0,u("""erreur de supression du répertoire de la variante""")
        return 1, u('ok')

    def xmlrpc_fichiers_variante(self,cred_user,id_variante,show_details=False):
        """ retourne la liste des fichiers personnalisés pour cette variante """
        self.parent.s_pool.check_var_credential(cred_user, id_variante)
        query = """select id,module from variantes where id = %s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._fichiers_zephir,db_client_failed,callbackArgs=[show_details])

    def _fichiers_zephir(self,data,show_details):
        """recherche les fichiers liés à une variante"""
        if data == []:
            return 0,u("serveur inconnu de zephir")
        else:
            id_variante = int(data[0][0])
            id_module = int(data[0][1])
            # répertoire de stockage de la variante sur zephir
            variante_dir = os.path.abspath(config.PATH_ZEPHIR)+os.sep+'modules'+os.sep+str(id_module)+os.sep+'variantes'+os.sep+str(id_variante)
            # création du dictionnaire de listes des différents fichiers
            dico_res={}
            try:
                # dictionnaires additionnels
                liste_dicos = []
                liste_dicos_var = []
                for fic in os.listdir(variante_dir+os.sep+'dicos'):
                    if fic.endswith('.eol') or fic.endswith('.xml'):
                        liste_dicos_var.append(fic)
                dico_res['dicos_var'] = liste_dicos_var
            except OSError:
                dico_res['dicos_var'] = ['répertoire non trouvé !']
            try:
                # fichiers templates variante
                dico_res['persos_var'] = (os.listdir(variante_dir+os.sep+'fichiers_perso'))
            except OSError:
                dico_res['persos_var'] = ['répertoire non trouvé !']
            try:
                # patchs variante
                dico_res['patchs_var'] = os.listdir(variante_dir+os.sep+'patchs')
            except OSError:
                dico_res['patchs_var'] = ['répertoire non trouvé !']
            try:
                # RPMS variante
                # lecture de la liste des rpms supplémentaires
                fic = open(variante_dir+'/fichiers_zephir/fichiers_variante')
                data = fic.read().split("\n")
                fic.close()
                # recherche de la section des RPMS
                liste_pkg_var = []
                section_rpm = 0
                for ligne in data:
                    ligne = ligne.strip()
                    if section_rpm == 1:
                        # on regarde si on a bien affaire à un paquetage
                        if not ligne.startswith('#') and ligne != '':
                            # on affiche le nom du paquetage
                            liste_pkg_var.append(ligne)
                    if ligne == '%%':
                        section_rpm = 1
                dico_res['rpms_var'] = liste_pkg_var
            except IOError:
                dico_res['rpms_var'] = []
            try:
                # fichiers du module
                liste_fic=[]
                try:
                    f=open(variante_dir+os.sep+'fichiers_zephir/fichiers_variante')
                    old_content=f.read()
                    f.close()
                    fichiers=old_content.split('%%\n')[0]
                except:
                    fichiers=""
                for f_zeph in fichiers.split('\n'):
                    if f_zeph.strip().startswith("""/"""):
                        f_local = os.path.join(variante_dir,'fichiers_zephir',os.path.basename(f_zeph.strip()))
                        if show_details:
                            f_info = config.get_file_info(f_local)
                            liste_fic.append((f_zeph,f_info))
                        elif os.path.exists(f_local):
                            liste_fic.append(f_zeph)
                dico_res['fichiers_var'] = liste_fic
            except:
                dico_res['fichiers_var'] = ['répertoire non trouvé !']
            return 1,u(dico_res)

    def xmlrpc_get_variante_perms(self, cred_user, id_variante, filepath=""):
        """renvoie les informations de permissions associées à un(des) fichier(s)
        """
        # on recherche le répertoire ou la variante est stockée
        query = """select id,module from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._get_variante_perms,db_client_failed,callbackArgs=[filepath])

    def _get_variante_perms(self, data, filepath):
        """crée l'archive de la variante et la renvoie"""
        if data != []:
            id_variante = data[0][0]
            id_module = data[0][1]
            # répertoire de la variante
            var_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        try:
            id_var = int(id_variante)

        except (KeyError, ValueError):
            return 0, u("""variante inconnue : %s""" % str(id_var))
        else:
            result = self.parent.s_pool.get_file_perms(var_dir, filepath)
            return 1, u(result)

    def xmlrpc_del_variante_perms(self, cred_user, id_variante, filepath="", pass_var=""):
        """supprime les informations de permissions associées à un(des) fichier(s)
        """
        # on recherche le répertoire ou la variante est stockée
        query = """select id,module,owner,passmd5 from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._del_variante_perms,db_client_failed,callbackArgs=[cred_user,filepath,pass_var])

    def _del_variante_perms(self, data, cred_user, filepath, pass_var):
        """crée l'archive de la variante et la renvoie"""
        if data != []:
            id_variante = data[0][0]
            id_module = data[0][1]
            owner = data[0][2]
            passmd5 = data[0][3]
            # vérification owner/mot de passe
            if owner != cred_user:
                # vérification du mot de passe
                if passmd5 != pass_var and passmd5 not in [None,'',BLANK_MD5]:
                    # mauvais mot de passe
                    return 0,u('mot de passe incorrect pour cette variante')
            # répertoire de la variante
            var_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        try:
            id_var = int(id_variante)
        except (KeyError, ValueError):
            return 0, u("""variante inconnue : %s""" % str(id_var))
        else:
            result = self.parent.s_pool.del_file_perms(var_dir, filepath)
            return 1, u(result)

    def xmlrpc_set_variante_perms(self, cred_user, id_variante, rights, pass_var=""):
        """enregistre les informations de permissions associées à un(des) fichier(s)
        @param rights: dictionnaire au format suviant : {'filepath':[mode,ownership]}
        """
        # on recherche le répertoire ou la variante est stockée
        query = """select id,module,owner,passmd5 from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._set_variante_perms,db_client_failed,callbackArgs=[cred_user,rights,pass_var])

    def _set_variante_perms(self, data, cred_user, rights, pass_var):
        """crée l'archive de la variante et la renvoie"""
        if data != []:
            id_variante = data[0][0]
            id_module = data[0][1]
            owner = data[0][2]
            passmd5 = data[0][3]
            # vérification owner/mot de passe
            if owner != cred_user:
                # vérification du mot de passe
                if passmd5 != pass_var and passmd5 not in [None,'',BLANK_MD5]:
                    # mauvais mot de passe
                    return 0,u('mot de passe incorrect pour cette variante')
            # répertoire de la variante
            var_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        try:
            id_var = int(id_variante)
        except (KeyError, ValueError):
            return 0, u("""variante inconnue : %s""" % str(id_var))
        else:
            res = self.parent.s_pool.set_file_perms(rights, var_dir)
            if res:
                return 1,"OK"
            else:
                return 0, u("""erreur d'enregistrement des permissions""")

    def xmlrpc_get_variante(self,cred_user,id_variante=None):
        """récupère la liste d'une variante (ou toutes)"""
        if id_variante :
            query = """select * from variantes where id=%s"""
            return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._got_variantes,db_client_failed,callbackArgs=[cred_user])
        else :
            query = """select * from variantes order by module desc, id desc"""
            return self.dbpool.runQuery(query).addCallbacks(self._got_variantes,db_client_failed,callbackArgs=[cred_user])

    def xmlrpc_edit_variante(self,cred_user,id_variante,dico_modifs,pass_var=""):
        """modification d'une variante
        cette fonction prend en compte un dictionnaire qui indique les
        champs à modifier et leur nouvelle valeur. l'application cliente
        doit s'assurer que ces champs existent dans la base"""
        self.parent.s_pool.check_var_credential(cred_user, id_variante)
        defer_perms = self.parent.xmlrpc_get_permissions(cred_user, cred_user)
        return defer_perms.addCallbacks(self._edit_variante, db_client_failed, callbackArgs=[cred_user,id_variante,dico_modifs,pass_var])

    def _edit_variante(self,permissions,cred_user,id_variante,dico_modifs,pass_var):
        # vérification du propriétaire/mot de passe
        query = """select owner,passmd5 from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._edit_variante2,db_client_failed,
                                                        callbackArgs=[cred_user,permissions,id_variante,dico_modifs,pass_var])

    def _edit_variante2(self,data,cred_user,permissions,id_variante,dico_modifs,pass_var):
        # on vérifie que l'identifiant n'est pas modifié
        if dico_modifs == {}:
            return 1,u("""aucune modification demandée""")
        owner = data[0][0]
        passmd5 = data[0][1]
        is_admin = False
        if permissions[0] == 1 and 4 in permissions[1]:
            is_admin = True
        # vérification owner/mot de passe
        if owner != cred_user:
            # vérification du mot de passe
            if passmd5 != pass_var and passmd5 not in [None,'',BLANK_MD5]:
                # mauvais mot de passe
                return 0,u('mot de passe incorrect pour cette variante')
        if ('id' in list(dico_modifs.keys())) or ('module' in list(dico_modifs.keys())):
            return 0,u("""l'identifiant et le module ne peuvent pas être modifiés""")
        if ('owner' in list(dico_modifs.keys())) or ('passmd5' in list(dico_modifs.keys())):
            # seul le propriétaire de la variante ou un administrateur a le droit
            # de modifier le mot de passe ou le propriétaire
            if cred_user != owner and not is_admin:
                return 0,u("""modification du propriétaire ou mot de passe interdits""")
            if 'owner' in list(dico_modifs.keys()):
                log.msg("nouveau propriétaire pour la variante %s : %s" % (str(id_variante), dico_modifs['owner']))
            if 'passmd5' in list(dico_modifs.keys()):
                log.msg("changement du mot de passe de la variante %s" % str(id_variante))
        # construction de la requête SQL de modification
        requete = ["update variantes set "]
        params = []
        for cle in list(dico_modifs.keys()):
            requete.append(str(cle))
            requete.append("=%s, ")
            params.append(str(dico_modifs[cle]))
        string_fin=""" where id=%s"""
        params.append(int(id_variante))
        query = "".join(requete)[:-2]
        query += string_fin
        return self.dbpool.runOperation(query, params).addCallbacks(lambda x:(1,'ok'), db_client_failed)

    def xmlrpc_get_migration_infos(self, cred_user, id_module):
        """récupère la liste des variantes d'un module et des variantes de migration définies
        """
        try:
            id_module = int(id_module)
        except:
            return 0,u("identifiant de module invalide")
        self.parent.s_pool.check_mod_credential(cred_user, id_module)
        # récupération des modules
        query = """select id, libelle, version from modules"""
        return self.dbpool.runQuery(query).addCallbacks(self._get_migration_infos,db_client_failed,callbackArgs=[id_module])

    def _get_migration_infos(self, data, id_module):
        # recherche des modules vers lesquels on peut migrer
        mod_src_lib = ""
        mod_src_version = 0
        info_modules = {}
        for module in data:
            if module[0] == id_module:
                mod_src_lib = module[1]
                mod_src_version = int(module[2])
                break
        migration_infos = {}
        if mod_src_version > 1:
            mod_src_name, mod_src_eole_vers = mod_src_lib.rsplit('-', 1)
            next_dist = config.get_next_dist(mod_src_version)
            if next_dist != -1:
                auto_upgrades = [next_dist]
            else:
                auto_upgrades = []
            # migration non standard acceptées
            if mod_src_name in config.allowed_upgrades:
                for dst_version in config.allowed_upgrades[mod_src_name].get(mod_src_version, []):
                    # pour les modules nécessitant une migration, pas de choix automatique de la variante
                    if not dst_version in config.allowed_migrations[mod_src_version]:
                        auto_upgrades.append(dst_version)
            for module in data:
                mod_name, mod_eole_vers = module[1].rsplit('-', 1)
                mod_version = int(module[2])
                if mod_name == mod_src_name:
                    if mod_version in auto_upgrades:
                        info_modules[module[0]] = [module[1],int(module[2])]
                        migration_infos[mod_version] = [[],[]]
            query="""select modules.version, id_source, id_dest, var_dst.module from migration_variantes, modules, variantes as var_dst, variantes as var_src where id_source=var_src.id and var_src.module=%s and var_dst.id=id_dest and modules.id = var_dst.module"""
            return self.dbpool.runQuery(query, (int(id_module),)).addCallbacks(self._get_migration_infos2,db_client_failed,callbackArgs=[id_module, migration_infos, info_modules])
        else:
            return 0,u("module inexistant dans la base")

    def _get_migration_infos2(self, data, id_module, migration_infos, info_modules):
        # correspondances déjà définies
        for var_migration in data:
            if var_migration[0] in migration_infos:
                migration_infos[var_migration[0]][1].append([var_migration[1], var_migration[2]])
        query = """select id, module from variantes"""
        return self.dbpool.runQuery(query).addCallbacks(self._get_migration_infos3, db_client_failed,callbackArgs=[id_module, migration_infos, info_modules])

    def _get_migration_infos3(self, data, id_module, migration_infos, info_modules):
        # liste des destinations possibles pour chaque module
        for id_var, id_mod in data:
            # on garde seulement les modules de même type
            if id_mod in info_modules:
                vers_mod = info_modules[id_mod][1]
                # si migration autorisée
                if vers_mod in migration_infos:
                    # liste des variantes dispos
                    migration_infos[vers_mod][0].append(id_var)
        result = []
        for vers, data in list(migration_infos.items()):
            result.append((vers, data[0], data[1]))
        return 1, result

    def xmlrpc_variantes_upgrade(self, cred_user, var_src, var_migr):
        """sauvegarde les équivalences d'une variable pour upgrade_auto
        """
        try:
            var_src = int(var_src)
            var_dest = []
            if type(var_migr) != list:
                var_migr = [var_migr]
            for var in var_migr:
                var_dest.append(int(var))
        except:
            return 0,u("identifiant de variante invalide")
        self.parent.s_pool.check_var_credential(cred_user, var_src)
        # supression des anciennes correspondances
        query = """delete from migration_variantes where id_source=%s"""
        return self.dbpool.runOperation(query, (int(var_src),)).addCallbacks(self._variantes_upgrade2, db_client_failed, callbackArgs=[var_src, var_dest])

    def _variantes_upgrade2(self, result, var_src, var_dest):
        """sauvegarde les équivalences d'une variable pour upgrade_auto
        """
        if var_dest != []:
            params = []
            inserts = []
            for dst in var_dest:
                inserts.append('(%s,%s)')
                params.extend([int(var_src), int(dst)])
            query = """insert into migration_variantes (id_source, id_dest) values %s""" % ", ".join(inserts)
            return self.dbpool.runOperation(query, params).addCallbacks(lambda x:(1,'ok'), db_client_failed)
        else:
            return 1, 'ok'

    def xmlrpc_add_files(self,cred_user,id_variante,dico_files,passwd="",encode=False, context='add'):
        """ajoute des fichiers, patchs, dictionnaires à une variante
        """
        self.parent.s_pool.check_var_credential(cred_user, id_variante)
        query="select modules.version,variantes.id,variantes.module,variantes.owner,variantes.passmd5 from variantes,modules where variantes.id=%s and modules.id=variantes.module"
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._add_files,db_client_failed,callbackArgs=[dico_files,cred_user,passwd,encode,context])

    def _add_files(self,data,dico_files,cred_user,passwd,encode, context):
        """ajoute des fichiers, patchs, dictionnaires,rpms à une variante
        """
        mod_version = data[0][0]
        mod_version = int(mod_version)
        id_variante = data[0][1]
        id_module = data[0][2]
        owner = data[0][3]
        pass_var = data[0][4]
        # si la variante ne nous appartient pas, on interdit la modification si mauvais mot de passe
        if owner != cred_user:
            # vérification du mot de passe
            if passwd != pass_var and pass_var not in [None,'',BLANK_MD5]:
                # mauvais mot de passe
                return 0,u('mot de passe incorrect pour cette variante')
        # on met en place les différents types de fichiers
        dest_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        # on récupère la liste des fichiers à supprimer existante
        liste_remove = []
        if os.path.isfile(dest_dir+os.sep+'fichiers_zephir/removed'):
            f_rem = open(dest_dir+os.sep+'fichiers_zephir/removed')
            for fic in f_rem.read().strip().split('\n'):
                liste_remove.append(fic)
            f_rem.close()
        def check_removed(nom_dest):
            # on regarde si le fichier était dans la liste des fichiers obsolètes
            if nom_dest in liste_remove:
                liste_remove.remove(nom_dest)
                f_rem = open(dest_dir+os.sep+'fichiers_zephir/removed', 'w')
                f_rem.write('\n'.join(liste_remove))
                f_rem.close()
        # on vérifie si des paquets non autorisés sont listés
        if 'rpms_var' in dico_files:
            # liste des paquets gérés au niveau variante/module
            denied = self.parent.dictpool.list_module(id_module, False)
            denied = set([paq_name for paq_name in denied if not paq_name.endswith('.xml')])
            # si un paquet demandé est dans la liste, on l'interdit
            bad_paqs = denied.intersection(set(dico_files['rpms_var']))
            if bad_paqs:
                return 0, u("""paquets déjà référencés au niveau du module : %s""" % ', '.join(bad_paqs))

        # dictionnaires locaux
        for dico in dico_files['dicos_var']:
            try:
                if dico[0].strip() != "":
                    f=open(dest_dir+os.sep+'dicos'+os.sep+os.path.basename(dico[0].strip().replace("\\","/")),'wb')
                    f.write(base64.decodebytes(to_bytes(dico[1])))
                    f.close()
            except:
                return 0,u("erreur de sauvegarde de %s" % dico)
        for template in dico_files['persos_var']:
            try:
                if template[0].strip() != "":
                    f=open(dest_dir+os.sep+'fichiers_perso'+os.sep+os.path.basename(template[0].strip().replace("\\","/")),'wb')
                    f.write(base64.decodebytes(to_bytes(template[1])))
                    f.close()
                    check_removed(os.path.join('fichiers_perso', template[0].strip().replace("\\","/")))
            except:
                return 0,u("erreur de sauvegarde de %s" % template)
        for patch in dico_files['patchs_var']:
            try:
                if patch[0].strip() != "":
                    f=open(dest_dir+os.sep+'patchs'+os.sep+os.path.basename(patch[0].strip().replace("\\","/")),'wb')
                    f.write(base64.decodebytes(to_bytes(patch[1])))
                    f.close()
            except:
                return 0,u("erreur de sauvegarde de %s" % patch)
        # on reprend la liste des fichiers existants
        try:
            f=open(dest_dir+os.sep+'fichiers_zephir/fichiers_variante')
            old_content=f.read()
            f.close()
            fichiers=old_content.split('%%\n')[0]
            rpms=old_content.split('%%\n')[1]
        except:
            fichiers="""# section 1
# liste des fichiers à sauvegarder
# (ne pas modifier sauf pour créer ou mettre à jour la variante)"""
            rpms="""# section 2
# inscrire les noms des paquetages qui seront installés à la mise à jour du serveur
# (ils doivent être présents sur le serveur de mise à jour)"""

        for fichier in dico_files['fichiers_var']:
            if fichier[0].strip() == '':
                continue
            # on ajoute le fichier à la liste si il n'est pas déjà présent
            localpath = ""
            if len(fichier) == 3:
                localpath = fichier[2]
            # on enlève le nom de conteneur si présent
            conteneur = ""
            if '::' in fichier[0]:
                fic_path, conteneur = fichier[0].strip().split('::')
            else:
                fic_path = fichier[0].strip()
            # nettoyage du nom de fichier
            nom_fic = fic_path.replace("\\","/")
            # on supprime les séparateurs en fin de ligne
            if fic_path.endswith('/'):
                nom_fic = fic_path[:-1]
            if fic_path.endswith("\\"):
                nom_fic = fic_path[:-2]
            if conteneur:
                nom_final = "%s::%s" % (nom_fic, conteneur)
            else:
                nom_final = nom_fic
            check_removed(nom_final)
            # on ajoute le fichier à la liste si il n'est pas déjà présent et si il n'est pas dans un sous répertoire
            if context != 'save':
                if  os.path.basename(nom_final) not in [os.path.basename(f) for f in fichiers.split('\n') if not f.startswith("#")] and localpath == "":
                    fichiers = fichiers.strip() + '\n' + nom_final +'\n'
                else:
                    return 0,u("Un fichier divers du même nom est déjà présent (%s). Modifiez le nom de destination pour permettre l’ajout." % os.path.basename(fichier[0]))

            # on écrit le contenu du fichier
            try:
                if nom_fic != "":
                    if localpath == "":
                        f=open(os.path.join(dest_dir,'fichiers_zephir',os.path.basename(nom_final)),'wb')
                    else:
                        f=open(os.path.join(dest_dir,localpath,os.path.basename(nom_final)),'wb')

                    if isinstance(fichier[1], Binary):
                        f.write(base64.decodebytes(to_bytes(fichier[1])))
                    else:
                        f.write(base64.decodebytes(fichier[1].encode()))

                    f.close()
            except:
                return 0,u("erreur de sauvegarde de %s" % fichier)

        # rpms
        if 'rpms_var' in dico_files:
            for rpm in dico_files['rpms_var']:
                # on ajoute le rpm si il n'est pas présent
                if rpm not in rpms.split('\n'):
                    rpms = rpms.strip() + '\n' + rpm +'\n'

            with open(Path(dest_dir).joinpath('fichiers_zephir', 'fichiers_variante'),'w') as fv_fh:
                fv_fh.write("\n".join((fichiers.strip(), "%%", rpms.strip())))
            # synchronisation des fichiers de dictionnaires
            self.parent.dictpool.sync_variante_paqs(id_variante)
        return 1,u("ok")

    def xmlrpc_del_files(self,cred_user,id_variante,dico_files,remove=False,passwd=None):
        """suppression de fichiers, patchs, dictionnaires d'une variante
        """
        self.parent.s_pool.check_var_credential(cred_user, id_variante)
        query = "select id,module,owner,passmd5 from variantes where id=%s"
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._del_files,db_client_failed,callbackArgs=[dico_files,cred_user,remove,passwd])

    def _del_files(self,data,dico_files,cred_user,remove,passwd):
        """supression de fichiers, patchs, dictionnaires,rpms d'une variante
        """
        id_variante = data[0][0]
        id_module = data[0][1]
        owner = data[0][2]
        pass_var = data[0][3]
        # si la variante ne nous appartient pas, on interdit la modification si mauvais mot de passe
        if owner != cred_user:
            # vérification du mot de passe
            if passwd != pass_var and pass_var not in [None,'',BLANK_MD5]:
                # mauvais mot de passe
                return 0,u('mot de passe incorrect pour cette variante')
        # on met en place les différents types de fichiers
        dest_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_variante)
        if remove:
            liste_remove = []
            # on récupère la liste des fichiers à supprimer existante
            if os.path.isfile(dest_dir+os.sep+'fichiers_zephir/removed'):
                f_rem = open(dest_dir+os.sep+'fichiers_zephir/removed')
                for fic in f_rem.read().strip().split('\n'):
                    liste_remove.append(fic)
                f_rem.close()
        # dictionnaires locaux
        for dico in dico_files['dicos_var']:
            try:
                if dico != "":
                    os.unlink(dest_dir+os.sep+'dicos'+os.sep+dico)
            except:
                return 0,u("erreur de suppression de %s" % dico)
        for template in dico_files['persos_var']:
            try:
                if template != "":
                    os.unlink(dest_dir+os.sep+'fichiers_perso'+os.sep+template)
            except:
                return 0,u("erreur de supression de %s" % template)
            # on supprime les droits associés si nécessaire
            self.parent.s_pool.del_file_perms(dest_dir,'fichiers_perso'+os.sep+template)
            # si demandé, on ajoute le template à la liste des fichiers à supprimer
            # sur le serveur cible
            if remove:
                fic_sup = os.path.join('fichiers_perso', template)
                if fic_sup not in liste_remove:
                    liste_remove.append(fic_sup)

        for patch in dico_files['patchs_var']:
            try:
                if patch != "":
                    os.unlink(dest_dir+os.sep+'patchs'+os.sep+patch)
            except:
                return 0,u("erreur de suppression de %s" % patch)
        # on reprend la liste des fichiers existants
        try:
            f=open(dest_dir+os.sep+'fichiers_zephir/fichiers_variante')
            old_content=f.read()
            f.close()
            fichiers=old_content.split('%%\n')[0]
            rpms=old_content.split('%%\n')[1]
        except:
            fichiers="""# section 1
# liste des fichiers à sauvegarder pour la variante
# (ne pas modifier sauf pour créer ou mettre à jour la variante)"""
            rpms="""# section 2
# inscrire les noms des paquetages qui seront installés à la mise à jour du serveur
# (ils doivent être présents sur le serveur de mise à jour)"""
        liste=fichiers.split('\n')

        for fichier in dico_files['fichiers_var']:
            localpath = "fichiers_zephir"
            if type(fichier) in (list, tuple) and len(fichier) == 2:
                localpath = fichier[1]
                fichier = fichier[0]
            # on supprime le fichier de la liste
            if fichier in liste:
                liste.remove(fichier)
                fichiers = "\n".join(liste)
            fic_path = os.path.join(dest_dir, localpath, os.path.basename(fichier.replace("\\","/")))
            # on efface le fichier
            try:
                if fichier != "":
                    if os.path.isdir(fic_path):
                        shutil.rmtree(fic_path)
                    elif os.path.isfile(fic_path):
                        os.unlink(fic_path)
            except:
                with open(Path(dest_dir).joinpath('fichiers_zephir', 'fichiers_variante'),'w') as fv_fh:
                    fv_fh.write("\n".join((fichiers.strip(), "%%", rpms.strip())))
                return 0,u("erreur de suppression de %s" % fichier)
            # on supprime les droits associés si nécessaire
            fic_sup = os.path.join(localpath,os.path.basename(fichier.replace("\\","/")))
            self.parent.s_pool.del_file_perms(dest_dir,fic_sup,True)
            # si demandé, on ajoute le fichier à la liste des fichiers à supprimer
            # sur le serveur cible
            if remove:
                fic_rem = fichier.replace("\\","/")
                if fic_rem not in liste_remove:
                    liste_remove.append(fic_rem)

        # rpms
        for rpm in dico_files['rpms_var']:
            # on supprime le rpm si il existe
            liste=rpms.split('\n')
            if rpm in liste:
                liste.remove(rpm)
                rpms = "\n".join(liste)
            else:
                with open(Path(dest_dir).joinpath('fichiers_zephir', 'fichiers_variante'),'w') as fv_fh:
                    fv_fh.write("\n".join((fichiers.strip(), "%%", rpms.strip())))
                return 0,u("rpm non trouvé dans la liste : %s" % rpm)

        with open(Path(dest_dir).joinpath('fichiers_zephir', 'fichiers_variante'),'w') as fv_fh:
            fv_fh.write("\n".join((fichiers.strip(), "%%", rpms.strip())))
        if remove and liste_remove:
            # liste des fichiers à supprimer
            f=open(dest_dir+os.sep+'fichiers_zephir/removed', 'w')
            f.write("\n".join(liste_remove))
            f.close()

        return 1,u("ok")

    def xmlrpc_get_var_file(self,cred_user,id_var,id_module,path,show_details=False):
        """renvoie le contenu d'un fichier de variante"""
        # définition du chemin de la variante
        self.parent.s_pool.check_var_credential(cred_user, id_var)
        query="select variantes.id,variantes.module from variantes where variantes.id=%s" % id_var
        return self.dbpool.runQuery(query, (int(id_var),)).addCallbacks(self._get_var_file,db_client_failed,callbackArgs=[path,show_details])

    def _get_var_file(self,data,path,show_details):
        id_var = data[0][0]
        id_module = data[0][1]
        try:
            dest_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"+os.sep+str(id_var)
        except:
            return 0,u("""lecture du fichier: paramètres non valides""")
        # on lit le fichier
        try:
            dest_path = os.path.join(dest_dir, path)
            # cas d'un répertoire
            if os.path.isdir(dest_path):
                dirfiles = os.listdir(dest_dir + os.sep + path)
                content = []
                if show_details:
                    for f in dirfiles:
                        f_local = os.path.join(dest_dir,path,f)
                        f_info = config.get_file_info(f_local)
                        content.append((f,f_info))
                else:
                    content = dirfiles
                return 1, u(content)
            elif os.path.isfile(dest_path):
                if istextfile(dest_path):
                    f=open(dest_dir + os.sep + path, 'rb')
                    content=f.read()
                    f.close()
                    content = base64.encodebytes(content)
                else:
                    content = "BINARY"
                return 1, content
            else:
                raise FileNotFoundError(f"{dest_path} Not Found.")
        except:
            #traceback.print_exc()
            return 0,u("""erreur de lecture du fichier""")

    def xmlrpc_export_variante(self,cred_user,id_variante):
        """envoie le contenu d'une variante sur un autre zephir"""
        # on recherche le répertoire ou la variante est stockée
        query = """select id,module from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_variante),)).addCallbacks(self._export_variante,db_client_failed)

    def _export_variante(self,data):
        """crée l'archive de la variante et le renvoie"""
        if data != []:
            id_variante = data[0][0]
            id_module = data[0][1]
            # répertoire de la variante
            parent_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"
            var_dir=parent_dir+os.sep+str(id_variante)
            # on crée une archive de ce répertoire
            archive=str(time.time())
            if os.path.isdir(var_dir):
                # création du fichier tar à envoyer
                cmd_tar = ['cd ',var_dir,';','tar','-chzf','/tmp/'+archive+'.tgz','*']
                cmd_tar.append('2>&1 >/dev/null')
                # exécution de tar
                res=os.system(" ".join(cmd_tar))
                if res != 0:
                    return 0, u("""erreur de création de l'archive %s.tgz dans /tmp""" % (archive))
                else:
                    # calcul et stockage d'un checksum md5 de l'archive
                    cmd_checksum = """cd /tmp/; md5sum -b %s.tgz > %s.md5""" % (archive,archive)
                    res=os.system(cmd_checksum)
                    # on stocke en mémoire les données
                    try:
                        # le checksum
                        f = open('/tmp'+os.sep+archive+'.md5')
                        data1 = f.read()
                        f.close()
                        os.unlink('/tmp'+os.sep+archive+'.md5')
                        # l'archive
                        f = open('/tmp'+os.sep+archive+'.tgz')
                        data2 = f.read()
                        f.close()
                        os.unlink('/tmp'+os.sep+archive+'.tgz')
                        return 1,u([archive,base64.encodebytes(data1),base64.encodebytes(data2)])
                    except Exception as e:
                        return 0,u("""erreur lors de l'envoi de l'archive : %s""" % str(e))
            else:
                return 0, u("""répertoire %s introuvable""" % var_dir)
        else:
            return 0, u("""variante inexistante""")

    def xmlrpc_import_variante(self,cred_user,pwd_var,id_local,id_distant,zephir_distant,login_distant,pwd_distant):
        """récupère le contenu d'une variante sur un autre zephir"""
        # création d'un proxy vers le serveur zephir distant
        z=EoleProxy("https://%s:%s@%s:%s" % (login_distant,pwd_distant,zephir_distant,config.PORT_ZEPHIR))
        # on vérifie l'existence des variantes des 2 cotés
        try:
            res=z.modules.get_variante(id_distant)
        except:
            return 0,u("""permissions insuffisantes""")
        if res[0] == 0:
            return 0,u("""erreur lors de la recherche de la variante d'origine""")

        # on recherche le module de la variante locale
        query = """select id, module, owner, passmd5 from variantes where id=%s"""
        return self.dbpool.runQuery(query, (int(id_local),)).addCallbacks(self._import_variante,db_client_failed,callbackArgs=[z,id_distant,cred_user,pwd_var])

    def _import_variante(self,data,proxy,id_distant,cred_user,pwd_var):
        """demande l'envoi de l'archive et met en place les fichiers"""
        if data == []:
            return 0, u("""variante locale non trouvée""")
        else:
            id_variante = data[0][0]
            id_module = data[0][1]
            owner = data[0][2]
            passmd5 = data[0][3]
            # on vérifie le mot de passe de la variante
            if owner != cred_user and pwd_var != passmd5:
                return 0,u("""mauvais mot de passe de variante""")
            else:
                # définition du chemin de destination
                parent_dir=os.path.abspath(config.PATH_MODULES)+os.sep+str(id_module)+os.sep+"variantes"
                var_dir = parent_dir+os.sep+str(id_variante)
                # on regarde si des fichiers existent déjà
                if not os.path.exists(var_dir):
                    return 0,u("""répertoire de la variante de destination non trouvé""")
                # onrécupère les données de la variante source
                res=proxy.modules.export_variante(id_distant)
                if res[0]==0:
                    return 0,u("""erreur lors de la récupération de la variante""")
                else:
                    # nom de l'archive
                    archive=res[1][0]
                    # somme md5
                    var_data=base64.decodebytes(to_bytes(res[1][1]))
                    # contenu de l'archive
                    md5_data=base64.decodebytes(to_bytes(res[1][2]))
                    try:
                        # on supprime l'ancien répertoire de la variante
                        shutil.rmtree(var_dir)
                        # on recrée un répertoire vide
                        os.makedirs(var_dir)
                        # on recrée l'archive et le fichier md5
                        f=open(var_dir+os.sep+archive+'.tgz','w')
                        f.write(md5_data)
                        f.close()
                        f=open(var_dir+os.sep+archive+'.md5','w')
                        f.write(var_data)
                        f.close()
                        # on vérifie la somme md5
                        cmd_md5 = """cd %s; md5sum -c %s.md5 2>&1 > /dev/null""" % (var_dir,archive)
                        res=os.system(cmd_md5)
                        if res != 0:
                            return 0,u("""archive corrompue""")
                        # on décompresse l'archive
                        cmd_tar = """cd %s ; tar -xzf %s.tgz > /dev/null""" % (var_dir,archive)
                        res=os.system(cmd_tar)
                        if res != 0:
                            return 0,u("""erreur de décompression de l'archive""")
                        # on supprime les fichiers temporaires
                        os.unlink(var_dir+os.sep+archive+'.tgz')
                        os.unlink(var_dir+os.sep+archive+'.md5')
                    except Exception as e:
                        return 0,u("""erreur de mise en place des fichiers de la variante : %s""" % str(e))
                # l'import s'est terminé correctement
                return 1,u("ok")

    def xmlrpc_upgrade_modules(self, cred_user, version_src, version_dst):
        """fonction de copie des personnalisations entre 2 versions de la distribution
        - copie les dictionnaires locaux déclarés dans la version source
        - crée des variantes équivalentes pour chaque module existant dans les 2 versions
        - renseigne les informations de migration de variantes pour celles créées
        """
        try:
            version_src=int(version_src)
            version_dst=int(version_dst)
            assert version_src in config.DISTRIBS, "Version de distribution source inconnue"
            assert version_dst in config.DISTRIBS, "version de distribution de destination inconnue"
            assert version_src < version_dst, "La version de destination doit être supérieure à la version source"
        except ValueError:
            return 0, u("Versions source ou destination non valides")
        except AssertionError as err:
            return 0, u(str(err))

        infos = {'modules':[], 'variantes':[], 'errors':[], 'dicos':[]}
        if version_src >= 6:
            # versions 2.4.1 et >, copie automatique des dictionnaires locaux de la version précédente
            try:
                src_local_dir = os.path.join(config.ROOT_DIR, 'dictionnaires', config.DISTRIBS[version_src][1], 'local')
                dst_local_dir = os.path.join(config.ROOT_DIR, 'dictionnaires', config.DISTRIBS[version_dst][1], 'local')
                def merge(arg, dirname, fnames):
                    destdir = dirname.replace(src_local_dir, dst_local_dir, 1)
                    if not os.path.exists(destdir):
                        os.makedirs(destdir)
                    for dico in fnames:
                        if not os.path.exists(os.path.join(destdir, dico)) and dico.endswith('.xml'):
                            shutil.copy2(os.path.join(dirname, dico), os.path.join(destdir, dico))
                            infos['dicos'].append(os.path.join(destdir.replace(dst_local_dir, "", 1), dico).lstrip(os.sep))
                os.path.walk(src_local_dir, merge, "")
                # copie et demande de prise en compte des dictionnaires par le backend
                self.parent.dictpool.update_dicts(version_dst, 'local')
            except:
                log.msg('Erreur lors de copie des dictionnaires locaux de %s vers %s' % (config.DISTRIBS[version_src][1], config.DISTRIBS[version_dst][1]))
                traceback.print_exc()

        query = """select id, libelle, version from modules"""
        return self.dbpool.runQuery(query).addCallbacks(self._upgrade_modules,db_client_failed,callbackArgs=[version_src, version_dst, cred_user, infos])

    def _upgrade_modules(self, modules_db, version_src, version_dst, cred_user, infos):
        """récupère les informations des modules à traiter
        """
        modules=[]
        for module in modules_db:
            if module[2] == None:
                version = 1
            else:
                version = module[2]
            modules.append({'id':module[0],'libelle':module[1],'version':version})
        query = """select id, libelle, module from variantes"""
        return self.dbpool.runQuery(query).addCallbacks(self._upgrade_modules2,db_client_failed,callbackArgs=[version_src, version_dst, modules, cred_user, infos])

    def _upgrade_modules2(self, variantes_db, version_src, version_dst, modules, cred_user, infos):
        """copie les variantes et met en place les informations de migration
        """
        # récupération des variantes sources (2.0)
        mods = {}
        variantes = {}
        variantes_dst = {}
        for module in modules:
            tag = ''
            mod_name = module['libelle'][:module['libelle'].rindex('-')]
            if module['version'] == version_src:
                tag = 'src'
                # recherche des variantes existantes
                for var in variantes_db:
                    if var[2] == module['id']:
                        if var[1] != 'standard':
                            if module['id'] in variantes:
                                variantes[module['id']].append((var[0],var[1]))
                            else:
                                variantes[module['id']] = [(var[0],var[1])]

            elif module['version'] == version_dst:
                mod_name = module['libelle']
                mod_name = mod_name[:mod_name.rindex('-')]
                tag = 'dst'
                # recherche déjà migrées
                for var in variantes_db:
                    if var[2] == module['id']:
                        if var[1] != 'standard':
                            if module['id'] in variantes_dst:
                                variantes_dst[module['id']].append(var[1])
                            else:
                                variantes_dst[module['id']] = [var[1]]
            # stockage de la correspondance des modules source/dest
            if tag != '':
                if mod_name not in mods:
                    mods[mod_name] = {tag:module['id']}
                else:
                    mods[mod_name][tag] = module['id']
        # transfert des données du module source
        d_list = []
        for modname, data in list(mods.items()):
            if 'src' in data and 'dst' in data:
                # fichiers des valeurs par défaut du module
                module_eol_src = os.path.join(os.path.abspath(config.PATH_ZEPHIR), 'modules', str(data['src']), 'module.eol')
                if os.path.isfile(module_eol_src):
                    module_eol_dst = os.path.join(os.path.abspath(config.PATH_ZEPHIR), 'modules', str(data['dst']), 'module.eol')
                    if not os.path.isfile(module_eol_dst):
                        infos['modules'].append(modname)
                        shutil.copy(module_eol_src, module_eol_dst)
                if data['src'] in variantes:
                    for var in variantes[data['src']]:
                        id_var, libelle = var
                        copy_needed = True
                        if libelle in variantes_dst.get(data['dst'],[]):
                            # si la variante existe déjà dans le module destination, on ne fait rien
                            copy_needed = False
                        if copy_needed:
                            # libelle de la nouvelle variante
                            d_list.append(defer.maybeDeferred(self.xmlrpc_copy_variante, cred_user, data['dst'],id_var,u(libelle),"",True))
        if d_list:
            d = defer.DeferredList(d_list, consumeErrors=True)
            return d.addBoth(self._upgrade_modules3, infos)
        return 1, u(infos)

    def _upgrade_modules3(self, result, infos):
        """vérifie le résultat des créations avec copie de variantes
        """
        # vérification du résultat des copies de variantes
        for copy_result in result:
            err = None
            copy_ok, var_result = copy_result
            if not copy_ok:
                # traceback pendant la copie
                err = str(var_result.getErrorMessage())
            else:
                code_ret, msg = var_result
                if code_ret == 0:
                    # la copie a retourné un message d'erreur
                    err = str(msg)
                else:
                    # copie ok
                    infos['variantes'].append(int(msg))
            if err is not None:
                log.msg('Erreur lors de la copie de variante : %s', str(err))
                infos['errors'].append(str(err))
        return 1, u(infos)
