# -*- 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
#
# lib_backend.py
#
# classes de base pour la gestion des serveurs sous zephir
#
###########################################################################
"""fonctions utiles pour l'accès à la base de données (via adbapi)
"""
from zephir.backend import config
from zephir.backend.config import log
import os, sys, time, string, traceback, apt_pkg, re, shutil
from pathlib import Path

try:
    env_perso = {}
    exec(compile(open(os.path.join(config.ROOT_DIR, 'migration_perso.py'), "rb").read(), os.path.join(config.ROOT_DIR, 'migration_perso.py'), 'exec'),env_perso)
    migration_perso = env_perso['migration_perso']
    destinations_perso = env_perso['destinations_perso']
except:
    migration_perso = {}
    destinations_perso = []
from zephir.lib_zephir import lock, unlock, is_locked, lock_dir
from zephir.utils.creolewrap import ZephirDict
from zephir.utils.convert import to_bytes
from zephir.monitor.agentmanager.util import get_md5files
from creole.loader1 import load_json
from twisted.internet import reactor
from twisted.internet.utils import getProcessOutputAndValue
import psycopg2 as PgSQL
import base64
from hashlib import md5
from configparser import ConfigParser
from glob import glob
from io import BytesIO

class ResourceAuthError(Exception):
    pass

class ServiceError(Exception):
    pass

class ParamsLock:
    def __init__(self, id_serveur):
        self.lockname = 'server_params_%s' % str(id_serveur)
    def __enter__(self):
        wait_time = 0
        while is_locked(self.lockname) and (wait_time <= 20):
            # on attend au maximum 20 secondes que la ressource soit libérée
            # après 20 secondes, on considére que l'appel concurrent est en échec
            time.sleep(0.25)
            wait_time += 0.25
        # on interdit temporairement d'autres modification des paramètres du serveur
        lock(self.lockname)
    def __exit__(self, type, value, traceback):
        unlock(self.lockname)

class CxPool(dict):
    """dictionnaire des connexions postgresql en cours"""

    def create(self):
        cx = PgSQL.connect(database=config.DB_NAME,user=config.DB_USER,password=config.DB_PASSWD)
        cu = cx.cursor()
        self[cu] = cx
        return cu

    def commit(self,cu):
        cu.close()
        self[cu].commit()
        self[cu].close()
        del(self[cu])

    def rollback(self,cu):
        cu.close()
        self[cu].rollback()
        self[cu].close()
        del(self[cu])

    def close(self,cu):
        cu.close()
        self[cu].close()
        del(self[cu])

class AptChecker:
    """gestionnaire de vérification d'état des paquets d'un serveur
    """
    def __init__(self):
        self.pkg_file = {}
        self.packages = {}
        self.timestamp = {}
        check_launched = False
        for zephir_version, data in list(config.DISTRIBS.items()):
            codename, version, maintained = data
            envole_version = config.ENVOLE_VERSION.get(zephir_version, None)
            # lecture des fichiers générés pour les différents serveurs de mise à jour
            if zephir_version not in self.pkg_file:
                self.pkg_file[zephir_version] = {'ubuntu':{}, 'eole':{}, 'envole':{}}
                self.packages[zephir_version] = {'ubuntu':{}, 'eole':{}, 'envole':{}}
                self.timestamp[zephir_version] = {'ubuntu':{}, 'eole':{}, 'envole':{}}
            for serv_maj in config.SERVEURS_MAJ_CLIENTS:
                self.pkg_file[zephir_version]['ubuntu'][serv_maj] = \
                     os.path.join(os.path.abspath(config.PATH_ZEPHIR),'packages_ubuntu_%s_%s.ini' % (codename, serv_maj))
                self.pkg_file[zephir_version]['eole'][serv_maj] = \
                     os.path.join(os.path.abspath(config.PATH_ZEPHIR),'packages_eole_%s_%s.ini' % (version, serv_maj))
                if envole_version:
                    self.pkg_file[zephir_version]['envole'][serv_maj] = \
                         os.path.join(os.path.abspath(config.PATH_ZEPHIR),'packages_envole_%s_%s.ini' % (envole_version, serv_maj))
                self.load_packages(zephir_version, 'ubuntu', serv_maj)
                self.load_packages(zephir_version, 'eole', serv_maj)
                if envole_version:
                    # dépôt envole séparé à partir de la version 7 (Eole 2.4.1)
                    self.load_packages(zephir_version, 'envole', serv_maj)
        # création d'une regexp compilée pour les paquets à ignorer
        if len(config.held_packages) > 0:
            self.held_pattern = re.compile("|".join(config.held_packages))
        else:
            self.held_pattern = None

    def load_packages(self, zephir_version, level, serv_maj):
        """fonction de chargement des paquets d'un serveur de mise à jour
        pour une version de la distribution EOLE
        """
        if serv_maj in self.pkg_file[zephir_version][level]:
            try:
                assert os.path.isfile(self.pkg_file[zephir_version][level][serv_maj])
                pkg_timestamp = os.path.getmtime(self.pkg_file[zephir_version][level][serv_maj])
                stored_timestamp = self.timestamp[zephir_version].get(level, {}).get(serv_maj, 0)
                if serv_maj not in self.packages[zephir_version].get(level, {}) or pkg_timestamp != stored_timestamp:
                    # pas encore de données ou fichier modifié
                    log.msg("Chargement de liste de paquets : %s" % self.pkg_file[zephir_version][level][serv_maj])
                    self.timestamp[zephir_version][level][serv_maj] = os.path.getmtime(self.pkg_file[zephir_version][level][serv_maj])
                    self.packages[zephir_version][level][serv_maj] = ConfigParser()
                    self.packages[zephir_version][level][serv_maj].read(self.pkg_file[zephir_version][level][serv_maj])
            except:
                if os.path.isfile(self.pkg_file[zephir_version][level][serv_maj]):
                    # données corrompues ?
                    log.msg("Erreur de chargement de %s (corrompu ?), fichier supprimé" % self.pkg_file[zephir_version][level][serv_maj])
                    os.unlink(self.pkg_file[zephir_version][level][serv_maj])
                # infos non disponibles sur le serveur  pour cette version ?
                self.timestamp[zephir_version][level][serv_maj] = 0
                if serv_maj in self.packages[zephir_version][level]:
                    del(self.packages[zephir_version][level][serv_maj])

    def check_min_version(self, current_version, min_version):
        apt_pkg.init_system()
        if apt_pkg.version_compare(current_version, min_version) >= 0:
            return True
        return False

    def get_serv_maj(self, serv_maj_client, version, level):
        """vérifie les informations du serveur de mise à jour
        et sélectionne un autre serveur si besoin
        """
        if level == "ubuntu":
            aff_distrib = config.DISTRIBS[version][0]
        elif level == "envole":
            aff_distrib = config.ENVOLE_VERSION.get(version, "")
        else:
            aff_distrib = config.DISTRIBS[version][1]

        serv_maj_ok = True
        serv_maj_client = serv_maj_client.split('| ')
        # on teste d'abord avec le premier serveur de mise à jour
        # déclaré dans la configuration du serveur
        serv_maj = serv_maj_client[0]
        if serv_maj in config.SERVEURS_MAJ_CLIENTS:
            self.load_packages(version, level, serv_maj)
        if serv_maj not in self.packages[version][level]:
            # serveur de maj inconnu ou ne gère pas cette version, on en recherche un valide
            serv_maj_ok = False
            # on recherche dans les serveurs gérés en commençant par ceux utilisés par le client
            serveurs = []
            for serv in serv_maj_client:
                if serv in config.SERVEURS_MAJ_CLIENTS:
                    serveurs.append(serv)
            for serv in config.SERVEURS_MAJ_CLIENTS:
                if serv not in serveurs:
                    serveurs.append(serv)
            for serv_maj_cli in serveurs:
                if serv_maj_cli != serv_maj:
                    self.load_packages(version, level, serv_maj_cli)
                    if serv_maj_cli in self.packages[version][level]:
                        serv_maj_ok = True
                        log.msg("Serveur de mise à jour", serv_maj, " : pas d'informations pour %s %s. utilisation de %s" % \
                               (level.capitalize(), aff_distrib, serv_maj_cli))
                        serv_maj = serv_maj_cli
                        break
        return serv_maj_ok, serv_maj

    def check_packages(self, id_serv, maj_infos, client_pkg_file, debnames, version, serv_maj_client,
                       serv_maj_ubuntu="", serv_maj_envole="", type_maj = 'complete', show_installed = False):
        """vérifie l'état de mise à jour des paquets
        type_maj : type de mise à jour (minimale/complete)
        show_installed : si True, renvoie la liste de tous les paquets installés
                         par défaut, seulement les paquets non à jour
        """
        # recherche de serveurs ayant des information sur les paquets ubuntu et eole de cette version
        serv_maj_ok, serv_ubuntu = self.get_serv_maj(serv_maj_ubuntu or serv_maj_client, version, 'ubuntu')
        if serv_maj_ok:
            serv_maj_ok, serv_eole = self.get_serv_maj(serv_maj_client, version, 'eole')
            if serv_maj_ok:
                try:
                    # recherche d'informations pour envole si disponible
                    assert version in config.ENVOLE_VERSION
                    serv_envole_ok, serv_envole = self.get_serv_maj(serv_maj_envole or serv_maj_client, version, 'envole')
                    assert serv_envole_ok == True
                    assert serv_envole in self.timestamp[version]['envole']
                    envole_tstamp = self.timestamp[version]['envole'][serv_envole]
                except AssertionError as err:
                    # sinon, dépôts envole = dépôts eole
                    serv_envole = serv_eole
                    envole_tstamp = self.timestamp[version]['eole'][serv_eole]
        if not serv_maj_ok:
            # infos manquantes pour eole ou ubuntu
            log.msg("Serveur %s : pas d'informations valides sur les version de paquets parmi les serveur de mises à jour déclarés" % str(id_serv))
        if not serv_maj_ok or not os.path.isfile(client_pkg_file):
            # pas de fichier remonté par le serveur ou pas de données
            # sur les serveurs de mise à jour !
            if maj_infos is not None:
                return maj_infos[0], maj_infos
            else:
                return [], None

        serveurs_maj = {'ubuntu':serv_ubuntu, 'eole':serv_eole, 'envole':serv_envole}
        depot_timestamp = {'eole':self.timestamp[version]['eole'][serv_eole],
                           'ubuntu':self.timestamp[version]['ubuntu'][serv_ubuntu],
                           'envole':envole_tstamp}
        client_changed = True
        depot_changed = True
        if os.path.isfile(client_pkg_file):
            md5_packs = md5(open(client_pkg_file, 'rb').read()).hexdigest()

        if maj_infos:
            # on regarde si les données à vérifier ont changé depuis la dernière vérification
            if maj_infos[1] == type_maj and maj_infos[2] == md5_packs:
                client_changed = False
                depot_changed = False
                for level, serv_maj in list(serveurs_maj.items()):
                    if maj_infos[3].get(level, '') != serv_maj:
                        # le serveur de mise à jour utilisé n'est plus le même
                        client_changed = True
                    else:
                        # on regarde si les données du serveur de maj ont changé
                        if maj_infos[4].get(level,'') != depot_timestamp[level]:
                            depot_changed = True
            if debnames == [] and not show_installed:
                # vérification globale de l'état de mise à jour du serveur
                # si on a déjà des infos et que rien n'a changé, on ne fait pas de traitements
                if not depot_changed and not client_changed:
                    return maj_infos[0], maj_infos

        # creation de la liste des paquets installés (/à vérifier si debnames non vide)
        pkg_liste = []
        if os.path.isfile(client_pkg_file):
            for line in open(client_pkg_file):
                pak_info = line.split()
                if debnames != []:
                    # travail sur des paquets particuliers
                    if pak_info[0] in debnames:
                        pkg_liste.append(pak_info)
                        if len(pkg_liste) == len(debnames):
                            # on a trouvé tous les paquets demandés : on sort
                            break
                else:
                    pkg_liste.append(line.split())

        # md5 des paquets installés
        check_needed = False
        if depot_changed or maj_infos is None or client_changed == True or debnames != [] or show_installed == True:
            # Des informations ne sont pas à jour ou on a demandé les infos sur des paquets particuliers
            check_needed = True
        liste_pack = []
        if check_needed:
            outdated = []
            apt_pkg.init_system()
            for pkg_name, pkg_version in pkg_liste:
                if (self.held_pattern is None) or (not self.held_pattern.match(pkg_name)):
                    try:
                        # les paquets n'ayant jamais eu de mise à jour ne sont pas présents
                        maj_version = ""
                        for level in ('envole', 'eole', 'ubuntu'):
                            serv_maj = serveurs_maj[level]
                            if serv_maj in self.packages[version][level] and \
                               self.packages[version][level][serv_maj].has_section(pkg_name):
                                try:
                                    serveur_version = self.packages[version][level][serv_maj].get(pkg_name, type_maj)
                                except:
                                    if type_maj != 'minimum':
                                        # version complète, l'information devrait être présente
                                        log.msg('Informations de version du paquet %s non trouvées' % (pkg_name))
                                        traceback.print_exc()
                                if maj_version:
                                    # présent sur les 2 serveurs, on prend le paquet le plus récent
                                    if apt_pkg.version_compare(serveur_version, maj_version) > 0:
                                        maj_version = serveur_version
                                else:
                                    maj_version = serveur_version
                        if maj_version:
                            # comparaison avec la version installée
                            if apt_pkg.version_compare(maj_version, pkg_version) > 0:
                                # on renvoie, le paquet, la version installée, et la version sur le serveur de maj
                                liste_pack.append((pkg_name, pkg_version, maj_version))
                                outdated.append((pkg_name, pkg_version, maj_version))
                                continue
                            elif show_installed == True:
                                liste_pack.append((pkg_name, pkg_version, maj_version))
                                continue
                    except:
                        traceback.print_exc()
                    # listing des paquets installés, on renvoie le paquet
                    # même si il n'est pas dans les repository de maj
                    if show_installed == True:
                        liste_pack.append((pkg_name, pkg_version, pkg_version))
            maj_infos = (outdated, type_maj, md5_packs, serveurs_maj, depot_timestamp)
        else:
            # pas de changement, on reprend la liste précédente
            liste_pack = maj_infos[0]
        return liste_pack, maj_infos


cx_pool = CxPool()
# maj_checker = AptChecker()

class Serveur:
    """classe utilitaire pour récupérer diverses données sur un serveur"""

    def __init__(self, pool, id_s, cu=None, data=None):
        """initialise l'objet serveur et récupère les infos
        """
        self.version = 'creole3'
        self.module_version = None
        # récupération des serveurs
        self.id_s = int(id_s)
        # date de dernière modification du serveur
        self.modified = time.time()
        # date de création de l'objet
        self.created = time.time()
        # serveur inactif ? (pas d'alerte)
        self.no_alert = False
        # lien sur le pool de serveurs
        self.pool = pool
        # liste des paquets non à jour
        self.maj_infos = None
        # liste des paquets de dictionnaires non activés
        self.installed_packages = None
        self.dict_packages = None
        # cache de la configuration Creole
        self.dico = None
        self.dico_mtime = None
        self.last_mode = None
        # set des clés par défaut pour le dictionnaire params
        self.default_params = set(config.DEFAULT_PARAMS)
        cursor = cu
        if cu == None:
            cursor = cx_pool.create()
        try:
            if data == None:
                # le rne ne peut pas être modifié : on le stocke une fois pour toutes
                cursor.execute("select rne from serveurs where id=%s", (int(self.id_s),))
                data = cursor.fetchone()
                self.rne = data[0]
                # appel de la fonction update_data pour les données modifiables
                self.update_data(cursor)
            else:
                self.rne = data[0]
            self.confdir = os.path.join(config.PATH_ZEPHIR,'conf',self.rne, str(self.id_s))
            if cu == None:
                cx_pool.close(cursor)
        except:
            traceback.print_exc()
            if cu == None:
                cx_pool.close(cursor)

    def update_data(self, cu=None, data=None):
        """recharge les données des serveurs
        """
        # récupération de l'établissement et de l'état
        cursor = cu
        if cu == None:
            cursor = cx_pool.create()
        try:
            if data == None:
                cursor.execute("select libelle, module_actuel, variante, timeout, etat, md5s, maj, no_alert from serveurs where id=%s", (int(self.id_s),))
                data = cursor.fetchone()
            # données du serveur
            self.libelle = data[0]
            self.id_mod = int(data[1])
            self.id_var = int(data[2])
            try:
                self.md5s = int(data[5])
            except:
                self.md5s = -1
            try:
                self.maj = int(data[6])
            except:
                self.maj = -1
            try:
                self.timeout = int(data[3])
            except:
                self.timeout = 0
            if data[4] == None:
                self.status = -1
            else:
                self.status = int(data[4])
            try:
                if str(data[7]) == '1':
                    self.no_alert = True
                else:
                    self.no_alert = False
            except:
                pass
            # libelle etablissement
            cursor.execute("select libelle from etablissements where rne=%s", (self.rne,))
            data = cursor.fetchone()
            self.etab = data[0]
            # libelle module
            cursor.execute("select libelle, version from modules where id=%s", (int(self.id_mod),))
            data = cursor.fetchone()
            self.module = data[0]
            # numéro de distribution Eole (1:Eole 1.X, 2:Eole 2.0, 3:Eole 2.1, ...)
            self.module_version = int(data[1])
            # détection de la version de creole utilisée
            self.version = config.CREOLE_VERSIONS[self.module_version]
            # libelle variante
            cursor.execute("select libelle from variantes where id=%s", (int(self.id_var),))
            data = cursor.fetchone()
            self.variante = data[0]
            self.modified = time.time()
            if cu == None:
                cx_pool.close(cursor)
        except:
            traceback.print_exc()
            if cu == None:
                cx_pool.close(cursor)

    def __repr__(self):
        """affichage du serveur
        """
        return """%s => %s (%s)""" % (self.id_s, self.module, self.rne)

    def __str__(self):
        """affichage du serveur
        """
        return """%s => %s (%s) - %s (%s)""" % (self.id_s, self.libelle, self.module, self.etab, self.rne)

    #####################
    ## LECTURE DE DONNEES

    def get_libelle(self):
        return self.libelle

    def get_rne(self):
        return self.rne

    def get_etab(self):
        return self.etab

    def get_module(self):
        return self.module

    def get_variante(self):
        return self.variante

    def get_timeout(self):
        return self.timeout

    def get_confdir(self):
        return self.confdir

    def get_config(self, mode="modif_config", encode=False, groups=None, separators=None, config=None):
        """retourne la configuration eole du serveur (zephir.eol)
        """
        # actualisation si des dictionnaires ont changé
        dico = self.check_dict(mode, encode, groups, separators, config)
        # vérification des valeurs chargées en mémoire
        return self.load_conf(mode, dico)

    def check_dict(self, mode, encode=False, groups=None, separators=None, config_=None):
        """récupération des dictionnaires de configuration du serveur
        """
        # avant traitement, appel au script de purge (creole3)
        self.backup_dicts()
        # vérification de la date de mise à jour des dictionnaires
        dicts = glob('%s/dicos/*/*.xml' % self.confdir)
        last_mtime = 0.0
        # on enregistre la liste des dictionnaires pris en compte
        # et leur dernière date de modification
        mtimes = set()
        for d in dicts:
            d_mtime = os.stat(d).st_mtime
            mtimes.add((d, d_mtime))
        if config.CREOLE_CACHE:
            # gestion de la date de dernière mise à jour du cache creole
            if self.dico_mtime is None or mtimes != self.dico_mtime:
                self.dico_mtime = mtimes
                # dictionnaires non à jour, on réinitialise
                self.dico = None
        if self.dico is None or config.CREOLE_CACHE == False:
            # Des dictionnaires ont été modifiés : on réinitialise l'objet dictionnaire
            # + chargement de la configuration dans le bon mode
            dicos = []
            for rep in ['module','variante','local','package','var_package']:
                if os.path.exists(os.path.join(self.confdir,'dicos',rep)):
                    dicos.append(os.path.join(self.confdir,'dicos',rep))
            if os.path.isfile(os.path.join(self.confdir,'cle_publique')) and mode == "modif_config":
                force_instanciate = 'oui'
            else:
                force_instanciate = 'non'
            dico = ZephirDict(dicos, self.confdir, mode, self.version, force_instanciate, eole_version=self.module_version, groups=groups, separators=separators, dico=config_)
            if config.CREOLE_CACHE:
                self.dico = dico
            return dico
        else:
            # le dictionnaire est déjà en cache
            return self.dico

    def backup_dicts(self):
        """met de côté d'éventuels dictionnaires 'indésirables' dans le
        répertoire 'dicos/local' du serveur (copiés à la main sur Zéphir,
        ou remontés par le client avant correction de Zéphir) cf #8264.
        """
        serv_dictdir = os.path.join(self.get_confdir(), 'dicos', 'local')
        backup_dir = os.path.join(self.get_confdir(), 'dicos', 'backup')
        dicos = glob(os.path.join(serv_dictdir, '*.xml'))
        # tous les fichiers xml devraient être des liens vers la bibliothèque
        for dico in dicos:
            if not os.path.islink(dico):
                # déplacement du dictionnaire indésirable
                if not os.path.isdir(backup_dir):
                    os.makedirs(backup_dir)
                os.rename(dico, os.path.join(backup_dir, os.path.basename(dico)))
                log.msg('serveur {0}: dictionnaire indésirable ({1}) déplacé de dicos/local vers dicos/backup'\
                        .format(self.id_s, os.path.basename(dico)))

    def parsedico(self, mode='modif_config', separator="| ", encode=False):
        if self.dico is not None:
            if config.CREOLE_CACHE and self.dico.mode == mode:
                # dans tous les cas, on appelle check_dict
                # au cas ou un dictionnaires aurait été modifié
                dico = self.check_dict(mode, encode)
                return dico.parsedico(separator=separator)
            if config.CREOLE_CACHE == False and self.last_mode == mode:
                if separator != "| ":
                    # cache partiel, les valeurs sont stockées avec le séparateur "| "
                    final_dict = {}
                    for varname, values in list(self.dico.items()):
                        final_dict[varname] = separator.join([val.strip() for val in values.split("| ")])
                    return final_dict
                return self.dico
        dico = self.get_config(mode, encode)
        return dico.parsedico(separator=separator)

    def load_conf(self, mode, dico=None):
        """vérifie que les valeurs déjà chargées correspondent au mode demandé
        """
        if config.CREOLE_CACHE:
            if self.dico.mode != mode or not mode.startswith('modif'):
                # rechargement des valeurs dans le mode demandé
                # on recharge si on change de mode ou si on veut repartir des
                # valeurs d'origine (le fichier de module/variante est susceptible d'avoir changé)
                self.dico.load_values(mode)
            return self.dico
        else:
            if self.last_mode != mode or not mode.startswith('modif'):
                dico.load_values(mode)
                # mode de cache réduit, on ne conserve que le dictionnaire de valeurs
                self.dico = dico.parsedico(separator="| ")
                self.last_mode = mode
        return dico

    def save_config(self, dico_zeph, mode='config', encode=False, force=False, new_method=True):
        """sauvegarde la configuration eole du serveur (zephir.eol)
        force : pas de validation des contraintes (pour une variante, la conf est incomplète)
        new_method : utiliser le nouveau calcul du md5 de la configuration
        """
        try:
            if mode in ['config','modif_config']:
                fic_zephir = os.path.join(self.confdir,'zephir.eol')
                key = 'config_ok'
                dict_mode = 'modif_config'
            elif mode in ['dico','modif_dico']:
                # si gen_dico, sauvegarde de dico.eol dans la variante
                fic_zephir = os.path.abspath(config.PATH_MODULES)+os.sep+str(self.id_mod)+os.sep+'variantes'+os.sep+str(self.id_var)+os.sep+'dico.eol'
                key = 'dico_ok'
                dict_mode = 'modif_dico'
            elif mode in ['migration','modif_migration']:
                fic_zephir = os.path.join(self.confdir,'migration.eol')
                key = 'migration_ok'
                dict_mode = 'modif_migration'
            # sauvegarde des données dans le fichier de configuration
            dico_zeph.save(fic_zephir, encode=encode, force=force)
            if dict_mode == "modif_config":
                # mise à jour du cache de configuration du serveur (valeurs actuelles)
                if config.CREOLE_CACHE and dict_mode == "modif_config":
                    self.dico = dico_zeph
                    # self.dico.load_values(dict_mode)
                else:
                    self.dico = dico_zeph.parsedico(separator="| ")
                    self.last_mode = dict_mode
                self.check_md5conf(new_method)
            if key != '':
                self.maj_params({key:1})
        except Exception as err:
            traceback.print_exc()
            raise IOError("serveur %s - erreur de sauvegarde de %s :\n%s" % \
            (self.id_s, os.path.basename(fic_zephir), str(err)))
        return ""

    def get_status(self):
        """renvoie le dernier état enregistré du serveur
        """
        try:
            assert self.status in range (6)
        except:
            return -1
        else:
            return self.status

    def set_status(self, status):
        """Met à jour l'état du serveur
        """
        self.status = int(status)
        cu = cx_pool.create()
        try:
            cu.execute("update serveurs set etat=%s where id=%s", (int(self.status), int(self.id_s)))
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
        return self.status

    def update_ip_pub(self, ip_publique):
        """Conserve la dernière adresse de connexion du serveur
        """
        cu = cx_pool.create()
        try:
            cu.execute("update serveurs set ip_publique=%s where id=%s", (ip_publique, int(self.id_s)))
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return False
        return True

    def get_params(self, params=None):
        """renvoie le champ paramètres du serveur
        """
        try:
            if params is None:
                cu = cx_pool.create()
                try:
                    cu.execute("select params from serveurs where id=%s", (int(self.id_s),))
                    data = cu.fetchone()
                    cx_pool.close(cu)
                except:
                    traceback.print_exc()
                    cx_pool.close(cu)
                params = {}
                try:
                    params = eval(data[0])
                    # on vérifie que tous les champs par défaut sont présents
                    assert type(params) == dict and self.default_params.issubset(set(params))
                except:
                    # met à jour le champ param si il n'est pas encore renseigné
                    params = self.update_params(params)
                    # sauvegarde du champs params
                    self.save_params(params)
            else:
                if type(params) == str:
                    params = eval(params)

            # nombre de commandes et transferts uucp en attente
            id_uucp = self.rne+'-'+str(self.id_s)
            params['uucp_transfert']=0
            params['uucp_cmd']=0
            if os.path.isdir('/var/spool/uucp/%s/D.' % id_uucp):
                params['uucp_transfert']=len(os.listdir('/var/spool/uucp/%s/D.' % id_uucp))
            if os.path.isdir('/var/spool/uucp/%s/D.X' % id_uucp):
                params['uucp_cmd']=len(os.listdir('/var/spool/uucp/%s/D.X' % id_uucp))
            # si la configuration n'était pas présénte, on vérifie si c'est toujours le cas
            if params['config_ok'] == 0:
                if os.path.isfile(self.confdir+os.sep+'zephir.eol'):
                    params['config_ok'] = 1
            if params['dico_ok'] == 0:
                if os.path.isfile(self.confdir+os.sep+'dico.eol'):
                    params['dico_ok'] = 1
            if os.path.isfile(self.confdir+os.sep+'new_key.pub'):
                if os.path.isfile(os.path.join(self.confdir,'new_addr')):
                    params['new_key'] = (2,open(os.path.join(self.confdir,'new_addr')).read().strip())
                elif os.path.isfile(os.path.join(self.confdir,'new_addr_ok')):
                    params['new_key'] = (3,open(os.path.join(self.confdir,'new_addr_ok')).read().strip())
                else:
                    params['new_key'] = (1, '')
            else:
                params['new_key'] = (0, '')
            if self.module_version not in config.allowed_migrations:
                params['migration_ok'] = 1
            elif os.path.isfile(self.confdir+os.sep+'migration.eol'):
                params['migration_ok'] = 0
            else:
                params['migration_ok'] = -1
            return params
        except:
            traceback.print_exc()
            return {}

    def edit_serveur(self, dico_modifs):
        # on vérifie que l'identifiant n'est pas modifié
        if 'id' in list(dico_modifs.keys()):
            return 0, config.u("""L'identifiant ne peut pas être modifié""")
        # idem pour le rne et le module
        if 'rne' in list(dico_modifs.keys()):
            return 0, config.u("""Le rne ne peut pas être modifié""")
        if 'module_initial' in list(dico_modifs.keys()):
            return 0, config.u("""Le module initial ne peut pas être modifié""")
        if 'module_actuel' in list(dico_modifs.keys()) and int(dico_modifs['module_actuel']) != self.id_mod:
            # modification amon <-> amonecole permis
            mod_name = self.module[:self.module.rindex('-')]
            allowed_edits = config.allowed_mod_edits.get(self.module_version, {})
            if mod_name in allowed_edits:
                # recherche de la variante par défaut et du libellé pour le modules de destination
                query = """select modules.libelle, variantes.id, modules.version from modules, variantes where modules.id=%s and modules.id=variantes.module and variantes.libelle='standard'"""
                cu = cx_pool.create()
                try:
                    cu.execute(query, (int(dico_modifs['module_actuel']),))
                    data = cu.fetchone()
                    cx_pool.close(cu)
                except:
                    traceback.print_exc()
                    cx_pool.close(cu)
                    return 0, config.u("""Le module n'a pas été retrouvé""")
                if (not data[0][:data[0].rindex('-')] in allowed_edits[mod_name]) or (int(data[2]) != int(self.module_version)):
                    return 0, config.u("""Le module demandé n'est pas compatible avec ce serveur""")
                # cas spécial : si on modifie le module et pas la variante, mettre variante standard par défaut
                if not 'variante' in list(dico_modifs.keys()):
                    dico_modifs['variante'] = data[1]
            else:
                return 0, config.u("""Le module ne peut pas être modifié""")
        # construction de la requête SQL de modification
        if dico_modifs == {}:
            return 1, config.u("""Aucune modification demandée""")
        else:
            params = []
            requete=["update serveurs set "]
            for cle in list(dico_modifs.keys()):
                requete.append(str(cle))
                requete.append("=%s, ")
                if cle in ('module_initial', 'module_actuel', 'variante', 'timeout', 'etat', 'maj', 'md5s', 'no_alert'):
                    params.append(int(dico_modifs[cle]))
                else:
                    params.append(str(dico_modifs[cle]))
            query = "".join(requete)[:-2]
            query += """ where id=%s"""
            params.append(int(self.id_s))
            # modification dans la base et appel à la fonction de mise à jour dans l'arborescence
        try:
            cu = cx_pool.create()
            cu.execute(query, params)
            cx_pool.commit(cu)
        except:
            return 0, config.u("""Erreur lors de la modification dans la base""")

        serveur_dir_ori = self.confdir
        if 'variante' in list(dico_modifs.keys()):
            if int(dico_modifs['variante']) != int(self.id_var):
                # on logue le changement de variante
                try:
                    params = (int(self.id_s), str(time.ctime()), int(self.id_var), int(dico_modifs['variante']))
                    cu = cx_pool.create()
                    cu.execute("""insert into log_serveur (id_serveur,date,type,message,etat) values (%s,%s,'ZEPHIR','Modification de la variante (%s -> %s)',0)""", params)
                    cx_pool.commit(cu)
                except:
                    # on ne bloque pas si le log échoue
                    pass

            if 'module_actuel' in list(dico_modifs.keys()) and int(dico_modifs['module_actuel']) != self.id_mod:
                code, msg = self._cree_arbo_serveur(new_module=int(dico_modifs['module_actuel']),new_variante=int(dico_modifs['variante']))
            else:
                code, msg = self._cree_arbo_serveur(new_variante=int(dico_modifs['variante']))
            if code == 0:
                return 0, config.u(msg)

            # mise à jour des statistiques des variantes
            self.pool.stats['serv_variantes'][str(self.id_var)] = self.pool.stats['serv_variantes'][str(self.id_var)] - 1
            self.pool.stats['serv_variantes'][str(dico_modifs['variante'])] = self.pool.stats['serv_variantes'].get(str(dico_modifs['variante']), 0) + 1
        if 'module_actuel' in list(dico_modifs.keys()) and int(dico_modifs['module_actuel']) != self.id_mod:
            # mise à jour des statistiques des modules
            self.pool.stats['serv_modules'][str(self.id_mod)] = self.pool.stats['serv_modules'][str(self.id_mod)] - 1
            self.pool.stats['serv_modules'][str(dico_modifs['module_actuel'])] = self.pool.stats['serv_modules'].get(str(dico_modifs['module_actuel']), 0) + 1
        self.update_data()
        # tout s'est bien passé (on peut toujours rêver)
        return 1, config.u('ok')

    def _cree_arbo_serveur(self,new_module=None,new_variante=None):
        """crée l'arborescence zephir d'un nouveau serveur"""
        # création des différents répertoires du serveur sur zéphir
        edit_mode = False
        if new_module is not None:
            edit_mode = True
            module = new_module
        else:
            module = self.id_mod
        if new_variante is not None:
            edit_mode = True
            variante = new_variante
        else:
            variante = self.id_var
        serveur_dir = self.confdir

        if not edit_mode:
            try:
                os.makedirs(serveur_dir)
            except:
                return 0, """erreur de création du répertoire du serveur"""
            # création du répertoire de stockage des fichiers spécifiques au module
            try:
                os.makedirs(serveur_dir+os.sep+'fichiers_zephir')
                os.makedirs(serveur_dir+os.sep+'fichiers_perso')
                os.makedirs(serveur_dir+os.sep+'dicos')
                os.makedirs(serveur_dir+os.sep+'patchs')
                os.makedirs(serveur_dir+os.sep+'uucp')
            except:
                shutil.rmtree(serveur_dir)
                return 0, """erreur de création du répertoire des fichiers spécifiques au module"""

        if (not edit_mode) or new_module:
            # création du lien vers le dictionnaire du module
            try:
                if os.path.isdir(os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/dicos'):
                    # module Eole2
                    if not edit_mode:
                        os.makedirs(serveur_dir+os.sep+'dicos/local')
                    assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/dicos '+serveur_dir+'/dicos/module') == 0
                    assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/module.eol '+serveur_dir+'/module.eol') == 0
                else:
                    # module Eole1
                    assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/dictionnaire'+' '+serveur_dir+'/dictionnaire') == 0
            except:
                shutil.rmtree(serveur_dir)
                return 0, """erreur de lien sur le dictionnaire"""

        if (not edit_mode) or new_variante:
            # création des liens vers les patchs et dicos des variantes (ou du module si pas de variante)
            # dico.eol
            try:
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+os.sep+'dico.eol'+' '+serveur_dir+os.sep+'dico.eol') == 0
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+os.sep+'droits_zephir'+' '+serveur_dir+os.sep+'droits_variante') == 0
                # patchs
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+'/patchs'+' '+serveur_dir+'/patchs/variante') == 0
                # dicos
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+'/dicos'+' '+serveur_dir+'/dicos/variante') == 0
                if os.path.isdir(os.path.join(config.PATH_MODULES, str(module), 'variantes', str(variante), 'package')):
                    assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+'/package'+' '+serveur_dir+'/dicos/var_package') == 0
                # fichiers additionnels (définis dans les dictionnaires locaux)
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+'/fichiers_perso'+' '+serveur_dir+'/fichiers_perso/variante') == 0
                # fichiers spécifiques au module (définis dans le dictionnaire zephir)
                assert os.system('ln -nsf '+os.path.abspath(config.PATH_MODULES)+os.sep+str(module)+'/variantes/'+str(variante)+'/fichiers_zephir'+' '+serveur_dir+'/fichiers_zephir/variante') == 0
            except:
                shutil.rmtree(serveur_dir)
                return 0, """erreur de création des liens vers la variante"""
        return 1, 'ok'

    def update_params(self,params_ori):
        # le champ params n'est pas initialisé
        params = {}
        params.update(config.DEFAULT_PARAMS)
        if self.module_version in config.allowed_migrations:
            # serveur nécessitant une migration
            params['migration_ok'] = -1
        try:
            params['agents'] = params_ori['agents']
        except:
            # le serveur n'a pas encore d'info sur les agents
            pass
        # présence de zephir.eol et clé d'enregistrement
        if os.path.isfile(os.path.join(self.confdir,'zephir.eol')):
            params['config_ok']=1
        if os.path.isfile(os.path.join(self.confdir,'cle_publique')):
            params['cle_ok']=1
        if os.path.isfile(os.path.join(self.confdir,'new_key.pub')):
            if os.path.isfile(os.path.join(self.confdir,'new_addr')):
                params['new_key'] = (2,open(os.path.join(self.confdir,'new_addr')).read().strip())
            elif os.path.isfile(os.path.join(self.confdir,'new_addr_ok')):
                params['new_key'] = (3,open(os.path.join(self.confdir,'new_addr_ok')).read().strip())
            else:
                params['new_key'] = (1,'')
        # vérification des logs pour maj et reconfigure
        query = """select type,etat,date,message from last_log_serveur where id_serveur = %s order by date desc,id desc limit 1"""
        cu = cx_pool.create()
        try:
            cu.execute(query, (int(self.id_s),))
            data = cu.fetchone()
            # on stocke la date du dernier log remonté
            if data is not None:
                params['last_log']=str(data[2])
            query = """select type,etat,date,message from last_log_serveur where id_serveur = %s and type != 'SURVEILLANCE' order by date desc,id desc"""
            cu.execute(query, (int(self.id_s),))
            data = cu.fetchall()
            cx_pool.close(cu)
            # on parse les logs
            not_found = ['maj','reconfigure','configure','lock','sauvegarde','reboot','service_restart','upgrade','perso']
            for log in data:
                # on regarde si le serveur n'a pas remonté un bloquage des tâches zephir
                # (pb : procédure pour remonter la résolution du problème)
                if log[0] == 'LOCK':
                    if 'lock' in not_found:
                        not_found.remove('lock')
                        if log[1] == 1:
                            params['lock_ok'] = [2,str(log[2]),log[3]]
                else:
                    for type_log in ['maj','configure','reconfigure','sauvegarde','reboot','service_restart','upgrade','perso']:
                        # état des actions
                        if log[0] == type_log.upper() and int(log[1]) != -2:
                            # on ne stocke que le dernier etat de chaque action
                            if type_log in not_found:
                                not_found.remove(type_log)
                                if log[1] == -1:
                                    params['%s_ok' % type_log] = [2,str(log[2]),log[3]]
                                elif log[1] > 0:
                                    params['%s_ok' % type_log] = [0,str(log[2]),log[3]]
                                elif log[1] == 0:
                                    params['%s_ok' % type_log] = [1,str(log[2]),log[3]]
                        break
                if not_found == []:
                    break
        except:
            traceback.print_exc()
            cx_pool.close(cu)
        return params

    def maj_params(self, param_updates):
        """modifie le champ param d'un serveur
        @param_updates : dictionnaire contenant les modifications
        """
        # on interdit d'autres modifications sur les paramètres de ce serveur
        # pendant la procédure de mise à jour
        with ParamsLock(self.id_s):
            params_ori = self.get_params()
            params_ori.update(param_updates)
            self.save_params(params_ori)

    def save_params(self, params):
        query = """update serveurs set params=%s where id=%s"""
        sql_params = (str(params), int(self.id_s))
        cu = cx_pool.create()
        try:
            cu.execute(query, sql_params)
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)

    def check_md5conf(self, new_method=True):
        # cas particulier : zephir.eol
        orig_eol = os.path.join(self.confdir,'zephir.eol')
        var_eol = os.path.join(self.confdir,'variables.eol')
        # création de la liste des fichiers présents sur zephir
        files=get_md5files(self.module_version)
        if os.path.isfile(orig_eol):
            f_var = open(var_eol, 'w')
            # création d'un fichier ordonné variable:valeur
            conf_orig = load_json(orig_eol)
            eole_vars = list(conf_orig.keys())
            eole_vars.sort()
            for var_name in eole_vars:
                if var_name not in ('mode_zephir', '___version___'):
                    # test supplémentaires au cas où d'autres 'fausses variables'
                    # que ___version___ seraient ajoutées.
                    # cf : https://dev-eole.ac-dijon.fr/issues/10548
                    if type(conf_orig[var_name]) == dict and 'val' in conf_orig[var_name]:
                        val = conf_orig[var_name].get('val')
                        if isinstance(val, dict):
                            val = {k: v for k, v in sorted(val.items(), key=lambda x: x[0])}
                        var_data = "{0}:{1}\n".format(var_name, val)
                    f_var.write(var_data)
            f_var.close()
        fics={}
        for src, dst, pattern in files:
            if os.path.isdir(os.path.join(self.confdir,dst)):
                for fic in os.listdir(os.path.join(self.confdir,dst)):
                    if os.path.isfile(os.path.join(self.confdir,dst,fic)):
                        if pattern == None or fic.endswith(pattern):
                            fics[os.path.join(dst,fic)] = os.path.join(src,fic)
            else:
                fics[dst] = src

        # A partir de la 2.7, les clients remontent plus de md5
        if self.module_version >= 30:
            fichiers_zephir_root = Path().joinpath(self.confdir, 'fichiers_zephir')
            fichiers_zephir_listing = fichiers_zephir_root.joinpath('fichiers_zephir')
            fichiers_variante_listing = fichiers_zephir_root.joinpath('variante', 'fichiers_variante')
            if fichiers_zephir_listing.is_file():
                with open(fichiers_zephir_listing, 'r') as listing_fh:
                    for l in listing_fh.readlines():
                        if l.startswith('#'):
                            continue
                        if l.startswith('%%'):
                            break
                        fichier_serveur = Path(l.strip())
                        fichier_zephir = Path('fichiers_zephir').joinpath(fichier_serveur.name)
                        fichier_zephir_full_path = Path(self.confdir).joinpath(fichier_zephir)
                        if fichier_zephir_full_path.is_file():
                            fics[fichier_zephir.as_posix()] = fichier_serveur.as_posix()
                        elif fichier_zephir_full_path.is_dir():
                            for step in os.walk(fichier_zephir_full_path):
                                for sub_fichier_zephir in step[2]:
                                    temp_root = Path('fichiers_zephir').joinpath(Path(step[0]).relative_to(fichiers_zephir_root))
                                    sub_fichier_zephir = temp_root.joinpath(sub_fichier_zephir)
                                    sub_fichier_serveur = fichier_serveur.joinpath(sub_fichier_zephir.relative_to(fichier_zephir))
                                    fics[sub_fichier_zephir.as_posix()] = sub_fichier_serveur.as_posix()
            if fichiers_variante_listing.is_file():
                with open(fichiers_variante_listing, 'r') as listing_fh:
                    for l in listing_fh.readlines():
                        if l.startswith('#'):
                            continue
                        if l.startswith('%%'):
                            break
                        fichier_serveur = Path(l.strip())
                        fichier_zephir = Path().joinpath('fichiers_zephir', 'variante', fichier_serveur.name)
                        if Path(self.confdir).joinpath(fichier_zephir).is_file():
                            fics[fichier_zephir.as_posix()] = fichier_serveur.as_posix()
        if config.USE_THREADS:
            # lancement depuis la boucle principale
            # (nécessaire pour getProcessOutputAndValue en mode threadé)
            reactor.callFromThread(self._check_md5conf, files, fics)
        else:
            self._check_md5conf(files, fics)

    def _check_md5conf(self, files, fics):
        modifs = []
        md5s = []
        md5file = os.path.join(os.path.abspath(config.PATH_ZEPHIR),'data','config%s.md5' % self.id_s)
        if os.path.isfile(md5file):
            # liste des fichiers remontés par le serveur
            data = open(md5file).read().strip()
            # comparaison
            if data != '':
                for line in data.split('\n'):
                    if line != "":
                        fic = line.split()[1]
                        if fic not in fics:
                            modifs.append("absent sur zephir : %s" % fic)
                        else:
                            md5s.append(fic)
            for fic in fics:
                if fic not in md5s:
                    # erreur : fichier non présent sur la machine distante
                    modifs.append("non envoyé : %s" % fics[fic])
            # test du contenu des fichiers
            cmd_md5 = getProcessOutputAndValue("/usr/bin/md5sum",
                                        args = ["-c",md5file],
                                        path = self.confdir,
                                        env = {'LC_ALL': 'C'})
            cmd_md5.addCallback(self._check_md5_res, md5s, modifs, fics)
        else:
            # pas d'infos md5 pour ce serveur
            self.maj_params({'md5s':[-1,""]})
            self.edit_serveur({'md5s':-1})

    def _check_md5_res(self, result, md5s, modifs, fics):
        # on effectue une vérification md5 des fichiers de configuration si disponible
        out, err, code = result
        out = out.decode()
        for fic in md5s:
            if not "%s: OK" % fic in out.split('\n'):
                if fic == 'variables.eol':
                    # cas particulier des variables creole 1, on indique une modification sur zephir.eol
                    modifs.append("contenu modifié : zephir.eol")
                else:
                    modifs.append("contenu modifié : %s" % fics[fic])
                md5_ok = False
        if modifs != []:
            self.edit_serveur({'md5s':0})
            self.maj_params({'md5s':[0,";".join(modifs).strip()]})
        else:
            self.edit_serveur({'md5s':1})
            self.maj_params({'md5s':[1,""]})

    def check_maj_status(self, maj_checker, show_installed = False, debnames = []):
        """vérifie l'état de mise à jour d'un serveur
        """
        # liste des paquets remontés par le serveur
        if type(debnames) != list:
            debnames = [debnames]
        liste_pkgs = []
        # on regarde le type de mise à jour configurée (minimale/complète) et le serveur de mise à jour
        try:
            dico = self.parsedico()
        except Exception as e:  # Dictionnaire invalide par exemple
            log.msg('Erreur lors de la lecture des dictionnaires: %s' % str(e))
            return []
        # champ type_maj inexistant sur eole 2.4 et >
        type_maj = 'complete'
        serv_maj = dico['serveur_maj']
        # utilisation de serveur spécifique pour les mises à jour ubuntu selon la configuration
        if dico.get('ubuntu_update_mirrors', ''):
            serv_maj_ubuntu = dico['ubuntu_update_mirrors']
        else:
            serv_maj_ubuntu = serv_maj
        # utilisation de serveur spécifique pour les mises à jour envole selon la configuration
        if dico.get('envole_update_mirrors', ''):
            serv_maj_envole = dico['envole_update_mirrors']
        else:
            serv_maj_envole = serv_maj
        version = self.module_version
        pkg_file = os.path.join(os.path.abspath(config.PATH_ZEPHIR),'data','packages%s.list' % self.id_s)
        # on compare les paquets installés avec ceux disponibles sur le serveur de maj
        liste_pkgs, maj_infos = maj_checker.check_packages(self.id_s, self.maj_infos,
                                                           pkg_file, debnames, version,
                                                           serv_maj, serv_maj_ubuntu,
                                                           serv_maj_envole, type_maj,
                                                           show_installed)
        if maj_infos is not None and not debnames and not show_installed and maj_infos != self.maj_infos:
            # la liste des paquets non à jour a changé, on met à jour l'état du serveur
            try:
                nb_pack = len(maj_infos[0])
                if len(maj_infos[0]) > 0:
                    self.edit_serveur({'maj':len(maj_infos[0])})
                    self.maj_params({'query_maj':[len(maj_infos[0]),time.ctime()]})
                else:
                    self.edit_serveur({'maj':0})
                    self.maj_params({'query_maj':[0,time.ctime()]})
                self.maj_infos = maj_infos
            except:
                traceback.print_exc()
                pass
        return liste_pkgs

    def check_min_version(self, maj_checker, debname, min_version, num_distrib=None, default_resp=False):
        """recherche de la version actuelle du paquet sur ce serveur
        num_distrib: tester uniquement pour ce numéro de version (utile pour groupe de serveurs)
        param default_resp: réponse à retourner si la version présente sur le serveur est inconnue
        """
        if num_distrib and self.module_version:
            try:
                num_distrib = int(num_distrib)
                assert num_distrib in config.DISTRIBS
                if self.module_version < num_distrib:
                    return False
                if self.module_version > num_distrib:
                    return True
            except (ValueError, AssertionError) as err:
                log.msg('check_min_version (serveur %s, paquet %s): Numéro de distribution invalide : %s' \
                % (self.id_s, debname, num_distrib))
        if maj_checker:
            pkg_file = os.path.join(os.path.abspath(config.PATH_ZEPHIR),'data','packages%s.list' % self.id_s)
            if os.path.isfile(pkg_file):
                for line in open(pkg_file):
                    pak_name, pak_version = line.split()
                    if pak_name == debname:
                        return maj_checker.check_min_version(pak_version, min_version)
        # information non trouvée, on retourne la réponse par défaut
        return default_resp

    def regen_key(self, new_addr=None):
        """prépare une nouvelle clé ssh pour la connexion uucp à zephir
        """
        new_key = os.path.join(self.confdir, 'new_key')
        addr_file = os.path.join(self.confdir, 'new_addr')
        if os.path.exists(new_key):
            os.unlink(new_key)
        if os.path.exists("%s.pub" % new_key):
            os.unlink("%s.pub" % new_key)
        for f_addr in [addr_file, '%s_ok' % addr_file]:
            if os.path.exists(f_addr):
                os.unlink(f_addr)
        zephir_addr =  new_addr or config.ADRESSE_ZEPHIR
        res = os.system(f"/usr/sbin/eole-ssh-keygen -f {new_key} -c uucp@{zephir_addr}")
        # si nouvelle adresse à envoyer, on crée un fichier new_addr
        if new_addr:
            f_adr = open(addr_file, 'w')
            f_adr.write(new_addr)
            f_adr.close()
        return res

    def get_key(self, old_key, new_key, confirm_ip):
        """envoie une nouvelle clé à un serveur.
        les 2 clés publiques (ancienne et nouvelles) doivent être passées en paramètre.
        """
        try:
            old_key_path = os.path.join(self.confdir, 'cle_publique')
            new_key_path = os.path.join(self.confdir, 'new_key.pub')
            new_key_priv_path = os.path.join(self.confdir, 'new_key')
            new_addr_path = os.path.join(self.confdir, 'new_addr')
            old_data = open(old_key_path).read().strip()
            old_data = base64.encodebytes(old_data.split('" ', 1)[1].encode()).decode()
            if old_data == old_key:
                # la clé correspond, on renvoie les nouvelles clés
                data_keys = []
                new_data = open(new_key_path).read().strip()
                new_data = base64.encodebytes(new_data.encode()).decode()
                if new_data != new_key:
                    return 0, "nouvelle clé publique non reconnue"
                if confirm_ip == True:
                    if os.path.isfile(new_addr_path):
                        os.rename(new_addr_path, '%s_ok' % new_addr_path)
                        return 1, ''
                    else:
                        return 0, "changement d'adresse non détecté sur zephir"
                new_data_priv = base64.encodebytes(open(new_key_priv_path, 'r').read().strip().encode()).decode()
                os.unlink(new_key_priv_path)
                return 1, new_data_priv
            else:
                return 0, "ancienne clé publique non reconnue"
        except Exception as err:
            traceback.print_exc()
            return 0, f"erreur de récupération des données : {err}"

    def get_last_contact(self):
        """renvoie la date de dernier contact avec le serveur
        """
        cu = cx_pool.create()
        try:
            cu.execute("select last_contact from serveurs where id=%s", (int(self.id_s),))
            data = cu.fetchone()
            cx_pool.close(cu)
        except:
            traceback.print_exc()
            cx_pool.close(cu)
            return 0.0
        return float(data[0])

    def migrate_config(self,module_dest,variante_dest,mode="migration",dest_version=None):
        """crée un objet creole dans la version de destination
        et importe les valeurs du serveur
        """
        assert self.module_version in config.allowed_migrations
        assert int(module_dest) >= 1
        assert int(variante_dest) >= 1
        # on stocke la variante choisie dans le répertoire de conf
        f_var = open(os.path.join(self.confdir, 'tmp_variante_migration'), 'w')
        f_var.write(str(variante_dest))
        f_var.close()
        # chemin vers les dictionnaires du module
        creoledirs = [os.path.join(os.path.abspath(config.PATH_MODULES),str(module_dest),'dicos')]
        var_dest_dir = os.path.join(os.path.abspath(config.PATH_MODULES),str(module_dest),'variantes',str(variante_dest))
        if os.path.exists(os.path.join(var_dest_dir, 'package')):
            creoledirs.append(os.path.join(var_dest_dir, 'package'))
        creoledirs.append(os.path.join(var_dest_dir, 'dicos'))
        # création du dictionnaire
        module_version = dest_version or self.module_version
        creole_version = config.CREOLE_VERSIONS[module_version]
        dico = ZephirDict(creoledirs, self.confdir, mode, creole_version, force_instanciate='non', \
                          no_auto_store=True, eole_version=module_version, server_version=self.module_version)
        return dico

    def migrate_data(self, check=False):
        """fonction de récupération des données d'un serveur migré
        check : vérifie seulement si des données sont migrables sur ce serveur
        """
        # fichiers à migrer : fichiers par défaut
        migration_files = {}
        module = self.module
        module_release = config.DISTRIBS[self.module_version][1]
        if module in config.migration_files:
            migration_files.update(config.migration_files[module])
        # définitions supplémentaires
        if module in migration_perso:
            for section in list(migration_perso[module].keys()):
                section_data = migration_files.get(section, [])
                section_data.extend(migration_perso[module][section])
                migration_files[section] = section_data
        dir_serv = os.path.join(self.confdir,'fichiers_zephir')
        dir_bak = os.path.join(self.confdir+'-backup','fichiers_zephir')
        # mode check : indique si des fichiers sont gérés pour ce serveur
        if check == True:
            if len(list(migration_files.keys())) > 0 and os.path.isdir(dir_bak):
                return 1, True
            else:
                return 1, False
        # liste des fichiers à copier
        if not len(list(migration_files.keys())) > 0:
            return 1, config.u("""pas de fichiers à migrer""")
        if not os.path.isdir(dir_bak):
            return 0, config.u("répertoire backup de migration non trouvé")
        not_found = []
        errors = []
        # copie des fichiers avec conversion de l'encoding si besoin
        for src, dst, convert in migration_files['files']:
            fic_src = os.path.join(dir_bak,src)
            fic_dst = os.path.join(dir_serv,dst)
            if os.path.isdir(fic_src):
                # répertoire
                os.path.walk(fic_src, self.tree_copy, (fic_src, fic_dst, convert, errors, migration_files))
            elif os.path.isfile(fic_src):
                self.copy_fic(fic_src, fic_dst, convert, errors)
            else:
                not_found.append(src)
        # application des droits
        droits_zephir = []
        dests=[]
        if 'rights' in migration_files:
            for dest, options, user, group, mode in migration_files['rights']:
                if dest.endswith("*"):
                    # cas "repertoire/*" : on recherche tous les fichiers
                    dests = glob(os.path.join(dir_serv,dest))
                else:
                    if os.path.exists(os.path.join(dir_serv,dest)):
                        dests = [os.path.join(dir_serv,dest)]
                for fic_dest in dests:
                    if 'exclude' in migration_files:
                        if os.path.basename(fic_dest) not in migration_files['exclude']:
                            cmd_rights = 'chmod %s %s %s;chown %s %s.%s %s' % (options, mode, fic_dest, options, user, group, fic_dest)
                            res = os.system(cmd_rights)
                            if res != 0:
                                errors.append('application des droits impossible : %s' % fic_dest)
                            else:
                                # ajout de la ligne pour droits_zephir
                                droits_zephir.append("%s#%s#%s#%s#%s" % (fic_dest[fic_dest.index('fichiers_zephir'):],mode,user,group,'-R' in options))
        # création du fichier droits_zephir
        fic_droits = open(os.path.join(self.confdir, 'droits_zephir'),'w')
        fic_droits.write('\n'.join(droits_zephir))
        fic_droits.close()
        # mise en place du fichier fichiers_zephir (copie des destinations personnalisées)
        # les fichiers de base du module seront remontés depuis le client
        # (récupérés par les scripts de migration ou présents si Upgrade-Auto)
        serveur_file = os.path.join(self.confdir,'fichiers_zephir','fichiers_zephir')
        data = ""
        if module in destinations_perso:
            data += "\n# fichiers ajoutés lors de la migration vers Eole %s" % module_release
            for dest in destinations_perso[module]:
                data += "\n%s" % dest
        # écriture en utilisant les templates définis dans config.py
        final_data = "%s\n%s\n%%%%\n%s" % (config.FILE_SECTION,data,config.RPM_SECTION)

        # écriture du fichier final
        dests = open(serveur_file,'w')
        dests.write(final_data)
        dests.close()

        return 1, config.u((not_found,errors))

    def variante_migration(self):
        path_var = os.path.join(self.confdir, 'variante_migration')
        if os.path.isfile(path_var):
            return open(path_var).read().strip()
        else:
            return ''

    def add_packages(self, paq_names):
        """ajoute un(des) paquet(s) aux paquets à installer d'un serveur
        """
        added = []
        f_perso = os.path.join(self.get_confdir(), 'fichiers_zephir', 'fichiers_zephir')
        fichiers, paqs = self.pool.get_fic_perso(f_perso)
        # ajout des nouveau paquet
        for paq in paq_names:
            # on ajoute le paquet si il n'est pas présent
            if paq not in paqs:
                paqs.append(paq)
                added.append(paq)
        self.pool.save_fic_perso(fichiers, paqs, f_perso)
        return added

    def remove_packages(self, paq_names):
        """ajoute un paquet aux paquets à installer d'un serveur
        """
        removed = []
        f_perso = os.path.join(self.get_confdir(), 'fichiers_zephir', 'fichiers_zephir')
        fichiers, paqs = self.pool.get_fic_perso(f_perso)
        # ajout des nouveau paquet
        for paq in paq_names:
            # on ajoute le paquet si il n'est pas présent
            if paq in paqs:
                paqs.remove(paq)
                removed.append(paq)
            else:
                log.msg("Suppression d'un paquet non défini sur le serveur %s : %s" % (str(self.id_s), paq))
        self.pool.save_fic_perso(fichiers, paqs, f_perso)
        return removed

    def add_info_etab(self, rne, id_client, f_conf):
        # lecture du fichier d'info etablissments
        repl_dir = os.path.join(self.confdir, 'replication')
        data_etab = ConfigParser()
        data_etab.read(os.path.join(repl_dir, 'etabs.ini'))
        # ajout d'informations sur l'établissement et le portail
        serv_client = dict.get(self.pool, id_client)
        if not data_etab.has_section(rne):
            data_etab.add_section(rne)
        data = self.pool.get_info_etab(serv_client.rne)
        if len(data) > 0:
            data_etab.set(rne, "type_etab", data[0][0])
            data_etab.set(rne, "libelle_etab", data[0][1])
        else:
            # infos non trouvées dans la base (mauvais rne renseigné ?)
            # Dans ce cas, on utilise le commentaire inseré par scribe
            # pour le libellé établissement
            log.msg("etabs.ini : les informations sur l'établissement %s ne sont pas disponibles" % rne)
            repl_lines = open(f_conf).strip().split('\n')
            libelle = ''
            for line in repl_lines:
                if line.startswith('#'):
                    libelle = line
                    break
            data_etab.set(rne, "libelle_etab", libelle)
            data_etab.set(rne, "type_etab", "")
        # recherche de l'adresse du portail dans la configuration
        # des serveurs de cet établissement
        portail_etab = self.pool.get_portail_etab(id_client)
        if portail_etab:
            if not portail_etab.startswith('http'):
                portail_etab = "https://%s" % portail_etab
            data_etab.set(rne, "portail_etab", portail_etab)
        # sauvegarde des infos etablissement
        fichier_etab = open(os.path.join(repl_dir, 'etabs.ini'), 'w')
        data_etab.write(fichier_etab)
        fichier_etab.close()

    def remove_info_etab(self, rne):
        # lecture du fichier d'info etablissments
        repl_dir = os.path.join(self.confdir, 'replication')
        data_etab = ConfigParser()
        data_etab.read(os.path.join(repl_dir, 'etabs.ini'))
        if data_etab.has_section(rne):
            # infos à supprimer trouvées
            data_etab.remove_section(rne)
            # sauvegarde
            fichier_etab = open(os.path.join(repl_dir, 'etabs.ini'), 'w')
            data_etab.write(fichier_etab)
            fichier_etab.close()

    def add_replication(self, rne, content, id_client):
        """ajoute une configuration de réplication LDAP
        """
        # chemin de sauvegarde de l'archive contenant la conf de réplication
        repl_dir = os.path.join(self.confdir, 'replication')
        if not os.path.exists(repl_dir):
            os.makedirs(repl_dir)
        script = os.path.join(repl_dir, 'replication-%s.conf' % rne)
        try:
            bin_file = BytesIO()
            data = base64.decodebytes(to_bytes(content))
            fd = open(script,'wb')
            # sauvegarde du fichier
            bin_file.write(data)
            bin_file.seek(0)
            fd.write(bin_file.read())
            fd.close()
        except:
            traceback.print_exc()
            return 0, config.u("erreur de l'écriture du fichier %s" % script)
        # stockage des infos sur l'établissement pour envoi sur le serveur de réplication
        try:
            self.add_info_etab(rne.upper(), id_client, script)
        except:
            log.msg('Erreur lors de la mise à jour du fichier etabs.ini')
            traceback.print_exc()
        return 1, "OK"

    def del_replication(self, filename):
        repl_dir = os.path.join(self.confdir, 'replication')
        try:
            assert os.path.isfile(os.path.join(repl_dir, filename))
            os.unlink(os.path.join(repl_dir, filename))
            # création d'un fichier .modified pour indiquer que la conf est modifiée manuellement
            open(os.path.join(self.confdir, 'replication', '.modified'), 'w').close()
        except:
            return 0, config.u("Erreur lors de la suppression du fichier %s" % filename)
        try:
            # supression des eventuelles informations dans etabs.ini
            rne = filename[filename.index('-')+1:filename.rindex('.')]
            self.remove_info_etab(rne.upper())
        except:
            log.msg('Erreur lors de la supression des informations dans etabs.ini')
            traceback.print_exc()

        return 1, "OK"

    def check_replication(self):
        # vérification du répertoire
        if not os.path.isdir(os.path.join(self.confdir, 'replication')):
            repl_state = 0
        elif os.path.isfile(os.path.join(self.confdir, 'replication', '.modified')):
            repl_state = 2
        else:
            repl_state = 1
        return 1, repl_state

    def get_replication(self):
        # définition du répertoire du serveur
        conf_repl = []
        # lecture de la liste des fichiers enregistrés
        if os.path.isdir(os.path.join(self.confdir, 'replication')):
            search_path = os.path.join(self.confdir, 'replication', 'replication-*.conf')
            try:
                for conf_file in glob(search_path):
                    conf_repl.append(os.path.basename(conf_file))
            except:
                return 0, config.u("Erreur lors de la recherche des configurations de réplication")
        return 1, config.u(conf_repl)

    def get_replication_infos(self):
        """renvoie les données du fichier etabs.ini
        """
        data_etabs = ""
        fic_etab = os.path.join(self.confdir, 'replication', 'etabs.ini')
        if os.path.isfile(fic_etab):
            data_etabs = open(fic_etab).read()
        return data_etabs


    def tree_copy(self, args, dirname, fnames):
        fic_src, fic_dst, convert, errors, migration_files = args
        if not os.path.isdir(fic_dst):
            os.makedirs(fic_dst)
        for fic in fnames:
            if fic not in migration_files['exclude']:
                src = os.path.join(dirname, fic)
                dst = src.replace(fic_src, fic_dst)
                if os.path.exists(src):
                    self.copy_fic(src, dst, convert, errors)

    def copy_fic(self, src, dst, convert, errors):
        """copie du fichier src sur dst
        convert: si True, convertit le fichier d'ISO-8859-1 vers UTF-8
        """
        if convert == True:
            cmd = '/usr/bin/iconv -f ISO-8859-1 -t UTF-8 -o "%s" "%s"' % (dst, src)
        else:
            cmd = '/bin/cp -f "%s" "%s"' % (src, dst)
        res = os.system(cmd)
        if res != 0:
            log.msg("erreur lors de la migration des données, la commande suivante a échoué :", cmd)
            errors.append(src)


class ServeurPool(dict):
    """dictionnaire des serveurs existants gérant les autorisations d'accès
    """

    def __init__(self):

        self._groupes_serveurs = {}
        self._restrictions = {}
        self.stats = {'load_percent':''}
        # self.update_groupes()
        # dictionnaire des variantes et leur module
        self.purge_locks()
        self._mod_var = self.get_mod_var()
        try:
            self.update_auths()
        except:
            # pas encore de table de restriction sur cette version
            pass

    def purge_locks(self):
        # nettoie les eventuels locks non supprimés à l'initialisation
        for lock_file in glob(os.path.join(lock_dir, 'server_params_*')):
            os.unlink(lock_file)

    def update_groupes(self):
        cu = cx_pool.create()
        # récupération des groupes de serveurs
        try:
            cu.execute("select id, libelle, serveurs from groupes_serveurs")
            data = cu.fetchall()
            need_commit = False
            for groupe in data:
                id = int(groupe[0])
                libelle = str(groupe[1])
                serveurs = []
                # suppression des éventuels serveurs inexistants
                update_gr = False
                for serveur in eval(groupe[2]):
                    if serveur in self:
                        serveurs.append(serveur)
                    else:
                        update_gr = True
                if update_gr:
                    # des serveurs ne sont plus valides, on les supprime du groupe dans la base de données
                    cu.execute("update groupes_serveurs set serveurs=%s where id=%s", (str(serveurs), int(id)))
                    need_commit = True
                timestamp = time.time()
                date_creat = timestamp
                if id in self._groupes_serveurs:
                    date_creat = self._groupes_serveurs[id][2][0]
                self._groupes_serveurs[id] = [libelle, serveurs, [date_creat, timestamp]]
            if need_commit:
                cx_pool.commit(cu)
            else:
                cx_pool.close(cu)
        except:
            traceback.print_exc()
            cx_pool.close(cu)

    def get_mod_var(self):
        """récupére les variantes et leur module"""
        cu = cx_pool.create()
        try:
            cu.execute("select id, module from variantes")
            data = cu.fetchall()
            cx_pool.close(cu)
        except:
            data = []
            traceback.print_exc()
            cx_pool.close(cu)
        return dict(data)

    def update_auths(self, user=None):
        params = []
        if user != None:
            user_clause = " where login=%s"
            params.append(user)
        else:
            user_clause = ""

        cu = cx_pool.create()
        try:
            # récupération des types de contraintes spécifiées
            cu.execute("select login, id_res, type_res from restrictions %s" % user_clause, params)
            data = cu.fetchall()
            for restriction in data:
                login, id_res, type_res = restriction
                if login not in self._restrictions:
                    self._restrictions[login] = {}
                if type_res in self._restrictions[login]:
                    self._restrictions[login][type_res].append(id_res)
                else:
                    self._restrictions[login][type_res] = [id_res]
            cx_pool.close(cu)
        except:
            traceback.print_exc()
            cx_pool.close(cu)

    def add_restriction(self, credential, type_res, id_res):
        """ajoute une restriction pour un utilisateur et une ressource donnés
        type_res : nature des objets à restreindre
        id_res ; identifiant de l'objet autorisé
        les différents types reconnus sont : 'rne, group, id_mod, id_var' et les attributs
        de la classe serveur (id_s, module, version, ...)
        """
        # ajout dans la base
        id_res = str(id_res)
        try:
            assert id_res in self._restrictions[credential][type_res]
        except:
            cu = cx_pool.create()
            # validation de l'existence
            libelle, table, field = config.type_res_label[type_res]
            if field == 'rne':
                # données de type str
                query = "select * from %s where %s like %%s" % (table, field)
                params = (id_res,)
            elif field == 'libelle':
                # données de type str
                query = "select * from %s where %s ilike %%s" % (table, field)
                params = (id_res,)
            else:
                # type entier
                query = "select * from %s where %s = %%s" % (table, field)
                params = (int(id_res),)
            try:
                cu.execute(query, params)
                # pas de données correspondante trouvée
                if cu.fetchone() == None:
                    raise ValueError('identifiant de ressource inconnu')
            except Exception as e:
                cx_pool.close(cu)
                return False
            try:
                params = (credential, type_res, id_res)
                cu.execute("""insert into restrictions (login, type_res, id_res) values (%s,%s,%s)""", params)
            except:
                # erreur d'insertion dans la base
                cx_pool.rollback(cu)
                return False
            cx_pool.commit(cu)
            if credential not in self._restrictions:
                self._restrictions[credential] = {}
            if type_res not in self._restrictions[credential]:
                self._restrictions[credential][type_res] = []
            self._restrictions[credential][type_res].append(id_res)
        else:
            # restriction déjà existante
            pass
        return True

    def get_restrictions(self, credential, type_res=None):
        """renvoie la liste des restrictions d'un utilisateur
        """
        if credential not in self._restrictions:
            return []
        if type_res == None:
            # toutes les restrictions : dictionnaire
            return self._restrictions[credential]
        else:
            # si restrictions d'un type précis, on renvoie une liste de ressources
            if type_res in self._restrictions[credential]:
                return self._restrictions[credential][type_res]
            else:
                return []

    def del_restriction(self, credential, type_res, id_res):
        """enleve une restriction d'un utilisateur
        """
        id_res = str(id_res)
        try:
            liste_res = self._restrictions[credential][type_res]
            assert id_res in liste_res
        except:
            # restriction non existante
            return False
        # suppression dans la base
        cu = cx_pool.create()
        try:
            cu.execute("""delete from restrictions where login=%s and type_res=%s and id_res=%s""", (credential, type_res, id_res))
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return False
        # supression en mémoire
        self._restrictions[credential][type_res].remove(id_res)
        if self._restrictions[credential][type_res] == []:
            del(self._restrictions[credential][type_res])
        return True

    def get_file_perms(self, data_dir, filepath=""):
        """renvoie les informations de permissions associées à un fichier
        """
        # lecture des informations sur le fichier si disponibles
        permsfile = data_dir+os.sep+'droits_zephir'
        if os.path.isfile(permsfile):
            f_rights = open(permsfile)
            data = f_rights.read().strip().split('\n')
            f_rights.close()
        else:
            data = []
        mode = user = group = ""
        recursive = False
        if filepath != "":
            result = {filepath:[mode, user, group, recursive]}
        else:
            result = {}
        for line in data:
            if line.startswith(filepath+'#') or filepath == "":
                try:
                    filename, mode, user, group, recursive = line.split('#')
                    if recursive != '':
                        recursive = eval(recursive)
                    result[filename] = [mode, user, group, recursive]
                    if filepath != "":
                        break
                except:
                    # fichier vide ?
                    pass
        return result

    def get_fic_perso(self, fic_perso):
        """renvoie la liste des fichiers personnalisés et paquets d'un serveur
        """
        # on reprend la liste des paquets supplémentaires définis
        try:
            f = open(fic_perso)
            old_content = f.read()
            f.close()
            fichiers = old_content.split('%%\n')[0].strip()
            paqs = old_content.split('%%\n')[1].strip()
        except:
            fichiers = config.FILE_SECTION
            paqs = config.RPM_SECTION
        fichiers = fichiers.split('\n')
        paqs = paqs.split('\n')
        return fichiers, paqs

    def save_fic_perso(self, fichiers, paquets, fic_perso):
        """sauvegarde la liste des fichiers personnalisés et paquets d'un serveur
        """
        # écriture du contenu du fichier
        f=open(fic_perso, 'w')
        f.write('\n'.join(fichiers) + "\n%%\n" + '\n'.join(paquets) + '\n')
        f.close()

    def set_file_perms(self, rights, data_dir):
        """enregistre les informations de permissions associées à un(des) fichier(s)
        @param data_dir: chemin ou trouver le fichier droits_zephir
        """
        # lecture des données existantes
        permsfile = data_dir+os.sep+'droits_zephir'
        data = self.get_file_perms(data_dir)
        data.update(rights)
        # stockage des informations
        lines = []
        for filepath in list(data.keys()):
            # vérification des données
            lines.append("%s#%s#%s#%s#%s" % (filepath, data[filepath][0], data[filepath][1], data[filepath][2],data[filepath][3]))
        # écriture des informations
        f_rights = open(permsfile,'w')
        f_rights.write('\n'.join(lines))
        f_rights.close()
        return True

    def del_file_perms(self, data_dir, filepath="", recurse=False):
        """supprime les informations de permissions associées à un fichier (ou tous)
        """
        # lecture des informations sur le fichier si disponibles
        permsfile = data_dir+os.sep+'droits_zephir'
        if not os.path.exists(permsfile):
            # pas de permissions exitantes ou mauvais chemin
            return False
        if filepath != "":
            f_rights = open(permsfile)
            data = f_rights.read().strip().split('\n')
            f_rights.close()
            if recurse == True:
                search_pattern = filepath
            else:
                search_pattern = filepath + '#'
            for line in data:
                if line.startswith(search_pattern):
                    data.remove(line)
        else:
            data = []
        # écriture des informations
        try:
            f_rights = open(permsfile,'w')
            f_rights.write('\n'.join(data))
            f_rights.close()
        except:
            return False
        return True

    def get_allowed_servers(self, credential):
        """récupération de la liste des serveurs accessibles par un utilisateur
        Vérifie les restrictions définies pour l'utilisateur 'credential'
        en terme de groupe/etablissement/serveur/module/variante
        """
        serveurs = []
        # récupération de la liste de tous les serveurs
        gr_denied = set()
        restrictions = {}
        # groupe, id_serveur, module, rne, variante
        if credential in self._restrictions:
            # liste des serveurs interdits par groupe
            restrictions = self._restrictions[credential]
            for id_gr in restrictions.get('groupe',[]):
                if int(id_gr) in self._groupes_serveurs:
                    gr_denied.update(self._groupes_serveurs[int(id_gr)][1])
        # création de la liste des serveurs autorisés
        for id_serv, infos in list(self.items()):
            if id_serv not in gr_denied:
                # vérification des attributs du serveur
                if infos.rne not in restrictions.get('rne', [infos.rne]):
                    continue
                if str(infos.id_s) not in restrictions.get('id_s', [str(infos.id_s)]):
                    continue
                if str(infos.id_mod) not in restrictions.get('id_mod', [str(infos.id_mod)]):
                    # serveur non accessible, on passe au serveur suivant
                    continue
                if str(infos.id_var) not in restrictions.get('id_var', [str(infos.id_var)]):
                    continue
                # pas de restrictions pour ce serveur, on le valide
                serveurs.append(id_serv)
        return serveurs

    def check_serv_credential(self, credential, key):
        # vérification des restrictions sur les attributs du serveur
        # (id_s, rne, id_mod, variante)
        serv = dict.get(self, key)
        if credential in self._restrictions:
            for type_res, res in list(self._restrictions[credential].items()):
                if res == []:
                    # ce cas ne devrait pas arriver car si on passe toujours par del_restriction
                    continue
                res_ok = True
                # si la ressource n'est pas autorisée
                # cas particuliers : rne et groupes
                if type_res == 'rne':
                    rne_ok = False
                    for rne in res:
                        # cas des rne avec wildcard (%)
                        if rne.endswith('%'):
                            if serv.rne.startswith(rne.split('%')[0]):
                                # ce rne correspond à l'expression
                                rne_ok = True
                                break
                    if not rne_ok:
                        if not serv.rne in res:
                            res_ok = False
                elif type_res == 'groupe':
                    # test d'appartenance à un groupe autorisé
                    serv_in_groupes = False
                    for groupe in res:
                        if serv.id_s in self._groupes_serveurs[int(groupe)][1]:
                            serv_in_groupes = True
                            break
                    if serv_in_groupes == False:
                        res_ok = False
                else:
                    # test attribut correspondant du serveur dans les valeurs autorisées
                    if not str(getattr(serv,type_res)) in [str(i) for i in res]:
                        res_ok = False
                if not res_ok:
                    # on interdit l'accès
                    raise ResourceAuthError("Serveur interdit : %s" % key)

    def check_gr_credential(self, credential, groupe):
        # validation des droits d'accès à un groupe
        if credential in self._restrictions:
            if 'groupe' in self._restrictions[credential]:
                if self._restrictions[credential]['groupe'] != []:
                    if str(groupe) not in self._restrictions[credential]['groupe']:
                        # accès refusé
                        # FIXME vérification suffisante ou regarder les serveurs ou rne du groupe ?
                        raise ResourceAuthError("Groupe interdit : %s" % groupe)

    def check_mod_credential(self, credential, id_mod):
        # validation des droits d'accès à un module
        if credential in self._restrictions:
            if 'id_mod' in self._restrictions[credential]:
                if self._restrictions[credential]['id_mod'] != []:
                    if str(id_mod) not in self._restrictions[credential]['id_mod']:
                        # accès refusé
                        raise ResourceAuthError("Module interdit : %s" % id_mod)

    def check_var_credential(self, credential, id_var):
        # validation des droits d'accès à une variante
        if credential in self._restrictions:
            try:
                var_mod = str(self._mod_var[int(id_var)])
            except:
                # on met à jour si la variante n'est pas trouvée
                self._mod_var = self.get_mod_var()
                var_mod = str(self._mod_var[int(id_var)])
            # on commence par vérifier le module auquel la variante appartient
            if 'id_mod' in self._restrictions[credential]:
                if self._restrictions[credential]['id_mod'] != []:
                    if var_mod not in self._restrictions[credential]['id_mod']:
                        raise ResourceAuthError("module %s interdit" % var_mod)
            # vérification de la variante elle-même
            if 'id_var' in self._restrictions[credential]:
                if self._restrictions[credential]['id_var'] != []:
                    if str(id_var) not in self._restrictions[credential]['id_var']:
                        # accès refusé
                        raise ResourceAuthError("variante %s interdite" % id_var)

    def check_etab_credential(self, credential, rne):
        # validation des droits d'accès à un établissement
        rne_ok = True
        if credential in self._restrictions:
            if 'rne' in self._restrictions[credential]:
                if self._restrictions[credential]['rne'] != []:
                    if str(rne) not in self._restrictions[credential]['rne']:
                        rne_ok = False
                        for etab in self._restrictions[credential]['rne']:
                            if etab.endswith('%'):
                                if str(rne).startswith(etab.split('%')[0]):
                                    rne_ok = True
        if not rne_ok:
            # accès refusé
            raise ResourceAuthError("Etablissement interdit : %s" % rne)

    def get(self, credential, key):
        if key in list(self.keys()):
            self.check_serv_credential(credential, key)
        else:
            raise KeyError("serveur inexistant")
        return dict.get(self,key)

    def get_stats(self):
        self.stats['nb_serv'] = len(self)
        return self.stats

    def get_alertes(self, credential):
        """renvoie la liste des serveurs en alerte
        """
        servs=[]
        for id_serv in list(self.keys()):
            try:
                serv = self.get(credential, int(id_serv))
            except ResourceAuthError:
                # accès à ce serveur non autorisé
                continue
            if serv.get_status() not in [1, -1]:
                servs.append(serv)
        return servs

    def get_migration_status(self, credential):
        """renvoie la liste des serveurs en cours de migration/non migrés
        """
        migration_st = {}
        for id_serv in list(self.keys()):
            try:
                serv = self.get(credential, int(id_serv))
            except ResourceAuthError:
                # accès à ce serveur non autorisé
                continue
            if serv.module_version in config.allowed_migrations:
                params = serv.get_params()
                if params['migration_ok'] == 0:
                    servs = migration_st.get(str(serv.module_version), [[],[]])
                    servs[0].append(serv)
                    migration_st[str(serv.module_version)] = servs
                elif params['migration_ok'] == -1:
                    servs = migration_st.get(str(serv.module_version), [[],[]])
                    servs[1].append(serv)
                    migration_st[str(serv.module_version)] = servs
        return migration_st

    def check_serveurs(self, credential, serveurs, last_check=None):
        """vérifie si des serveurs ont été modifiés
        @param serveurs : dictionnaires {idserveur:timestamp}
        @param last_check : Si != None, on renvoie les serveurs créés après cette date (timestamp)
        @return : liste d'id de serveurs
        """
        modifs = {}
        serv_pool = []
        for id_serv in [str(cle) for cle in list(self.keys())]:
            try:
                serv = self.get(credential, int(id_serv))
            except ResourceAuthError:
                # serveur non existant ou non accessible
                log.msg("server %s not authorized" % id_serv)
                if id_serv in serveurs:
                    modifs[id_serv] = "deleted"
            else:
                serv_pool.append(str(serv.id_s))
                if id_serv in serveurs:
                    if serv.modified > serveurs[id_serv]:
                        # serveur modifié
                        modifs[id_serv] = serv.modified
                elif last_check != None:
                    if serv.created > last_check:
                        # si le serveur a été créé après la dernière mise à jour du client
                        # on l'ajoute comme nouveau serveur
                        modifs[id_serv] = serv.modified

        for id_serv in list(serveurs.keys()):
            if id_serv not in serv_pool:
                modifs[id_serv] = "deleted"

        return modifs

    def check_groupes(self, credential, groupes, last_check=None):
        """vérifie si des groupes ont été modifiés
        @param groupes : dictionnaires {idgroupe:timestamp}
        @param last_check : Si != None, on renvoie les serveurs créés après cette date (timestamp)
        @return : dictionnaire  {id_gr:[libelle, serveurs, [date_creat, date_modif]]}
        """
        modifs = {}

        for id_gr, data in list(self._groupes_serveurs.items()):
            try:
                group = self.get_groupes(credential, id_gr)
            except KeyError as ResourceAuthError:
                # groupe non existant ou non accessible
                log.msg("group %s no longer exists or not authorized" % id_gr)
                modifs[str(id_gr)] = "deleted"
            else:
                # la clé des dictionnaires doit être une chaine pour xmlrpc
                id_gr = str(id_gr)
                if id_gr in groupes:
                    if data[2][1] > groupes[id_gr]:
                        # groupe modifié
                        modifs[id_gr] = data
                elif last_check != None:
                    if data[2][0] > last_check:
                        # nouveau groupe
                        modifs[id_gr] = data
        return modifs

    def add_groupe(self, credential, libelle, serveurs):
        query = """insert into groupes_serveurs (libelle,serveurs) values (%s, %s)"""
        params = (libelle, str(serveurs))
        cu = cx_pool.create()
        try:
            cu.execute(query, params)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return False
        cx_pool.commit(cu)
        self.update_groupes()
        # ajout automatique du groupe pour l'utilisateur l'ayant créé si il a des restrictions de groupes
        if credential in self._restrictions:
            if 'groupe' in self._restrictions[credential]:
                # on recherche l'id du groupe ajouté
                for id_gr in list(self._groupes_serveurs.keys()):
                    if self._groupes_serveurs[id_gr][0] == libelle:
                        self.add_restriction(credential,'groupe',str(id_gr))
        return True

    def get_groupes(self, credential, id_groupe=None, alertes=False):
        if id_groupe != None:
            groupes = [int(id_groupe)]
        else:
            groupes = list(self._groupes_serveurs.keys())
        # vérification des restrictions sur les groupes
        res = []
        for groupe in groupes:
            try:
                self.check_gr_credential(credential, groupe)
            except:
                # groupe non autorisé, on passe au suivant
                continue
            libelle, serveurs, timestamps = self._groupes_serveurs[groupe]
            # sinon on ajoute le groupe à la liste
            res.append([int(groupe), libelle, serveurs])
        return res

    def edit_groupe(self, credential, id_groupe, libelle, serveurs):
        # vérification des droits sur le groupe
        id_groupe = int(id_groupe)
        self.check_gr_credential(credential, id_groupe)
        # mise à jour de la base
        cu = cx_pool.create()
        query = """update groupes_serveurs set libelle=%s, serveurs=%s where id=%s"""
        params = (libelle,str(serveurs),int(id_groupe))
        try:
            cu.execute(query, params)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return False
        cx_pool.commit(cu)
        # mise à jour interne
        date_creat = self._groupes_serveurs[id_groupe][2][0]
        self._groupes_serveurs[id_groupe] = [libelle, serveurs, [date_creat,time.time()]]
        return True

    def del_groupe(self, credential, id_groupe):
        # vérification des droits sur le groupe
        id_groupe = int(id_groupe)
        self.check_gr_credential(credential, id_groupe)
        # on supprime le groupe des groupes surveillés par les utilisateurs
        rech_params = ('[%d]' % id_groupe, '[%%, %d, %%]' % id_groupe, '[%%, %d]' % id_groupe, '[%d, %%]' % id_groupe)
        cursor = cx_pool.create()
        try:
            cursor.execute("""select login,groupes from users where groupes like %s or groupes like %s or groupes like %s or groupes like %s""", rech_params)
            data=cursor.fetchall()
            # pour chaque utilisateur
            for user in data:
                # modification de la liste des groupes
                groupes = eval(user[1])
                if id_groupe in groupes:
                    groupes.remove(id_groupe)
                    sql_update = """update users set groupes=%s where login=%s"""
                    params = (str(groupes), user[0])
                    cursor.execute(sql_update, params)
            # suppression du groupe dans la base
            cursor.execute("""delete from groupes_serveurs where id = %s""", (int(id_groupe),))
            cx_pool.commit(cursor)
        except:
            traceback.print_exc()
            cx_pool.rollback(cursor)
            return False
        # suppression du groupe en mémoire
        del(self._groupes_serveurs[id_groupe])
        return True


    def extend_groupe(self, credential, id_groupe, serveurs):
        # vérification des droits sur le groupe
        id_groupe = int(id_groupe)
        self.check_gr_credential(credential, id_groupe)
        # extension des serveurs du groupe
        # dans le dictionnaire interne
        for serv in serveurs:
            if serv not in self._groupes_serveurs[id_groupe][1]:
                self._groupes_serveurs[id_groupe][1].append(serv)
        self._groupes_serveurs[id_groupe][2] = [time.time(),time.time()]
        cu = cx_pool.create()
        # dans la base de données
        try:
            query = """update groupes_serveurs set serveurs=%s where id=%s"""
            params = (str(self._groupes_serveurs[id_groupe][1]), id_groupe)
            cu.execute(query, params)
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)

    def add_serveur(self, credential, rne, libelle, materiel, processeur, disque_dur, date_install, installateur, tel, remarques, module_initial, module_actuel, variante, timestamp_serveur, timeout):
        # on vérifie si l'accès au rne , module et variante est permis
        if credential in self._restrictions:
            if 'rne' in self._restrictions[credential]:
                self.check_etab_credential(credential, rne)
            if 'id_mod' in self._restrictions[credential]:
                if module_actuel not in self._restrictions[credential]['id_mod']:
                    raise ResourceAuthError("Module interdit : %s" % module_actuel)
            if 'variante' in self._restrictions[credential]:
                cu = cx_pool.create()
                cu.execute('select libelle from variantes where id=%s', (int(variante),))
                libel_var = cu.fetchone()[0]
                cx_pool.close(cu)
                # dans le cas d'une variante standard, on l'autorise toujours
                if libel_var != 'standard' and variante not in self._restrictions[credential]['variante']:
                    raise ResourceAuthError("Variante interdit : %s" % variante)
        # insertion dans la base de données
        cu = cx_pool.create()
        query = """insert into serveurs (rne,libelle,materiel,processeur,disque_dur,date_install,installateur,tel,remarques,module_initial,module_actuel,variante,timestamp,timeout) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
        params = (rne, libelle, materiel, processeur, disque_dur, date_install, installateur, tel, remarques, int(module_initial), int(module_actuel), int(variante), timestamp_serveur, int(timeout))
        cu.execute(query, params)
        # récupération de l'identifiant attribué par la base
        query = """select id from serveurs where rne=%s and libelle=%s and date_install=%s and installateur=%s and remarques=%s and module_actuel=%s and variante=%s and timestamp=%s"""
        params = (rne, libelle, date_install, installateur, remarques, int(module_actuel), int(variante), timestamp_serveur)
        cu.execute(query, params)
        id_serveur = int(cu.fetchone()[0])
        self[id_serveur] = Serveur(self, id_serveur,cu)
        self.stats['no_contact'].append(id_serveur)
        self.stats['serv_modules'][str(module_actuel)] = self.stats['serv_modules'].get(str(module_actuel),0) + 1
        self.stats['serv_variantes'][str(variante)] = self.stats['serv_variantes'].get(str(variante),0) + 1
        cx_pool.commit(cu)
        return self[id_serveur]

    def del_serveur(self, credential, id_serveur):
        """Supression d'un serveur de la base
        """
        # vérification des restrictions
        self.check_serv_credential(credential, id_serveur)
        cu = cx_pool.create()
        # on supprime le serveur dans la base
        try:
            cu.execute("""delete from serveurs where id=%s""", (int(id_serveur),))
            cx_pool.commit(cu)
            # supression interne
            self.stats['serv_modules'][str(self[id_serveur].id_mod)] = self.stats['serv_modules'][str(self[id_serveur].id_mod)] - 1
            self.stats['serv_variantes'][str(self[id_serveur].id_var)] = self.stats['serv_variantes'][str(self[id_serveur].id_var)] - 1
            del(self[id_serveur])
            if int(id_serveur) in self.stats['no_contact']:
                self.stats['no_contact'].remove(int(id_serveur))
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)

    def edit_serveur(self, id_serveur, dico_modifs):
        # modification d'un serveur dans la base
        serv = self[id_serveur]
        return serv.edit_serveur(dico_modifs)

    def update_contact(self, id_serveur):
        """mise à jour de la date de contact d'un serveur"""
        id_serveur = int(id_serveur)
        cu = cx_pool.create()
        try:
            # on met à jour le serveur dans la base
            query = """update serveurs set last_contact=%s where id=%s"""
            params = (str(time.time()), int(id_serveur))
            cu.execute(query, params)
            cx_pool.commit(cu)
            # mise à jour des statistiques
            if int(id_serveur) in self.stats['no_contact']:
                self.stats['no_contact'].remove(int(id_serveur))
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)

    def get_portail_etab(self, id_client):
        """recherche l'adresse du portail etablissement dans la configuration d'un serveur
        variables recherchées (cf var_portail dans config.py commun)
        """
        client = dict.get(self, id_client)
        portail_etab = self._get_portail_etab(client.id_s)
        if portail_etab == '':
            # pas de portail trouvé, on vérifie les autres serveurs de l'établissement
            for serv_id in self.etabs[client.rne]:
                if serv_id != id_client:
                    portail_etab = self._get_portail_etab(serv_id)
                    if portail_etab:
                        # portail trouvé
                        break
        return portail_etab

    def _get_portail_etab(self, id_client):
        client = dict.get(self, id_client)
        for mod in config.mod_portail:
            if client.module.startswith(mod):
                # recherche des variables renseignant l'adresse du portail
                config_serv =  client.parsedico()
                for v_portail in config.var_portail:
                    if config_serv.get(v_portail, '') != '':
                        # les variables sont en priorité décroissante, on s'arrête
                        # à la première occurence trouvée
                        return config_serv.get(v_portail)
        return ''

    def get_info_etab(self, rne):
        query = """select distinct types_etab.libelle, etablissements.libelle from types_etab, etablissements where \
        types_etab.id = etablissements.type and etablissements.rne = %s"""
        sql_params = (rne,)
        cu = cx_pool.create()
        try:
            cu.execute(query, sql_params)
            data = cu.fetchall()
            cx_pool.close(cu)
        except:
            traceback.print_exc()
            data = []
        return data

    def check_replication_infos(self, id_serv):
        """Génère un fichier d'information sur les établissements répliqués
        sur le serveur id_serv (réplication LDAP)
        --> type / libelle / adresse du portail si disponible
        """
        # serveur de réplication
        serv = dict.get(self,id_serv)
        # lecture du fichier d'information sur les établissements répliqués

        # récupération des RNEs des établissements répliqués. Nécessite qu'ils aient été saisis correctement
        search_path = os.path.join(serv.confdir, 'replication', 'replication-*.conf')
        f_confs = glob(search_path)
        for f_conf in f_confs:
            # on recherche le rne dans le fichier de replication
            rne = ""
            for line in open(f_conf):
                if line.strip().startswith('searchbase='):
                    rne = line[line.index('ou=')+3:]
                    rne = rne[:rne.index(',')]
                    break
            if rne == "":
                # RNE non trouvé dans le fichier de réplication, ne devrait pas arriver
                # dans ce cas on utilise celui présent dans le nom du fichier
                rne = f_conf[f_conf.index('-')+1:f_conf.rindex('.')]
            # vérification des informations stockées dans etabs.ini
            self.check_info_etab(serv, rne, f_conf)

    def check_info_etab(self, serv, rne, f_conf):
        """vérifie la disponibilité des informations sur le portail/établissement répliqué
        """
        repl_dir = os.path.join(serv.confdir, 'replication')
        data_etab = ConfigParser()
        data_etab.read(os.path.join(repl_dir, 'etabs.ini'))
        rne = rne.upper()
        if not data_etab.has_section(rne):
            # on appelle la fonction de mise à jour des infos etablissement
            # sur le premier serveur trouvé dans l'établissement
            try:
                id_client = self.etabs[rne][0]
                serv.add_info_etab(rne, id_client, f_conf)
            except:
                log.msg("Aucun serveur trouvé pour l'établissement %s" % rne)
                return

def serveur_pool():
    """crée un pool de tous les serveurs existants
    """
    # création du pool de serveurs en mémoire
    pool = ServeurPool()
    cu = cx_pool.create()
    try:
        cu.execute("select id, rne, libelle, module_actuel, variante, timeout, etat, md5s, maj, no_alert, last_contact from serveurs")
        data = cu.fetchall()
        nbserv = len(data)
        no_contact = []
        # Instanciation des serveurs
        pool.stats['serv_modules'] = {}
        pool.stats['serv_variantes'] = {}
        pool.etabs = {}
        # initialisation des statistiques des modules/variantes
        for var, module in list(pool._mod_var.items()):
            if str(module) not in pool.stats['serv_modules']:
                pool.stats['serv_modules'][str(module)] = 0
            if str(var) not in pool.stats['serv_variantes']:
                pool.stats['serv_variantes'][str(var)] = 0
        for serv in data:
            # liste des serveurs n'ayant jamais contacté zephir
            if serv[10] == None:
                no_contact.append(int(serv[0]))
            # nombre total de serveurs
            pool[serv[0]] = Serveur(pool, serv[0], cu, serv[1:2])
            pool[serv[0]].update_data(cu, serv[2:10])
            # on conserve une liste des serveurs par établissement
            rne_serv = pool[serv[0]].rne
            if rne_serv in pool.etabs:
                pool.etabs[rne_serv].append(serv[0])
            else:
                pool.etabs[rne_serv] = [serv[0]]
            # décompte par module et variante
            pool.stats['serv_modules'][str(serv[3])] = pool.stats['serv_modules'].get(str(serv[3]),0) + 1
            pool.stats['serv_variantes'][str(serv[4])] = pool.stats['serv_variantes'].get(str(serv[4]),0) + 1
        pool.stats['no_contact'] = no_contact
        pool.stats['nb_serv'] = len(pool)
        # chargement des groupes
        pool.update_groupes()
        cx_pool.close(cu)
    except:
        traceback.print_exc()
        cx_pool.close(cu)
    return pool

# utility functions

text_characters = str.maketrans({c: None for c in [chr(e) for e in list(range(32, 255))] + list("\n\r\t\b")})

def istextfile(filename, blocksize = 1024):
    with open(filename, 'rb') as filehandler:
        first_blocks = filehandler.read(blocksize)
        return istext(first_blocks)

def istext(s):
    try:
        s = s.decode()
    except UnicodeDecodeError:
        return False

    if "\0" in s:
        return False

    if not s:  # Empty files are considered text
        return True

    # Get the non-text characters (maps a character to itself then
    # use the 'remove' option to get rid of the text characters.)
    t = s.translate(text_characters)

    # If more than 10% non-text characters, then
    # this is considered a binary file
    if len(t)/len(s) > 0.10:
        return False
    return True

