Forum Français

 View Only

ArubaOS-CX et l'automatisation - Part 4.3 : Utilisation avancée du Network Analytics Engine

This thread has been viewed 1 times
  • 1.  ArubaOS-CX et l'automatisation - Part 4.3 : Utilisation avancée du Network Analytics Engine

    Posted Jan 21, 2019 11:31 AM

    Nous avons vu dans le post précédent comment gérer les scripts et agents dans le Network Analytics Engine (ArubaOS-CX et l'automatisation - Part 4.2 : Gestion des scripts et agents du NAE), mais également comment faire une action de configuration lorsqu'un evenement est detecté par l'outil.

     

    Dans ce nouveau post sur ArubaOS-CX et le Network Analytics Engine, nous allons aller un peu plus loin et voir certaines capacités de cet outil, permettant de réaliser des actions un peu plus complexes et élaborées.

     

    Commençons tout de suite :

     

    1. Réaliser des requêtes complémentaires sur l'équipement

     

    Nous avons vu que le Network Analytics Engine fournit des fonctions natives afin de réaliser des actions, ce que l'on appelle les "actions callbacks".

    Ces actions sont prédéfinies, et permettent par exemple de générer un Syslog, de modifier le niveau d'alerte de l'agent ou de passer un ensemble de lignes de commande sur l'équipement.

    Mais comment faire pour par exemple récupérer de l'information complémentaire à ce qui est remonté par le Monitor ?

    Pour rappel, NAE instancie des scripts Python afin de créer les agents. A partir de là, il est possible d'utiliser les fonctions natives de Python ou les modules intégrés à ArubaOS-CX pour implémenter des capacités complémentaires aux agents.

     

    Reprenons le script de surveillance d'une interface, que nous avons utilisé dans les posts précédents. Pour rappel, ce script, instancié en agent, permet de surveiller le statut d'un port, qui est passé en paramètre, et de le réactiver automatiquement si ce dernier est détecté en status "down".

     

    Nous allons améliorer le script de manière à récupérer automatiquement les informations LLDP de l'équipement connecté en face, et afficher les informations importantes, comme le hostname, l'adresse IP et le port de connexion, dans le détail de l'évènement.

     

    Ajoutons les lignes suivantes à notre script :

     

    def action_interface_up(self, event):
            self.logger.debug("================ Up ================")
            self.getLLDP()
            self.hostname_lldp = self.lldp_info[0]["neighbor_info"]['chassis_name']
            self.ip_lldp = self.lldp_info[0]["neighbor_info"]['mgmt_ip_list']
            self.connected_port_lldp = self.lldp_info[0]["port_id"]
            ..............<existing script>................
                ActionCLI("show lldp neighbor {}",
                          [self.params['interface_id']])
                ActionSyslog("LLDP - Neighbor Hostname : {}".format(self.hostname_lldp))
                ActionSyslog("LLDP - Neighbor IP : {}".format(self.ip_lldp))
                ActionSyslog("LLDP - Neighbor Connected Port : {}".format(self.connected_port_lldp))
                self.remove_alert_level()
            self.logger.debug("================ /Up ================")
    
    def getLLDP(self):
            port_name = str(self.params['interface_id']).replace("/", "%2F")
            url_lldp = "{}/rest/v1/system/interfaces/{}/lldp_neighbors?depth=2".format(HTTP_ADDRESS, port_name)
            get_lldp = requests.get(url_lldp)
            self.lldp_info = get_lldp.json()

    Que fait-on ici ?

    1. Nous créons une fonction : "get_lldp", qui va récupérer les informations LLDP du port au travers d'une requête complémentaire. Dans cette fonction, nous lançons une nouvelle requête, en prenant en compte 2 informations importantes : Nous utilisons "HTTP_ADDRESS", qui est une variable native de NAE, et qui pointe vers l'équipement local. De même, puisque NAE est intégré à l'équipement, nous n'avons pas besoin d'ouvrir une session. 

    3. Nous transformons le nom de l'interface donné en paramètre, de manière à être utilisée dans l'URI. Ex : "1/1/2" devient "1%2F1%2F2"

    4. Nous récupérons le JSON, et le traitons dans l'action principale de manière à créer des Syslog avec les informations que l'on souhaite.

     

    Si on regarde de plus près ce que cela donne :

    Screenshot 2019-01-21 at 09.54.39.png

    Notre agent a bien détecté le passage de "up" a "down" de l'interface, et a automatiquement réaliser les actions que l'on a configurées :

    Screenshot 2019-01-21 at 09.54.21.png

    On retrouve donc, en plus les actions réalisées à la base, comme le changement d'alertes ou le passage de commandes en CLI, ainsi que 3 nouveaux Syslog, donnant les informations LLDP de l'équipement connecté en face.

     

    Bien sûr, vous pouvez également utiliser cette méthode pour réaliser une requête vers le Peer VSX, en utilisant l'extension "vsx-peer" dans l'URI. Pour cela, vous pouvez utiliser la définition de fonction suivante :

    def getLLDP_VSXPeer(self):
            port_name = str(self.params['interface_id']).replace("/", "%2F")
            url_lldp_peer = "{}/vsx-peer/rest/v1/system/interfaces/{}/lldp_neighbors?depth=2".format(HTTP_ADDRESS, port_name)
            get_lldp_peer = requests.get(url_lldp_peer)
            self.lldp_info_peer = get_lldp_peer.json()

    2. Requête vers un équipement tiers

     

    Cette capacité à pouvoir interroger l'équipement local en plus de ce qui est réalisé avec le Monitor permet donc de pouvoir avoir plus de profondeur dans les actions disponibles.

    Mais ce qui est également intéressant, c'est la capacité à pouvoir récuperer des informations ou réaliser des actions sur des solutions tierces, notamment via les API.

     

    Continuons ainsi avec notre script exemple, et allons un peu plus loin dans la vérification.

    En plus d'afficher les informations LLDP de l'équipement connecté, nous souhaitons vérifier que cet équipement est bien celui attendu, mais également qu'il répond à la politique de securité en étant intégré dans ClearPass, comme Network Device.

     

    Pour cela, nous allons utiliser un outil de DCIM/IPAM nommé NetBox, qui permet de pouvoir gérer les connexions physique entre les équipements au travers d'inventaires, et qui supporte les API.

     

    Tout d'abord, il est necessaire d'importer le module "requests", qui nous permet de pouvoir effectuer des requetes API vers nos differentes solutions. Pour cela, intégrer en haut du code :

    # specific language governing permissions and limitations
    # under the License.
    
    import requests
    
    Manifest = {

    Modifions alors notre script de la manière suivante :

     

    def action_interface_up(self, event):
            .........<existing script>...........
            self.getInfoNetbox()
            self.getInfosClearPass()
            self.checks()
            ............<existing script>..............
                ActionCLI("show lldp neighbor {}",
                          [self.params['interface_id']])
                ActionSyslog('Remote device IP Verification : {} (Source : NetBox)'.format(self.check_remote_ip))
                ActionSyslog('Remote device Port Verification : {} (Source : NetBox)'.format(self.check_remote_port))
                ActionSyslog('Remote device in ClearPass : {} (Source : CPPM)'.format(self.check_clearpass))
                self.remove_alert_level()
            self.logger.debug("================ /Up ================")
    
    ..............<existing script>...........
    def getInfoNetbox(self): headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Token <token_netbox>' } url_request = "http://<ip_netbox>:8000/api/dcim/cables/" get_devices = requests.get(url_request, headers=headers, verify=False) .........<Netbox function>.......... def getInfosClearPass(self): header = { "Content-Type": "application/json", "Authorization": "Bearer <bearer_clearpass>" } uri_cppm = "https://<ip_clearpass>/api/network-device?limit=100" get_devices_cppm = requests.get(uri_cppm, headers=header, verify=False) .............<ClearPass function>........... def checks(self): .............<checks function>...........

    Que fait-on ici :

    1. Nous nous connectons à notre outil Netbox afin de savoir quel est l'équipement attendu sur l'interface locale de notre 8320, et nous récuperons l'ensemble des informations, grâce à la fonction "getInfoNetbox()".

    2. Nous vérifions que l'équipement récupéré dans NetBox est bien celui récupéré via LLDP, que ce soit en termes d'IP et de port distant de connexion. Selon la réponse, nous formatons une phrase pour indiquer le résultat, et ce grâce à la fonction "checks".

    2. Nous nous connectons à ClearPass afin de savoir si l'équipement est bien inscrit dans les Network Devices, grâce à la fonction "getInfosClearPass".

     

    En fonction des résultats, nous créons des ActionSyslog qui permettrons de pouvoir afficher les résultats dans la fenêtre de détails de l'évènement.

     

    Illustrons tous cela - Nous faisons un shutdown sur le port supervisé :

    Switch-CX# conf
    Switch-CX(config)# int 1/1/2
    Switch-CX(config-if)# shutdown

    Notre agent détecte l'évènement, et réalise l'action de "no shutdown" sur l'interface :Screenshot 2019-01-21 at 09.54.39.png

    L'interface est bien remontée, et les actions correspondantes sont bien réalisées - Dans ces dernières, nous voyons bien que les fonctions que nous avons rajouter, et qui permettent de pouvoir réaliser des actions particulières selon les informations récupérées sur des solutions tierces sont effectuées :

    Screenshot 2019-01-21 at 10.15.51.png

     

    Cependant, 3 éléments peuvent venir améliorer ce dispositif :

    1. Les éléments de connexion, tels que les tokens ou les adresses IP des serveurs distants, sont inscrits statiquement et en clair dans le script. Ce qui pose un réel probleme de sécurité, puisque toute personne ayant accès à NAE peut voir ces informations.

    2. L'affichage des résultats pourrait être optimisé.

    3. Nous voyons dans notre exemple que même si équipement qui est connecté n'est pas le bon, notre agent retourne en statut "normal". Nous devrions mettre un niveau d'alerte de type "Major" dans ce cas de figure.

     

    Nous allons voir comment y remédier.

     

    Encrypted parameters

     

    Avec l'arrivée de la version 10.02, il est désormais possible d'encrypter les informations sensibles dans NAE.

    Pour cela, il suffit d'utiliser la clé "Encrypted", et de la fixer à True dans la déclaration du paramètre, pour que ce dernier n'apparaissent nul part en clair. Par exemple, dans notre script :

     

    ParameterDefinitions = {
        .........<existing parameters>...........
        'netbox_token': {
            'Name': 'Netbox Token',
            'Description': 'Token used to connect to Netbox',
            'Type': 'string',
            'Default': '',
            'Encrypted': True
        },
        'netbox_ip': {
            'Name': 'Netbox IP',
            'Description': 'IP Address used to connect to Netbox',
            'Type': 'string',
            'Default': '',
            'Encrypted': True
        }
    }

    Il sffit alors, dans le corps du script, d'appeler le bon paramètre, comme par exemple :

    def getInfosClearPass(self):
            header = {
                "Content-Type": "application/json",
                "Authorization": "Bearer {}".format(self.params['clearpass_bearer'])
            }

    Ensuite, d'un point de vue exploitation, voici ce que cela donne lorsque l'on crée un nouvel agent :Screenshot 2019-01-21 at 14.30.30.png

    Les informations sensibles sont donc maintenant utilisées de manière confidentielles.

     

    Custom Reports

     

    Le second point d'amélioration que nous avons evoqué est un affichage plus optimisé des résultats retournées par les fonctions présentes dans notre script. Pour faire cela, nous allons utiliser la notion de "Custom Scripts".

    L'idée est d'afficher les résultats reçus dans une page dédiée, et accessible via les détails de l'évènement.

     

    Pour faire cela, modifions notre script de la manière suivante :

    def action_interface_up(self, event):
    ...........<existing script>................
    ActionCLI("show lldp neighbor {}",
    [self.params['interface_id']])
    self.createReport()
    self.remove_alert_level()
    self.logger.debug("================ /Up ================")

    def createReport(self):
    report = HTML_HEAD

    report += '<p><b>Remote Device Infos</b>'
    report += self.addLLDPInfos()

    report += '<p><b>What is expected by NetBox</b></p>'
    report += '<p>Remote device IP Verification : {} (Source : NetBox)'.format(self.check_remote_ip)

    report += '<br>Remote device Port Verification : {} (Source : NetBox)</p>'.format(self.check_remote_port)
    report += '<p><b>What is expected by ClearPass</b></p>'
    report += 'Remote device in ClearPass : {} (Source : CPPM)'.format(self.check_clearpass)

    report += '</div>'
    ActionCustomReport(report, title=Title("Remote Device Analysis"))


    .........<existing script>...................

    def addLLDPInfos(self):
    self.getLLDP()
    infos_hostname = "<p>Remote Device Name : {}".format(self.lldp_info[0]["neighbor_info"]['chassis_name'])
    report = infos_hostname
    infos_ip = "<br>Remote Device Name : {}</p>".format(self.lldp_info[0]["neighbor_info"]['mgmt_ip_list'])
    report += infos_ip
    return report

    .............<existing script>................

    HTML_HEAD = """<style>
    table {
    font-family: arial, sans-serif;
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 1cm;
    }

    td, th {
    border: 1px solid #dddddd;
    text-align: left;
    vertical-align: top;
    white-space: nowrap;
    font-size: medium;
    }

    tr:nth-child(even) {
    background-color: #d8d5e5;
    }
    tr:hover {
    background-color: #efa837;
    }
    #hidden {
    display: none;
    }
    :checked + #hidden {
    display: block;
    }
    </style>"""

    Que faisons nous ici :

    1. Au lieu d'utiliser la fonction ActionSyslog, nous appelons une fonction tierce, nommée "createReport". Cette fonction va créer le report et y intégrer les valeurs que nous souhaitons y mettre.

    2. La fonction createReport crée donc le template de page HTML qui va être utilisé. Dans ce cadre, nous gérons une variable, nommée "report", et dans laquelle nous mettons tout le template HTML complété avec les variables. Pour commencer ce report, nous intégrons les premières lignes HTML de la page, et qui sont contenues dans la variable "HTML_HEAD", définie à la fin du script. Ensuite nous y associons toutes les lignes, au format HTML.

    3. Une fois que le template est complété, nous faisons appel à la callback action "ActionCustomReport", à qui nous envoyons la variable "report", qui contient notre code HTML, et nous lui donnons un titre, ici "Remote Device Analysis"

     

    Verifions cela de manière concrète.

    Nous faisons un shut sur notre interface :

    Switch-CX# conf
    Switch-CX(config)# int 1/1/2
    Switch-CX(config-if)# shutdown

    L'action est donc déclenchée, et le port remonte aussitôt - Nous constatons alors un nouveau type d'action : l'"Analysis Report".

    Screenshot 2019-01-21 at 14.48.43.png

     

    Si on va le détail de l'évènement, on retrouve notre titre utilisé dans la fonction ActionCustomReport, à savoir "Remote Device Analysis", mais également le statut de la création du Report :

    Screenshot 2019-01-21 at 14.49.58.png

     Si on clique sur "Output" :Screenshot 2019-01-21 at 14.46.56.png

     

    On retrouve alors l'ensemble des informations que nous avions, formatées par le template/code HTML que nous généré, et ce dans une page nouvelle.

     

    Gestion des Alertes

     

    Le dernier point est de pouvoir créer une alerte spécifique si les tests de conformité au travers de notre outil Netbox et/ou de ClearPass ne sont pas bons.

    Pour cela, partons du principe que si un des tests n'est pas conforme a ce qui est attendu, alors nous mettons l'agent en alerte MAJOR, et non CRITICAL.

    Si tout est conforme, alors nous supprimons l'alerte, et revenons en statut NORMAL.

     

    Modifions le script en ce sens.

    Nous allons utiliser une variable, nommée "self.major", qui sera mise à True dès qu'un test de conformité n'est pas bon.

    A la fin, si cette variable est à True, alors le niveau d'alerte est mis à MAJOR. Sinon, l'alerte est enlevée.

     

    def action_interface_up(self, event):
            ........<existing script>.............
            self.major = False
                ...........<existing script>.............
                if self.major:
                    self.set_alert_level(AlertLevel.MAJOR)
                else:
                    self.remove_alert_level()
            self.logger.debug("================ /Up ================")
    
    def getInfosClearPass(self):
            ............<existing script>.............
            if present == 0:
                self.check_clearpass = "Remote device not in CPPM - We could automatically add it right now"
                self.major = True
            else:
                self.check_clearpass = "Remote Device already in CPPM"
    
    def checks(self):
            ..........<existing script>...........
            else:
                self.check_remote_ip = "This is not the requested device which is connected to this port - We were waiting for {}".format(self.ip_netbox)
                print(self.get_alert_level())
                self.major = True
            ..........<existing script>.........
                self.check_remote_port = "Remote device doesn't use the good uplink port - It should be connected on {}".format(
                    self.connected_port_netbox)
                print(self.get_alert_level())
                self.major = True

    Si nous regardons tout cela en action :

    Nous faisons un shut sur notre interface :

    Switch-CX# conf
    Switch-CX(config)# int 1/1/2
    Switch-CX(config-if)# shutdown

    L'action est donc déclenchée, et le port remonte aussitôt - Cependant, l'adresse IP de l'équipement connecté n'est pas celle attendue par notre outil NetBox. Le niveau d'alerte passe donc à MAJOR :

    Screenshot 2019-01-21 at 15.40.26.png

     Et les détails de l'alerte :

    Screenshot 2019-01-21 at 15.40.17.png

     

    Bien entendu, nous pourrions aller encore plus loin, en spécifiant par exemple, le point qui exactement ne passe pas la conformité.

    Dans tous les cas, l'administrateur est informé que le port est bien remonté, mais que quelque chose de non conforme est connecté.

     

    3. Le cas de VSX et les graphs

     

    La technologie VSX est une technologie unique sur le marché du Campus Switching, qui permet de pouvoir virtualiser uniquement le plan de données, tout en conservant des mecanismes simples d'exploitation, comme Active Gateway ou la synchronisation de blocs de configuration.

    Le plan n'etant du coup pas virtualiser, un agent NAE mis en place sur le VSX Primary n'est donc pas repliqué sur le Secondary.

    Du coup, comment optimiser et mutualiser les informations dans un agent sur le Primary ?

     

    VSX

     

    Reprenons le script de base sur la surveillance des ports, et modifions-le de la manière suivante :

    def __init__(self):
    .............<existing script>...........
    self.r1 = Rule('Interface Status changed on VSX Primary Peer')

    .........<existing script>.........

    uri2 = '/vsx-peer/rest/v1/system/interfaces/{}?attributes=link_state'
    self.m2 = Monitor(

    uri2,
    'Interface Link status on Secondary',
    [self.params['interface_id']])
    self.r2 = Rule('Interface Status changed on VSX Secondary Peer')

    self.r2.condition('transition {} from "up" to "down"', [self.m2])
    self.r2.action(self.action_interface_down_secondary)
    self.r2.clear_condition('transition {} from "down" to "up"', [self.m2])
    self.r2.clear_action(self.action_interface_up)

    Ce que nous faisons ici :

    Nous instancions un second Monitor, avec les règles, conditions et actions associées, en utilisant l'extension "vsx-peer" dans l'URI utilisée pour le Monitor, ce qui indique la récupération d'une valeur sur le Peer VSX.

     

    Cela nous donne donc une fois l'agent instancié :Screenshot 2019-01-19 at 14.43.06.png

     

    Cela nous donne une visbilité sur l'ensemble du cluster. 

    Cependant, comme c'est le cas ici, les lignes sont superposées - Ce qui ne facilite donc pas la lisibilité.

    Pour cela, nous allons introduire la notion de Graphs, et mettre en place un graphe par Monitor, donc pas interface monitorée.

     

    Les Graphs

     

    Reprenons notre script, et modifions le comme suit :

    def __init__(self):
            # Interface status
           .............<existing script>....................
            self.r1 = Rule('Interface Status changed on VSX Primary Peer')
            self.graph_primary = Graph([self.m1], title=Title("Interface Status on Primary"), dashboard_display=True)
    
            .............<existing script>...................
    
            self.r2 = Rule('Interface Status changed on VSX Secondary Peer')
            self.graph_secondary = Graph([self.m2], title=Title("Interface Status on Secondary"), dashboard_display=False)

    Ce que nous faisons ici :

    Nous instancions un graph par Monitor, en invoquant la fonction "Graph". Cette fonction prend en paramètre le Monitor qu'il doit afficher, un titre, et son statut sur le Dashboard. Cette dernière option permet d'indiquer quel graphe doit être afficher lorsqu'il est decidé d'afficher l'agent sur le Dashboard de la page "Analytics".

     

    Avec le code ci-dessus, voici ce l'aspect du nouvel agent :Screenshot 2019-01-19 at 16.19.34.png

     

    Nous voyons bien ici 2 graphes distincts - 1 par interface sur chaque peer VSX.

     

    Dernier element concernant cette partie : L'alerte est globale à l'agent. Comment faire alors pour savoir, et ainsi réaliser les actions adequates sur le bon equipement ?

    Il suffit alors de prendre en compte le dictionnaire "event". Pour cela, modifions notre script de la facon suivante :

    def action_interface_down(self, event):
    if self.get_alert_level() != AlertLevel.CRITICAL:
    if "Primary" in event['rule_description']:
    ActionSyslog(
    'Interface {} Link gone down on VSX Primary',
    [self.params['interface_id']])
    else:
    ActionSyslog(
    'Interface {} Link gone down on VSX Secondary',
    [self.params['interface_id']])
    self.set_alert_level(AlertLevel.CRITICAL)

    def action_interface_up(self, event):
    if self.get_alert_level() is not None:
    if "Primary" in event['rule_description']:
    ActionSyslog(
    'Interface {} Link came up on VSX Primary',
    [self.params['interface_id']])
    else:
    ActionSyslog(
    'Interface {} Link came up on VSX Secondary',
    [self.params['interface_id']])
    self.remove_alert_level()

    Comme nous l'avons vu dans un post précédent, le dictionnaire"event" est composé de plusieurs clés/éléments sur l'évènement qui vient de se passer. Notamment la clé "rule_description", qui reprend la description donnée dans la regle. Selon cette information, dans notre cas de figure, nous pouvons alors determiner de quel equipement vient l'alerte :Screenshot 2019-01-21 at 16.45.56.png