#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-

"""
Fichier de base d'interface avec les switchs manageables.

Donne la classe switch qui permet d'effectuer les opérations
élémentaires sur les switchs manageable HP 26xx.

Frédéric PAUGET
TODO réécrire ce script (la moitié des fonctions ne marchent plus,
     l'autre ferait bien d'utiliser netsnmp ou équivalent)
"""
from time import sleep
from sys import stderr, path
from commands import getstatusoutput
from os.path import exists
from os import system
import os
import sys
from re import findall
import re
import netsnmp

if '/usr/scripts' not in sys.path:
    path.append('/usr/scripts')
import gestion.secrets_new as secrets
from gestion.config import vlans, bats_virtuels
from gestion.annuaires_pg import chbre_prises, all_switchs

try:
    config_snmp_secrete = secrets.get('config_snmp_secrete')
    reconf_snmp = secrets.get('reconf_snmp')
except:
    # Si a  pas le droit de lire config_snmp_secrete
    # on va tenter de tout faire en snmpv1 et communauté public
    def config_snmp_secrete(snmp,switch) :
        snmp = snmp(switch,version='1',community='public')
        return snmp.get, snmp.set, snmp.walk

#############################################################################################
### Définitions de classes utiles

# Quelques exceptions
class ConnectionTimout(Exception) :
    pass

class ConnectionClosed(Exception) :
    pass

class ConversationError(Exception) :
    pass

# Classes de base pour comminiquer (ssh et snmp)

import pexpect
class ssh :
    """ Ouverture d'une connexion ssh, envoi de commandes et récupération du résultat """


    def __init__(self,host) :
        """ Ouverture d'une connexion ssh vers le switch choisi """
        self.switch = host.split('.')[0] # On garde pas le fqdn
        self.__sshout = ''
        self.ssh = pexpect.spawn("ssh %s" % host)
        self.ssh.sendline("")
        self.ssh.sendline("configure")
        try:
            self.ssh.expect("%s\(config\)# " % self.switch, timeout=20)
        except pexpect.TIMEOUT:
            print "Timeout !"
            import traceback
            traceback.print_exc()
            print "Contenu du buffer :"
            print self.ssh.before

    def __del__(self) :
        """Ferme la connexion : envoi logout et attend """
        self.ssh.sendline("logout")
        self.ssh.send("y")
        self.ssh.send("y")
        self.ssh.close()

    def send_cmd(self,cmd,timeout=15):
        """ Envoi une commande, attend le prompt et retourne la réponse """
        # Envoi de la commande
        self.ssh.sendline(cmd)
        self.__sshout = ''

        try:
            # Attente de la réponse
            while 1:
                index = self.ssh.expect([' \[y/n\]\? ',
                                         '%s\(config\)# ' % self.switch,
                                         'quit: Control-C'],
                                        timeout=timeout)

                self.__sshout = self.__sshout + self.ssh.before
                if index == 0:
                    # On répond oui
                    self.ssh.send("y")
                elif index == 1:
                    # On est revenu au prompt
                    break
                elif index == 2:
                    # On doit continuer, on envoie espace
                    self.ssh.send(" ")

            return self.__sshout
        except pexpect.TIMEOUT:
            print "Timeout !"
            import traceback
            traceback.print_exc()
            print "Contenu du buffer :"
            print self.ssh.before

class snmp :
    """ Classe de communication SNMP """
    def __init__(self,host,version=None,community=None,authentication_protocol=None, authentication_pass=None, username=None, privacy_pass=None) :
        """ host doit être la machine sur laquelle se connecter
        version est la verion du protocole snmp à utiliser : 1, 2c ou 3
        le reste des données doit être ou non fourni suivant la version

        pour v1 et v2c seule la communauté est prise en compte
        pour v3 authentication_protocol, authentication_pass et username sont requis si accès en authNoPriv
        si privacy_pass est fourni accès en authPriv
        """
        self.host = host
        self.version = version
        self.community = community

        if version == '1' or version == '2c' :
            self.options = "-v %s -c '%s' %s " % ( version, community, host )
        elif version =='3' :
            self.fetch_engineid()
            self.options = "-v 3 -e %s -u %s -a %s -A '%s' -l authNoPriv" % ( self._engineid, username, authentication_protocol, authentication_pass )
            if privacy_pass :
                self.options += " -x DES -X '%s' -l authPriv" % privacy_pass
            self.options += " %s " % host
        else :
            raise ValueError('Version incorrecte')

    def fetch_engineid(self):
        short_host = self.host.split('.', 1)[0]
        try:
            self._engineid = secrets.get('engineid').get(short_host, None)
        except:
            self._engineid = None


    def __exec(self,cmd) :
        status, response = getstatusoutput(cmd)
        if status:
            response = response.replace('snmpget: ','')
            print 'Erreur : '+response+' : '+cmd
        return response

    def get(self,oid) :
        """ Retourne le résultat correspondant à l'oid demandé """
        return self.__exec('snmpget -O vq %s %s ' % ( self.options, oid ) )

    def get_string(self,oid) :
        """ Retourne le resultat convertit en String correspondant à l'oid demandé. Fonctionne avec les types de depart String, Integer, Hex-String. Raise ValueError sinon. """
        s= self.__exec('snmpget -O v %s %s ' % ( self.options, oid ) )
        if s=="\"\"":
            return ""
        type=s[0:s.find(":")]
        var=s[s.find(":")+2:]
        if type=="STRING":
            return var[1:-1]
        elif type=="Hex-STRING":
            return var.replace(" ","").replace("\n", "").decode("hex")
        elif type=="INTEGER":
            return unicode(var)
        else:
            raise ValueError('Type inconnu')

    def set(self,oid,typ,val) :
        """ Change la valeur le l'oid donné.
        type est le type de la valeur
        val est la valeur à écrire
        """
        return self.__exec('snmpset -O vq %s %s %s %s' % (self.options, oid, typ, val ) )

    def walk(self,base_oid, bin_comp=False, output='x'):
        """ Retourne le résultat de snmpwalk
        le retour est un dictionnaire { oid : valeur }
        """
        if bin_comp:
            return snmp.bin_walk(self, base_oid)
        if output not in list('abeEfnqQsStTuUvxX'):
            output = 'q'
        lignes = self.__exec('snmpwalk -O%s %s %s' % (output, self.options, base_oid ) ).split('\n')
        result = {}
        for ligne in lignes:
            try:
                oid, valeur = ligne.split('Hex-STRING: ')
            except ValueError:
                try:
                    oid, valeur = ligne.split(' ', 1)
                except ValueError:
                    continue
            result[oid] = valeur
        return result

    def bin_walk(self, base_oid):
        """ Retourne le résultat de snmpwalk
        le retour est un dictionnaire { oid : valeur }
        TODO: prendre en compte l'auth et le snmp vn, n>1
        TODO: malheureusement, la plupart des fonctions faisant appel
              à walk ne marchent pas out-of-the-box, il faut donc
              d'abord les convertir avant de leur faire utiliser cette nouvelle
              fonction
        """
        try:
            prefix, b_oid = base_oid.split('::', 1)
        except ValueError:
            raise NotImplementedError('Merci de préciser un MIB')

        os.environ['MIBS'] = prefix
        # ^^^ Je suis triste de faire ça

        oid_obj = netsnmp.VarList(netsnmp.Varbind(b_oid))
        # oid_obj est remple des réponses du snmpwalk
        netsnmp.snmpwalk(oid_obj,
                         Version = int(self.version),
                         DestHost=self.host,
                         Community=self.community,
                        )
        #UseLongNames=1, #ceci devrait marcher selon la doc
        #en pratique, non (cf /usr/lib/python2.7/dist-packages/netsnmp/client.py)

        # On renvoie un résultat ressemblant à ce que fait le snmpwalk shell
        return { '%s::%s.%s' % (prefix, res.tag, res.iid): res.val
                 for res in oid_obj }

#############################################################################################
### Gestion des switchs proprement dite

class hpswitch :
    """ Classe pour l'administration des switchs HP. """

    # Variables internes
    __debug=0
    __logDest=stderr

    # Variables internes
    switch = None # nom du switch
    prise = ''
    __conn_ssh = None

    def __init__(self,switch) :
        """ Switch doit être le nom du switch """
        if self.__debug : self.__logDest.write("HP DEBUG : __init__(switch=%s)\n" % switch )
        self.switch = switch.lower()

        # Config snmp
        self.get, self.set, self.walk = config_snmp_secrete(snmp,switch)

    def send_cmd(self,cmd,timout=15) :
        """ Envoi une commande par ssh au switch """
        if not self.__conn_ssh :
            self.__conn_ssh = ssh(self.switch)

        return self.__conn_ssh.send_cmd(cmd,timout)

    def show_prise_mac(self,prise='') :
        """ Retourne le(s) adresse(s) MAC présentes sur la prise."""
        if not prise : prise = self.prise
        if self.__debug : self.__logDest.write("HP DEBUG : show_prise_mac(prise=%s)\n" % prise)
        try:
            data = self.walk('STATISTICS-MIB::hpSwitchPortFdbAddress.%d' % int(prise),
                                 bin_comp=True)
            macs = []
            for value in data.itervalues():
                if len(value) != 6:
                    continue # Should not happen
                mac = ':'.join('%02x' % ord(c) for c in value)
                macs.append(mac)
            return macs
        except ValueError:
            # Pas de MAC trouvée
            return []

    def where_is_mac(self, mac) :
        """Retrouve la prise correspondant à une adresse MAC donnée"""
        if self.__debug : self.__logDest.write("HP DEBUG : where_is_mac(mac=%s)\n" % mac)
        mac = mac.lower()
        # On interroge le switch
        try:
            data = self.walk('STATISTICS-MIB::hpSwitchPortFdbAddress',
                                 bin_comp=True)
        except ValueError:
            print >> sys.stderr, "Le switch %s fait du caca..." % self.switch
            return None

        # On cherche dans data la bonne adresse MAC
        for (onesnmp, onemac) in data.iteritems():
            try:
                onemac = onemac.replace(' ', '').lower().replace('"', '')
                if not re.match('([0-9a-f]{2}){6}', onemac):
                    onemac = onemac.encode('hex').lower()
                onemac = "%s:%s:%s:%s:%s:%s" % (onemac[0:2], onemac[2:4], onemac[4:6], onemac[6:8], onemac[8:10], onemac[10:12])
                if onemac.startswith(mac):
                    return int(onesnmp.split(".")[1])
            except:
                pass
            onemac = ""

        # On a rien trouvé
        return None

    def __scp(self,destination,fichier) :
        if self.__debug :
            self.__logDest.write("HP DEBUG : scp(%s,%s,%s)\n" % (fichier, self.switch, destination))
        if exists(fichier):
            system('scp %s %s:%s' % (fichier, self.switch, destination))

    def update(self,file=None) :
        """ Upload le fichier de config fourni
            Fait rebooter le switch """
        self.__scp('cfg/startup-config',file)

    def upgrade(self,file=None) :
        """ Changement de firmware,
        le firmware est dans le fichier fourni en argument
        Ne reboote pas le switch """
        self.__scp('os/primary',file)

    def add_key(self,file=None) :
        """ Ajoute la clef publique ssh aux clefs autorises
        par le switch """
        self.scp('ssh/mgr_keys',file)

    def multicast(self,ip='') :
        """ Donne la liste des ports du swich inscrits au flux multicast donné
        Si aucun flux donné teste tous les flux multicast possibles.

        Retourne un dictionnaire : { adresse du flux : [ ports inscrits ] }
        """
        if self.__debug : self.__logDest.write("HP DEBUG : multicast(ip=%s)\n" % ip)
        if ip :
            data = self.walk('STATISTICS-MIB::hpIgmpStatsPortIndex2.%s' % ip)
            return { ip : data.values() }

        else :
            data = self.walk('STATISTICS-MIB::hpIgmpStatsPortIndex2')
            result = {}
            # On veut tout
            for oid, port in data.items() :
                try : ip = '.'.join(oid.split('.')[2:6])
                except : continue
                result.setdefault(ip,[])
                result[ip].append(port)

        return result

    def nb_prises(self) :
        """ Retourne le nombre de prises du switch """
        if self.__debug : self.__logDest.write("HP DEBUG : nb_prises()\n")
        return int(self.get('SNMPv2-SMI::mib-2.17.1.2.0'))

    def version(self) :
        """ Retourne la version du firmware du switch """
        if self.__debug : self.__logDest.write("HP DEBUG : version()\n")
        return self.get('SNMPv2-MIB::sysDescr.0')

    def enable(self,prise=0) :
        """ Active une prise """
        if not prise : prise = self.prise
        if self.__debug : self.__logDest.write("HP DEBUG : enable(prise=%s)\n" % prise)
        return self.set('IF-MIB::ifAdminStatus.%d' % int(prise), 'i', 1)

    def disable(self,prise=0) :
        """ Désactive une prise """
        if not prise : prise = self.prise
        if self.__debug : self.__logDest.write("HP DEBUG : disable(prise=%s)\n" % prise)
        return self.set('IF-MIB::ifAdminStatus.%d' % int(prise), 'i', 2)

    def __is(self,oid,prise) :
        if not prise : prise = self.prise
        prise = str(prise)
        if prise=='all' :
            nb = 0
            for oid,etat in self.walk(oid).items() :
                if etat == 'up' and int(oid.split('.')[1])<51 :
                    # Le <51 est ici pour éviter de compter les ports fictifs
                    nb += 1
            return nb
        prise = prise.replace('-','')
        return self.get(oid + '.' + prise) == 'up'

    def is_fake(self, prise=0):
        """Retourne True ou False selon que le switch est virtuel."""
        if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
            return True
        return False

    def is_enable(self,prise=0):
        """ Retoune True ou False suivant si la prise est activée ou non
        Si prise=all retourne le nombre de prises activées sur le switch """
        if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
            return False
        if prise != 'all': prise = int(prise)
        return self.__is('IF-MIB::ifAdminStatus',prise)

    def is_up(self,prise=0) :
        """ Retoune True ou False suivant si la prise est up
        Si prise=all retourne le nombre de prises up sur le switch """
        if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
            return False
        if prise != 'all': prise = int(prise)
        return self.__is('IF-MIB::ifOperStatus', prise)

    def nom(self,nom=None,prise=0) :
        """ Retourne ou attribue le nom à la prise fournie """
        if not prise : prise = self.prise
        oid = 'IF-MIB::ifAlias.%d' % int(prise)
        if nom==None :
            return self.get(oid)
        else :
            self.set(oid, 's' , nom)

    def eth_mode(self,mode=None,prise=0) :
        """ Fixe ou retourne le mode d'une prise
        mode est un tuple : (vitesse, duplex) ou simplement "auto"
        vitesse est : 10 100 ou 1000
        duplex est FD, HD ou auto
        """
        if not prise : prise = self.prise
        oid = 'CONFIG-MIB::hpSwitchPortFastEtherMode.%d' % int(prise)

        if mode == None :
            return self.get(oid)

        # Conversion du mode
        if mode == 'auto' :
            code = 5
        else :
            code = { 'HD' : 2 , 'FD' : 4 , 'AUTO' : 8 }[mode[1].upper()]
            if mode[0] == 10 :
                code -= 1
            elif mode[0] == 1000 :
                if code == 8 : code += 1
                elif code == 2 : raise ValueError('Mode invelide %s' % mode)
                else: code += 1

        self.set(oid,'i',code)

    def vlans(self, prise = None):
        """Récupère les vlans activés sur la prise 'prise'"""
        result = []
        if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
            return result
        if not prise:
            prise = self.prise
        prise = int(prise)
        oid_base = 'SNMPv2-SMI::enterprises.11.2.14.11.5.1.7.1.15.3.1.1'
        oid_format = oid_base + '.%(vlan)d.%(prise)d'
        oids = self.walk(oid_base)
        for vlan_name, vlan in vlans.iteritems():
            if oid_format % {'vlan': vlan, 'prise': prise} in oids:
                result.append(vlan_name)

        return result


class sw_chbre(hpswitch) :
    def __init__(self,chbre) :
        # On retrouve la chbre dans l'annuaire
        self.chbre = chbre
        try :
            bat = chbre[0].lower()
            prise = chbre_prises(bat, chbre[1:])
            self.prise_brute = prise
            self.switch = 'bat%s-%i.adm.crans.org' % (bat , int(prise[0]) )
            if prise[-1] == '-' :
                #Prise en 10
                self.prise = int(prise[1:-1])
                self.prise10Mb = True
            else :
                self.prise = int(prise[1:])
                self.prise10Mb = False
        except :
            raise ValueError('Chambre %s inconnue' % chbre)

        # Config snmp
        self.get, self.set, self.walk = config_snmp_secrete(snmp,self.switch)


    def reconfigure(self) :
        """ Reconfigure la prise (nom et vitesse) """
        in10 = self.eth_mode().find('10Mbits') != 1

        if self.prise10Mb and not in10 :
            self.eth_mode(('10','auto'))
        elif not self.prise10Mb and in10 :
            self.eth_mode('auto')

        nom = 'Chambre_%s' % self.chbre.capitalize()
        if nom != self.nom() :
            self.nom(nom)

class sw_prise(sw_chbre):
    def __init__(self, prise):
        bat = prise[0].lower()
        prise = prise[1:]
        self.prise_brute = prise
        self.switch = 'bat%s-%i.adm.crans.org' % (bat , int(prise[0]))
        if prise[-1] == '-' :
            #Prise en 10
            self.prise = int(prise[1:-1])
            self.prise10Mb = True
        else :
            self.prise = int(prise[1:])
            self.prise10Mb = False

        # Config snmp
        self.get, self.set, self.walk = config_snmp_secrete(snmp,self.switch)


if __name__ == '__main__' :
    import sys, getopt, re

    try :
        options, arg = getopt.getopt(sys.argv[1:], 'U:hc:', [ 'help', 'snmp' ])
    except getopt.error, msg :
        print msg
        sys.exit(1)

    cmds = []
    firmware=''
    wait=True
    for opt, val in options :
        if opt == '-h' or opt=='--help' :
            print "Usage : %s [[-c commande1] -c commande2...] [-U firmware] [--snmp] regex "
            print "Envoi les commandes données au switchs matchant la regex"
            print "si aucune commande est founie lit l'entree standart"
            print "L'envoi de firmware ne fait pas rebooter le switch"
            print "L'option --snmp ajoute les commandes de reconfiguration snmp"
            sys.exit(0)

        elif opt=='-c' :
            cmds.append(val)

        elif opt=='-U' :
            firmware=val

        elif opt=='--snmp' :
            cmds.append(reconf_snmp)
            cmds.append("write memory")

    # Quels switchs ?
    switchs=[]
    if arg :
        rexp=re.compile(arg[0])
        for sw in all_switchs() :
            if rexp.match(sw) :
                switchs.append(sw)

    if not switchs :
        print "Aucun switch trouvé"
        print "Note : il faut une _regex_ (!= wilcards au sens du shell)"
        sys.exit(3)

    if not cmds and not firmware :
        cmds=map(str.strip,sys.stdin.readlines())

    # Ce que l'on va faire
    print "Commandes :\n\t", '\n\t'.join(cmds)
    print "\nSwitchs : ", ' '.join(switchs)
    print

    for sw in switchs :
        print sw
        # Au cas ou le switch ne répondrai pas
        s = hpswitch(sw)
        if firmware :
            s.upgrade(firmware)
        for cmd in cmds :
            print s.send_cmd(cmd)

