Dernière modification le 3 mars 2022
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
Envoyer le lien de téléchargement à :
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é.
#!/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
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, comme400
en cas d’échec d’authentification. - Une valeur
5xx
si le serveur a rencontré une erreur interne telle qu’une erreur de ressource.
- Une valeur
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.
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