#! /usr/bin/env python
# -*- 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
#
# uucp_utils.py
#
# fonctions utilitaires pour la gestion d'uucp
#
###########################################################################

import os,time,shutil,tempfile

UUCP_DIR = "/var/spool/uucp"
CMD_UUX = "/usr/bin/uux2"
CMD_UUCP = "/usr/bin/uucp2"
LOG_FILE = "/tmp/rapport.zephir"
LOCK = "/var/spool/uucp/lock"
IGNORE_LIST = ['.ssh','.Status','.Temp','.Received','.bash_history','dead.letter']

# commandes connues: dictionnaires {"commande uucp" : (libellé de la commande, rejouable, transfert_rejouable)}
# dans le cas où une commande n'est pas rejouable, il faut écraser son  appel précédent
# lorsqu'elle est appelée plusieurs fois (concerne surtout les commandes liées à un transfert de fichier)
# si le transfert est rejouable, on ne supprime pas le transfert associé à l'ancienne commande
# lors d'un écrasement d'appel (par exemple si le fichier peut varier à chaque appel)
COMMANDS = {"zephir_client save_files": ("Sauvegarde complète", True, False),
            "zephir_client save_files 0": ("Sauvegarde (complète)", True, False),
            "zephir_client save_files 1": ("Sauvegarde (configuration)", True, False),
            "zephir_client save_files 2": ("Sauvegarde (configuration/patchs/dicos)", True, False),
            "zephir_client save_files 3": ("Sauvegarde (fichiers divers)", True, False),
            "zephir_client maj_auto": ("Mise à jour", True, False),
            "zephir_client maj_auto E": ("Mise à jour complète", True, False),
            "zephir_client maj_client": ("Mise à jour de zephir-client", True, False),
            "zephir_client download_upgrade": ("Préchargement des paquets (Upgrade-Auto)", True, False),
            "zephir_client configure": ("Envoi de configuration", False, False),
            "zephir_client reboot": ("Redémarrage du serveur", True, False),
            "zephir_client service_restart": ("Redémarrage du service", True, False),
            "zephir_client reconfigure": ("Reconfiguration", True, False),
            "zephir_client update_key regen_certs": ("Renouvellement des clés d'enregistrement et certificats ssl", False, False),
            "zephir_client update_key": ("Renouvellement des clés d'enregistrement", False, False),
            "zephir_client update_replication": ("Regénération de la configuration de réplication LDAP", False, False),
            "zephir_client change_ip": ("Préparation au changement d'adresse de zephir", False, False),
            "zephir_client purge_ip": ("Annulation du changement d'adresse de zephir", True, False),
            "zephir_client import_aaf": ("Prise en compte des fichiers d'import AAF", False, True),
           }

class UUCPError(Exception): pass

NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z','a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

# fonction de conversion
def convert_num(chaine):
    result=0
    digits = list(range(len(chaine)))
    # on parcourt la chaine en partant de la fin
    digits.reverse()
    for i in range(len(chaine)):
        # valeur du digit en cours
        digit = NUMBERS.index(chaine[digits[i]])
        # valeur * longueur_base^position
        result = result + digit * len(NUMBERS).__pow__(i)
    return result

def get_lock():
    # on regarde si on a le droit d'écrire
    timeout = 0
    while os.path.isfile(LOCK):
        # si non on attend la libération des ressources
        timeout += 1
        # si plus de 5 secondes d'attente, il y a surement un problème
        if timeout > 10:
            # print "Problème d'accès concurrent, fichier %s présent" % LOCK
            raise UUCPError("Problème d'accès concurrent, fichier %s présent" % LOCK)
        time.sleep(0.5)
    # on bloque les autres appels à cette fonction en cas de demandes simultanées
    lock=open(LOCK,"w")
    lock.close()

def free_lock():
    # supprime le fichier de lock si il existe
    if os.path.isfile(LOCK):
        os.unlink(LOCK)

class UUCP:
    """wrapper d'uucp pour permettre la gestion des files dans zephir"""

    def __init__(self,peers=None):
        """initialisation de l'objet"""
        self.peers_ori=peers
        self.pool={}
        self._scan_pool()

    def _scan_pool(self, peer=None):
        """crée la liste d'attente en mémoire"""
        # on crée la liste des commandes et transferts actuels
        # si pas de noms de machines spécifiées, on recherche dans tous les systèmes
        if peer is not None:
            peers = [peer]
        else:
            if self.peers_ori is None:
                peers = os.listdir(UUCP_DIR)
                for peer in IGNORE_LIST:
                    if peer in peers:
                        peers.remove(peer)
            else:
                peers = self.peers_ori
        # parcours de /var/spool/uucp
        for peer in peers:
            # répertoire uucp de ce correspondant
            peer_dir = os.path.abspath(os.path.join(UUCP_DIR, peer))
            # liste des actions en attente
            lst={}
            if os.path.isdir(os.path.join(peer_dir, "C.")):
                for fic in os.listdir(os.path.join(peer_dir, "C.")):
                    # on récupère la date de création de la commande (timestamp)
                    date_creat = os.stat(os.path.join(peer_dir, "C.", fic)).st_ctime
                    f=open(os.path.join(peer_dir, "C.", fic))
                    l=f.read().strip().split('\n')[0]
                    f.close()
                    # séparation des paramètres
                    # ex de lignes:
                    # S /var/lib/zephir/conf/0210056X/4/config-zephir.tar ~ root -Cd D.0008 0644
                    # S D.X0007 X.zephirN0007 root -C D.X0007 0666
                    args=l.split()[1]
                    script=l.split()[5]
                    seq_number=convert_num(script[-4:])
                    # si on a une commande
                    type_cmd = "transfert"
                    orig_cmd = ""
                    if args.startswith("D.X"):
                        type_cmd = "execute"
                        # on regarde laquelle dans le fichier correspondant
                        f=open(os.path.join(peer_dir, "D.X", script))
                        l=f.read().strip().split('\n')
                        f.close()
                        for line in l:
                            line = line.strip()
                            if line[:2] == "C ":
                                args = line[2:]
                                orig_cmd = line[2:]
                                # on regarde si c'est une commande connue
                                cmds = list(COMMANDS.keys())
                                cmds.sort(reverse=True)
                                for cmd in cmds:
                                    if line[2:].startswith(cmd):
                                        args = line[2:].replace(cmd, COMMANDS[cmd][0])
                                        break

                    lst[seq_number]=(type_cmd,args,script,fic,date_creat,orig_cmd)

            self.pool[peer]=lst


    def add_cmd(self,peer,commande):
        """met en place une commande distante"""
        peer_dir = os.path.abspath(os.path.join(UUCP_DIR, peer))
        get_lock()
        # on recherche le grade à affecter à la commande (pour assurer l'ordre d'execution)
        self._scan_pool(peer)
        used_grades = []
        if peer in self.pool:
            cmd_files = [uucmd[3] for uucmd in list(self.pool[peer].values())]
            for uucmd in cmd_files:
                cmd_grade = convert_num(uucmd[2])
                if cmd_grade not in used_grades:
                    used_grades.append(cmd_grade)
        if used_grades != []:
            new_grade = max(used_grades) + 1
        else:
            # grade de départ pour les commandes : 'O' (les transferts de fichiers ont 'N' par défaut)
            new_grade = 24
        # si on est déjà au grade maximum, on le conserve (ne devrait pas arriver sauf si file d'attente bloquée longtemps ?)
        if new_grade >= len(NUMBERS):
            new_grade = NUMBERS[-1]
        else:
            new_grade = NUMBERS[new_grade]
        old_calls = []
        if commande in COMMANDS and COMMANDS[commande][1] == False:
            # on recherche d'éventuels appels précédents à supprimer si la commande n'est pas rejouable
            for old_cmd in self.list_cmd(peer)[peer]:
                # pour les commandes, il faut comparer à l'appel réel et non au libellé
                if self.pool[peer][old_cmd[0]][5] == commande:
                    old_calls.append((old_cmd[0], COMMANDS[commande][2]))
        # construction de l'appel à uux
        cmd = "%s -g %s -r '%s!%s >%s!%s'" % (CMD_UUX, new_grade, peer, commande, peer, LOG_FILE)
        try:
            # appel systeme
            res=os.system(cmd)
            # on ajoute la commande à la liste des commandes de ce peer
            if res == 0:
                # recherche du fichier correspondant (dernier fichier créé dans "C.")
                tempbuf=tempfile.mktemp()
                os.system("/bin/ls -t %s > %s" % (os.path.join(peer_dir, "C./"), tempbuf))
                output=open(tempbuf)
                res2=output.read()
                output.close()
                os.unlink(tempbuf)
                # on récupère le nom du fichier le plus récent
                filename = res2.split()[0]
                # nom du script correspondant
                f=open(os.path.join(peer_dir, "C.", filename))
                l=f.read().strip().split('\n')[0]
                f.close()
                script = l.split()[5]
                # on déduit le n° dans la file à partir du nom de script
                seq_num=convert_num(script[-4:])
                if peer not in self.pool:
                    self.pool[peer]={}
                date_creat = os.stat(os.path.join(peer_dir, "C.", filename)).st_ctime
                self.pool[peer][seq_num]=("execute", commande, script, filename, date_creat, commande)
                # on libère la ressource
                free_lock()
                # on supprime les éventuels fichiers précédents
                if old_calls != []:
                    for call_id, keep_transfert in old_calls:
                        self.remove_cmd(peer, call_id, keep_transfert)
                return seq_num
            else:
                free_lock()
                raise Exception("uux2 a retourné une erreur")
        except Exception as e:
            free_lock()
            # print """erreur lors de la préparation de l'exécution de %s : %s""" % (commande,e)
            raise UUCPError("""erreur lors de la préparation de l'exécution de %s : %s""" % (commande, str(e)))

    def add_file(self,peer,fichier,destination="~"):
        """prépare l'envoi d'un fichier"""
        peer_dir = os.path.abspath(os.path.join(UUCP_DIR, peer))
        # Si un transfert est déjà stocké pour ce fichier, on prépare son annulation
        old_trans = []
        for transfert in self.list_files(peer)[peer]:
            if transfert[1] == fichier:
                old_trans.append(transfert[0])
        # construction de l'appel à uucp
        cmd = "%s -r %s %s\\!%s" % (CMD_UUCP,fichier,peer,destination)
        get_lock()
        try:
            # appel systeme à uucp
            res=os.system(cmd)
            # on ajoute le transfert à la liste des transferts de ce peer
            if res == 0:
                # recherche du fichier correspondant
                tempbuf=tempfile.mktemp()
                os.system("/bin/ls -t %s > %s" % (os.path.join(peer_dir, "C./"), tempbuf))
                output=open(tempbuf)
                res2=output.read()
                output.close()
                os.unlink(tempbuf)
                # on récupère le nom du fichier le plus récent
                filename = res2.split()[0]
                # nom du script correspondant
                f=open(os.path.join(peer_dir, "C.", filename))
                l=f.read().strip().split('\n')[0]
                f.close()
                script = l.split()[5]
                # on déduit le n° dans la file à partir du nom de script
                seq_num=convert_num(script[-4:])
                if peer not in self.pool:
                    self.pool[peer]={}
                date_creat = os.stat(os.path.join(peer_dir, "C.", filename)).st_ctime
                self.pool[peer][seq_num] = ("transfert",fichier,script,filename,time.time(),date_creat,fichier)
                # on libère la ressource
                free_lock()
                # on supprime les éventuels fichiers précédents
                if old_trans != []:
                    for trans_id in old_trans:
                        self.remove_cmd(peer, trans_id)
                return 0
            else:
                free_lock()
                raise UUCPError("""echec à l'exécution de uucp""")
        except Exception as e:
            free_lock()
            raise UUCPError("""erreur lors de la préparation du transfert de %s : %s""" % (fichier,e))

    def _create_liste(self,type_cmd,peers):
        """fonction interne qui liste les actions d'un type particulier"""
        cmds = {}
        # pour chaque peer
        for peer in peers:
            cmds[peer] = []
            if peer not in self.pool:
                continue
            # on parcourt la liste des actions en attente
            numeros = list(self.pool[peer].keys())
            numeros.sort()
            for n in numeros:
                action = self.pool[peer][n]
                # si c'est une commande distante
                if action[0] == type_cmd:
                    # on l'ajoute à la liste
                    cmds[peer].append((n, action[1]))
        return cmds

    def check_timeout(self,max_time,peer=None):
        """vérifie si il existe des commandes plus anciennes
        que max_time pour un serveur donné (ou tous)
        @param max_time: age maximum en seconde accepté pour une commande
        retourne un dictionnaire {serveur:liste des ids de commande trop anciens}"""
        dic_res = {}
        if peer is not None:
            peers=[peer]
        else:
            self._scan_pool()
            peers=list(self.pool.keys())
        for serveur in peers:
            timeouts = []
            if serveur in self.pool:
                for seq_num, data in list(self.pool[serveur].items()):
                    test_time = time.localtime(float(data[-2] + max_time))
                    # si timeout (date de création + délai > date actuelle)
                    if test_time < time.localtime():
                        # on renvoie le n° de commande et sa date de création
                        timeouts.append((seq_num, time.ctime(data[-2])))
            if timeouts != []:
                dic_res[serveur] = timeouts
        return dic_res

    def list_cmd(self,peer=None):
        """renvoie la liste des commandes en attente"""
        if peer is not None:
            self._scan_pool(peer)
            peers=[peer]
        else:
            self._scan_pool()
            peers=list(self.pool.keys())
        return self._create_liste("execute",peers)

    def list_files(self,peer=None):
        """renvoie la liste des transferts en attente"""
        if peer is not None:
            self._scan_pool(peer)
            peers=[peer]
        else:
            self._scan_pool()
            peers=list(self.pool.keys())
        return self._create_liste("transfert",peers)

    def remove_cmd(self, peer, num_cmd, keep_transfert=False):
        """supprime une commande ou un transfert"""
        num_cmd = int(num_cmd)
        type_cmd,fichier,script,filename,date_creat,fichier_orig = self.pool[peer][num_cmd]
        peer_dir = os.path.abspath(os.path.join(UUCP_DIR, peer))
        get_lock()
        # supression du fichier correspondant dans C.
        try:
            if os.path.isfile(os.path.join(peer_dir, 'C.', filename)):
                os.unlink(os.path.join(peer_dir, 'C.', filename))
            if type_cmd == "transfert":
                # supression de la copie du fichier dans D.
                if os.path.isfile(os.path.join(peer_dir, 'D.', script)):
                    os.unlink(os.path.join(peer_dir, 'D.', script))
            else:
                # supression du script correspondant dans D.X
                if os.path.isfile(os.path.join(peer_dir, 'D.X', script)):
                    os.unlink(os.path.join(peer_dir, 'D.X', script))
            # on libère la ressource
            free_lock()
        except:
            # print "erreur lors de la suppression des fichiers"
            free_lock()
            raise UUCPError("erreur lors de la suppression des fichiers")

        del(self.pool[peer][num_cmd])
        # dans le cas d'une commande avec fichier, on supprime le transfert associé si on le trouve
        if fichier_orig in COMMANDS and COMMANDS[fichier_orig][1] == False and not keep_transfert:
            previous_cmd = self.pool[peer].get(num_cmd-1,[''])
            if previous_cmd[0] == "transfert":
                self.remove_cmd(peer, num_cmd-1)
        return 0

    def flush(self,peers=None):
        """supprime toute la file d'attente"""
        if peers is None:
            for i in os.listdir(UUCP_DIR):
                if i not in IGNORE_LIST:
                    try:
                        # on supprime le répertoire d'échange
                        if os.path.isdir(os.path.join(UUCP_DIR, i)):
                            shutil.rmtree(os.path.join(UUCP_DIR, i))
                        self.pool[i]={}
                    except:
                        raise UUCPError("""erreur de suppression de la file d'attente de %s""" % i)
        else:
            try:
                for peer in peers:
                    if os.path.isdir(os.path.join(UUCP_DIR, peer)):
                        shutil.rmtree(os.path.join(UUCP_DIR, peer))
                    self.pool[peer]={}
            except:
                raise UUCPError("""erreur de supression de la file d'attente""")
        return 0

uucp_pool = UUCP()

if __name__ == '__main__':
    peers = ["0210056X-1","0210056X-2","0210056X-3","0210056X-4","0210056X-5"]
    uucp=UUCP(peers)
    peer = ""
    while not peer in peers:
        peer = input("\nvoir la file d'attente de : ")
    print("\ncommandes :\n")
    for cmd in uucp.list_cmd(peer)[peer]:
        print("  "+str(cmd))

    print("\ntransferts :\n")
    for cmd in uucp.list_files(peer)[peer]:
        print("  "+str(cmd))
    print('\n')
