ALE AOS8 Web Services

Alcatel-Lucent Enterprise le rappelle régulièrement : « l’objectif est de n’avoir à terme que des commutateurs Ethernet en AOS8 ». Effectivement, tous les modèles qui sont sortis et se sont rajoutés au catalogue, parfois en remplacement de modèles existants, font tourner la version 8 de l’AOS. Dans ce contexte il était intéressant de voir les possibilités offertes en matière d’automatisation externe en environnement homogène.

Pour rappel, lorsqu’on est dans un contexte de mixité AOS6 et AOS8, les possibilités sont les suivantes :

  • Utiliser la librairie Netmiko avec Python, pour laquelle j’avais écrit le module AOS en 2017 et qui est toujours maintenu par Kirk Byers lors des différentes montées de version de la librairie.
  • Utiliser le Network Module ale_aos que j’ai écrit pour Ansible en 2020.

C’est la fonction Web Services qui permet de personnaliser et d’étendre l’interface de gestion sur les dispositifs AOS8. Dans le cas qui nous intéresse, il prend en charge l’utilisation d’une interface web basée sur REST qui interagit avec les variables de gestion AOS (MIB) et les commandes CLI. Donc, il fournit deux méthodes de configuration via la gestion directe des variables MIB ou l’utilisation de commandes CLI et prend en charge à la fois les formats de réponse en XML et en JSON.

Il ne faut pas confondre REST et le protocole RESTCONF qui utilise le modèle de données YANG. REST est un ensemble de directives pour l’architecture logicielle des systèmes distribués et AOS8 supporte les verbes suivants :

  • GET : pour récupérer des informations. C’est un équivalent grossier de SNMP / MIB GET mais aussi, à un niveau supérieur, un équivalent de la commande show. Ce verbe est utilisé exclusivement pour les commandes en lecture seule et sans autre effet secondaire.
  • PUT : pour créer de nouvelles informations, comme, par exemple, un nouveau VLAN. Il s’agit d’une opération d’écriture.
  • POST : fonctionne de la même façon que lors de la soumission de formulaires Web et il est utilisé, dans un contexte de service Web, pour mettre à jour des informations existantes.
  • DELETE : pour supprimer des informations existantes.

La sécurité est maintenue grâce à l’utilisation de sessions dans le backend et de cookies dans le frontend, ce qui équivaut à la sécurité HTTP actuelle pour les clients légers.

  • Authentification – Adhère à un modèle de Web Service, via son propre domaine REST et l’utilisation du verbe GET.
  • Autorisation – Suit le mécanisme d’autorisation habituellement utilisé dans WebView, où WebView vérifie avec Partition Manager à quelles familles d’autorisations appartient un utilisateur, spécifiant ainsi quelles tables MIB sont accessibles par cet utilisateur.
  • Cryptage – Si l’accès non crypté (HTTP) est autorisé, alors le service Web est autorisé sur le même transport. De même, si les ports d’écoute HTTP / HTTPS sont modifiés, le service Web sera disponible via ces ports.

Les éléments suivants sont utilisés pour créer l’URL REST :

  • Protocole – Le protocole peut être HTTP (en texte clair) qui utilise par défaut le port 80, ou HTTPS (chiffré) qui utilise par défaut le port 443.
  • Adresse du serveur [: port] – L’adresse IP est celle utilisée pour accéder au commutateur. Si le port d’écoute a été modifié, le numéro de port doit être ajouté. La combinaison de Protocol + Server address [: port] constitue le « endpoint » du Web Service.
  • Domaine – Il s’agit du premier élément que le service Web AOS REST examinera. Il indique dans quel domaine se trouve la ressource accédée :
    • AUTH – Utilisé pour les fonctions d’authentification.
    • MIB – Utilisé pour désigner l’accès aux variables MIB.
    • CLI – Utilisé pour demander au service Web d’exécuter les commandes CLI.
    • INFO – Utilisé pour renvoyer des informations sur une variable MIB.
  • URN – Représente la ressource à laquelle accéder. Par exemple, lors de la lecture d’informations du domaine MIB, les URNs sont des noms de variables MIB (dans la plupart des cas, des tables). Les URNs sont accessibles à l’aide des verbes suivants : GET, PUT, POST, DELETE.
  • Variables – Une liste de variables qui dépendent du domaine auquel se fait l’accès. Lors de la lecture du domaine MIB, il s’agit d’une liste de variables à extraire d’une table MIB.

Le format de sortie peut être codé en utilisant XML ou JSON. L’en-tête « Accept » peut être utilisée pour spécifier un type de sortie :

  • application/vnd.alcatellucentaos+json
  • application/vnd.alcatellucentaos+xml

En raison de la nature volatile du contenu renvoyé, le Web Service (producer) indiquera à tout système positionné entre lui et le « consumer » (inclus) de ne pas mettre en cache sa sortie. Les en-têtes suivantes sont renvoyées par le « producer » :

  • Cache-Control: no-cache, no-store
  • Pragma: no-cache
  • Vary: Content-Type

Les deux premières en-têtes indiquent que la mise en cache ne doit pas avoir lieu. La dernière en-tête est destinée aux serveurs proxy, les informant que l’en-tête Content-Type est une variable à ne pas mettre en cache. Si un serveur proxy décidait de ne pas respecter cette directive, il serait possible d’avoir des comportements inattendus tels que la récupération du format de données JSON après avoir spécifiquement demandé le format XML.

Pour la mise en application de REST sur AOS8, je ne trouve pas la documentation officielle très utile. En effet, la proposition se base sur la librairie consumer.py qui n’est pas officiellement maintenue et dont l’évolution n’est pas garantie. Pour ceux qui seraient intéressés pour jouer avec, je propose un téléchargement de la version la plus récente que je possède pour Python3.

ALE AOS8 consumer.py

Téléchargement gratuit

Envoyer le lien de téléchargement à :

Je confirme avoir lu et accepté la Politique de confidentialité.

Pour réaliser des scripts Python, je préfère utiliser la librairie requests qui est ma préférée pour le support d’HTTP. Son usage est simple pour gérer l’authentification, il suffit de créer une session et générer toute les requêtes dans le cadre de cette session.

Avant de pouvoir utiliser la MIB, il convient de l’analyser en la téléchargeant et en navigant dans les fichiers à l’aide d’un outil adapté.

AOS8 MIB
AOS8 MIB vlanTable
#!/usr/bin/env python
"""
    Restconf python3 example by Gilbert MOÏSIO
    Installing python dependencies:
    > pip install requests pyyaml
"""

import requests
import yaml
import json
from pprint import pprint


requests.packages.urllib3.disable_warnings()
headers = {'Accept': 'application/vnd.alcatellucentaos+json'}


if __name__ == '__main__':
    # get parameters from yaml file
    with open('hosts.yaml') as f:
        hosts = yaml.load(f, Loader=yaml.FullLoader)
    # iterate onto the list
    for host in hosts:
        # execute requests
        try:
            # create a session for persistence
            session = requests.Session()
            # login into the switch
            payload = {'username': host['username'],
                       'password': host['password']}
            uri = f"https://{host['host']}/auth/"
            session.get(uri, params=payload, verify=False)
            # get MIB structure using info domain
            uri = f"https://{host['host']}/info/vlanTable?"
            response = session.get(uri, headers=headers, verify=False)
            print('{0}{1} vlanTable MIB structure {1}{0}'.format('\n', '='*10))
            pprint(json.loads(response.text)['result']['data'])
            # add a new vlan using mib domain
            payload = {'mibObject0': 'vlanNumber:666',
                       'mibObject1': 'vlanDescription:go_to_hell'}
            uri = f"https://{host['host']}/mib/vlanTable"
            response = session.put(
                uri, data=payload, headers=headers, verify=False)
            print('{0}{1} add vlan 666 {1}{0}'.format('\n', '='*10))
            print(json.loads(response.text)['result']['error'])
            # modify vlan description using mib domain
            payload = {'mibObject0': 'vlanNumber:666',
                       'mibObject1': 'vlanDescription:this_one_is_better'}
            uri = f"https://{host['host']}/mib/vlanTable"
            response = session.post(
                uri, data=payload, headers=headers, verify=False)
            print('{0}{1} change vlan 666 description {1}{0}'.format('\n', '='*10))
            print(json.loads(response.text)['result']['error'])
            # get vlan informations with CLI command
            payload_str = '&cmd=show+vlan+666'
            uri = f"https://{host['host']}/cli/aos"
            response = session.get(uri, headers=headers,
                                   params=payload_str, verify=False)
            print('{0}{1} vlan 666 informations {1}{0}'.format('\n', '='*10))
            print(json.loads(response.text)['result']['output'])
            # delete vlan using mib domain
            payload = {'mibObject0': 'vlanNumber:666'}
            uri = f"https://{host['host']}/mib/vlanTable"
            response = session.delete(
                uri, data=payload, headers=headers, verify=False)
            print('{0}{1} delete vlan 666 {1}{0}'.format('\n', '='*10))
            print(json.loads(response.text)['result']['error'])
            # logout from the switch
            uri = f"https://{host['host']}/auth/?"
            session.get(uri, verify=False)
        except requests.exceptions.RequestException as e:
            print(e._raw)
---
- host: 192.168.199.193
  username: admin
  password: switch
AOS Python REST
AOS8 Python requests REST

Que ce soit au format XML ou JSON, la réponse contient les éléments suivants :

  • domaine – Montre comment le « producer » a interprété le paramètre de domaine. Dans la plupart des cas, il sera le même que celui passé par le « consumer ».
  • diag – Cet entier sera un code de diagnostic standard HTTP :
    • Une valeur 2xx si la commande a réussi. Dans la plupart des cas, 200 sera utilisé.
    • Une valeur 3xx si une ressource a été déplacée (non implémentée).
    • Une valeur 4xx si la requête contenait une erreur, comme 400 en cas d’échec d’authentification.
    • Une valeur 5xx si le serveur a rencontré une erreur interne telle qu’une erreur de ressource.
  • error – Peut être une chaîne contenant un message d’erreur en texte clair. Il peut également s’agir d’un tableau de ces chaînes au cas où le « producer » trouve plusieurs problèmes sur cette demande.
  • output – Dans certains cas, le sous-système interrogé peut souhaiter renvoyer du texte dans cette variable.
  • data – Si une requête GET est émise, cette variable doit contenir les valeurs interrogées sous une forme structurée.
AOS8 REST Response
AOS8 Réponse

Pour construire des playbooks Ansible, c’est le module uri qui est utilisé. Il y a un peu plus de manipulations à effectuer. En effet, il faut faire générer le cookie par l’authentification et le faire passer aux requêtes suivantes.

---
- name: This is a test to request AOS8 API
  hosts: ale
  vars:
    ansible_python_interpreter: "python"
  connection: local
  gather_facts: no
  tasks:
    - name: Login into the switch
      uri:
        url: "https://{{ ansible_host }}/auth/?username={{ username }}&password={{ password }}"
        validate_certs: no
        method: GET
      register: login
    - name: get MIB structure using info domain
      uri:
        url: "https://{{ ansible_host }}/info/vlanTable?"
        validate_certs: no
        method: GET
        return_content: yes
        headers:
          Accept: "application/vnd.alcatellucentaos+json"
          Cookie: "{{ login.cookies_string }}"
      register: result
    - name: Print body of the response
      debug:
        msg:
          - "{{ result.json.result.data }}" 
      when: result.json.result.diag  == 200
    - name: Add a new vlan using mib domain
      uri:
        url: "https://{{ ansible_host }}/mib/vlanTable"
        validate_certs: no
        method: PUT
        headers:
          Accept: "application/vnd.alcatellucentaos+json"
          Cookie: "{{ login.cookies_string }}"
        body_format: form-urlencoded
        body:
          mibObject0: "vlanNumber:666"
          mibObject1: "vlanDescription:go_to_hell"
      register: result
    - name: Print body of the response
      debug:
        msg:
          - "{{ result.json.result.error }}" 
      when: result.json.result.diag  == 200
    - name: Modify vlan description using mib domain
      uri:
        url: "https://{{ ansible_host }}/mib/vlanTable"
        validate_certs: no
        method: POST
        headers:
          Accept: "application/vnd.alcatellucentaos+json"
          Cookie: "{{ login.cookies_string }}"
        body_format: form-urlencoded
        body:
          mibObject0: "vlanNumber:666"
          mibObject1: "vlanDescription:this_one_is_better"
      register: result
    - name: Print body of the response
      debug:
        msg:
          - "{{ result.json.result.error }}" 
      when: result.json.result.diag  == 200
    - name: Get vlan informations with CLI command
      uri:
        url: "https://{{ ansible_host }}/cli/aos?cmd=show+vlan+666"
        validate_certs: no
        method: GET
        return_content: yes
        headers:
          Accept: "application/vnd.alcatellucentaos+json"
          Cookie: "{{ login.cookies_string }}"
      register: result
    - name: Print body of the response
      debug:
        msg:
          - "{{ result.json.result.output }}" 
      when: result.json.result.diag  == 200
    - name: Delete vlan using mib domain
      uri:
        url: "https://{{ ansible_host }}/mib/vlanTable"
        validate_certs: no
        method: DELETE
        headers:
          Accept: "application/vnd.alcatellucentaos+json"
          Cookie: "{{ login.cookies_string }}"
        body_format: form-urlencoded
        body:
          mibObject0: "vlanNumber:666"
      register: result
    - name: Print body of the response
      debug:
        msg:
          - "{{ result.json.result.error }}" 
      when: result.json.result.diag  == 200
    - name: Logout from the switch
      uri:
        url: "https://{{ ansible_host }}/auth/?"
        validate_certs: no
        method: GET
[ale]
6465T ansible_host=192.168.199.193

[ale:vars]
username=admin
password=switch
[defaults]
inventory = ./hosts
host_key_checking = False
roles_path = roles.galaxy:roles
stdout_callback = yaml
AOS8 Ansible REST
AOS8 Ansible uri REST