La semaine dernière, Aruba a publié la première version du module Python officiel pour ArubaOS-CX : PyAOSCX.
Ce module est désormais disponible au téléchargement et à l'installation, de manière à vous simplifier l'intégration et le développement de vos workflows d'automatisation sur ArubaOS-CX.
Nous allons voir ici comment le mettre en oeuvre.
1. Installation du module PyAOSCX
a. Pré-requis
Certains pré-requis sont nécessaires au fonctionnement de PyAOSCX.
Sur votre poste/serveur, il est nécessaire que Python soit en version 3.5 minimum.
Sur les équipements, le support de REST doit être activé, et un utilisateur doit être créé :
user <username> group administrators password ciphertext <password>
<!--------!>
https-server rest access-mode read-write
https-server vrf <vrf_name>
Pour rappel, sur les équipements des gammes 6x00, REST est actif par défaut, sur les VRFs "default" et "mgmt".
b. Installation
Comme tout module Python non disponible nativement avec l'interpréteur, il suffit tout simplement d'invoquer la commande "pip install" (ou "pip3 install") pour Python3.x.
Ces commandes installeront en quelques secondes le module mais également les modules complémentaires nécessaires à son bon fonctionnement ou à celui de certains des workflows proposés par Aruba.

Dans le cadre de PyAOSCX, seul le module additionnel PyYaml est nécessaire. Il sera installé automatiquement car étant indiqué dans le fichier "requirements.txt"
Le module Requests étant natif à Python.
Nous avons donc maintenant le module PyAOSCX installé sur notre poste/serveur, et nous pouvons commencer à l'utiliser afin de créer nos propres workflows.
2. Mode de fonctionnement de PyAOSCX
a. Import du module
L'import du module dans votre script se fait de manière traditionnelle, pas de surprise de ce côté-là.
Vous pouvez soit faire un import global, soit importer uniquement les objets dont vous avez besoin :
import pyaoscx
< --- ou --- >
from pyaoscx import *
< --- ou --- >
from pyaoscx import <liste_des_fonctions>
Par exemple : from pyaoscx import session, vlan, system, lldp
b. Eléments de base
Lorsque vous allez appeler les différents fonctions de PyAOSCX, 2 éléments doivent obligatoirement être joints en variables à la fonction :
1. Votre cookie de session, qui est généré lors de votre authentification. Ce cookie est nécessaire pour les appels à toutes les fonctions, sauf à la la fonction login bien entendu...
2. L'URL de base - Il s'agit de la partie invariable de l'URI de la ressource. Vous devez donc créer une variable qui contient cette URL de base. Elle peut pointer indifféremment sur les API v1 ou v10.04, PyAOSCX s'adaptera automatiquement. Cette URL de base est nécessaire pour toutes les fonctions, y compris pour l'authentification. Par exemple :
base_url = "https://<@IP>/rest/v10.04/"
c. Authentification / Ouverture d'une session
Pour ouvrir une session sur un équipement avec PyAOSCX, il faut utiliser l'objet "session".
from pyaoscx import session
session.login(<votre_url_de_base>, username="<username>", password="<password>")
Si on prend maintenant un exemple concret :
from pyaoscx import session
base_url = "https://<@IP>/rest/v1/"
cookie = session.login(base_url, username="admin", password="admin")
Votre session est donc ouverte, et le cookie persistent est inclut dans la variable "cookie".
d. Les paramètres
En fonction de la fonction appelée, différents paramètres peuvent être envoyé dans la fonction.
Par exemple, si nous prenons la fonction de création d'un VLAN :
pyaoscx.vlan.create_vlan(vlan_id, vlan_name, vlan_desc=None, vlan_type='static', admin_conf_state='up', **kwargs)
Ici :
- Les paramètres "vlan_id" et "vlan_name" sont obligatoires, car ils s'agit des éléments obligatoires dans la requête POST sous-jacente
- "vlan_desc", "vlan_type" et "admin_conf_state" sont optionnels. Cela est indiqué par le fait qu'ils sont indiqués avec un "=", ce qui indique un paramètre optionnel dans la fonction (kwargs)
- "**kwargs", qui représente le reste des éléments qui peuvent être donnés - Dans notre cas, les éléments de connexion (cookie + URL de base).
Il est également possible de donner des notions comme des attributs ou la profondeur (depth) :
system.get_system_info(params={"selector": "configuration"}, **session_dict)
e. Ce qui est disponible aujourd'hui dans PyAOSCX

3. Première requête
a. Exemple simple
Faisons maintenant notre première requête, en appelant la fonction "get_all_vlans", qui permet de lister l'ensemble des VLAN présents sur les équipements.
Pour ce faire :
from pyaoscx import session, system, evpn, vlan, vrf
from pprint import pprint
base_url = "https://<@IP>/rest/v1/"
cookie = session.login(base_url, username="admin", password="admin")
session_dict = dict(s=cookie, url=base_url)
pprint(vlan.get_all_vlans(**session_dict))
Ce qui nous donne :
[Running] python3 "/Python/CX/airheads-pyaoscx.py"
['/rest/v1/system/vlans/1',
'/rest/v1/system/vlans/90',
'/rest/v1/system/vlans/2001',
'/rest/v1/system/vlans/401',
'/rest/v1/system/vlans/60',
'/rest/v1/system/vlans/80',
'/rest/v1/system/vlans/100',
'/rest/v1/system/vlans/40',
'/rest/v1/system/vlans/50',
'/rest/v1/system/vlans/70',
'/rest/v1/system/vlans/91',
'/rest/v1/system/vlans/85']
Que peut-on voir ici, et qu'apprend-t-on sur le fonctionnement de PyAOSCX :
- Comme déjà évoqué, lorsqu'on appelle une fonction de PyAOSCX, il est nécessaire de lui fournir 2 éléments : Le cookie de session et l'URL de base. Ceci peut-être fait soit sous la forme de variable (s et url), soit sous la forme d'un dictionnaire.
vlan.get_all_vlans(s=cookie, url=base_url)
<---- ou ---->
# on crée notre dictionnaire comprenand le cookie et l'URL de base
session_dict = dict(s=cookie, url=base_url)
# on passe le dictionnaire en variable de la fonction
vlan.get_all_vlans(**session_dict)
- La fonction nous renvoie une liste des valeurs recherchées. C'est le comportement attendu d'une fonction "get_xxx". Les ayant un impact sur les paramètres (POST/PUT/DELETE) renverront True si l'opération c'est bien passée, ou False dans le cas contraire.
b. Objet common_ops
PyAOSCX embarque également un objet nommé "common_ops". Cet objet est là pour fournir des fonctions complémentaires comme la lecture d'un fichier YAML.
L'utilisation d'un fichier inventaire, souvent au format YAML, est intéressante pour stocker des informations communes et/ou sensibles à l'ensemble des équipements, sans avoir à les écrire en dur dans votre code.
C'est donc votre premier niveau de Source of Truth.
Pour cela, il suffit d'appeler la fonction read_yaml comme suit :
from pyaoscx import common_ops
# on cherche le chemin absolu de notre fichier YAML
folder_target = os.path.dirname(os.path.abspath(__file__))
file_target = os.path.join(folder_target, 'inventory.yml')
# on donne ce chemin absolu à la fonction pour lecture et extraction des données.
data = common_ops.read_yaml(file_target)
c. Fermeture de la session
Lorsque nous avons fini nos actions sur un équipement, il est préférable de fermer la session en cours.
Pour cela, il suffit tout simplement d'appeler la fonction "logout", qui est comprise dans l'objet session :
from pyaoscx import session
session.logout(**session_dict)
4. Cas concret
Nous allons imaginer maintenant un workflow plus complet : La création d'un nouveau réseau Overlay VXLAN.
L'idée est tout simplement de dire ici que l'infrastructure VXLAN/eVPN est déjà opérationnelle, mais que nous souhaitons créer un nouveau service, matérialisé par un VNI mappé sur un nouveau VLAN, entre 3*6300.
Pour cela, quel va être le workflow :
a. Nous avons nos informations nécessaires contenues dans un fichier YAML.
b. Nous nous connectons sur chacun sur chacun des équipements contenus dans le fichier YAML
c. Nous créons le VLAN demandé (VLAN ID = 87, VLAN Name = "new_service")
d. Si cela s'est bien passé, nous le mappons à une VNI, dont l'ID est également stocké dans le fichier YAML (VNI 8787)
e. Nous ajoutons ce nouveau VNI dans la liste de ceux annoncés par eVPN.
Fichier YAML :
switch_ips: [{"ip": "<@IP_6300-1>"},
{"ip": "<@IP_6300-2>"},
{"ip": "<@IP_6300-3>"}]
credentials: {
"username": "<username>",
"password": "<password>"
}
vxlan_infos:
{"vni": 8787,
"vxlan": "vxlan1",
"vlan_id": 87,
"vlan_name": "new_service"
}
evpn_infos:
{"vlan_id": 87,
"rd": "auto",
"export_route": ['auto'],
"import_route": ['auto']}
Vérifions au préalable que ce VNI 8787 n'existe pas sur l'infrastructure. Pour cela, on se connecte à NetEdit, on demande l'affichage du VNI 8787 sur la topologie et on vérifie s'il s'affiche ou non - Dans notre cas, non, donc on peut le créer sans risque d'impact sur le service du réseau.

Nous allons donc utiliser le script suivant :
from pyaoscx import session, evpn, vxlan, vlan, common_ops
import os
import timeit
def display_formatted(text, spaces):
text_length = len(text) + spaces
print(text.rjust(text_length))
def main():
# Read YAML File, providing the absolute path to the inventory file.
folder_target = os.path.dirname(os.path.abspath(__file__))
file_target = os.path.join(folder_target, 'inventory.yml')
data = common_ops.read_yaml(file_target)
# Global datas extrations from YAML
username = data['credentials']['username']
password = data['credentials']['password']
vlan_id = data['vxlan_infos']['vlan_id']
vlan_name = data['vxlan_infos']['vlan_name']
vni = data['vxlan_infos']['vni']
vxlan_id = data['vxlan_infos']['vxlan']
export_route = data['evpn_infos']['export_route']
import_route = data['evpn_infos']['import_route']
rd = data['evpn_infos']['rd']
# Launch the creation of New Service
for ip in data['switch_ips']:
print("Connection to device {}".format(ip['ip']))
try:
base_url = "https://{}/rest/v1/".format(ip['ip'])
session_dict = dict(s=session.login(base_url, username=username, password=password), url=base_url)
output = "| - Creating VLAN {}".format(vlan_id)
display_formatted(output, 3)
# We create the new VLAN
create_vlan = vlan.create_vlan(vlan_id=vlan_id, vlan_name=vlan_name, **session_dict)
if create_vlan == True:
output = "| - VLAN {} has been created".format(vlan_id)
display_formatted(output, 7)
output = "| - Mapping it to VNI {}".format(vni)
display_formatted(output, 3)
# If everything went fine, we create the new VNI and map it to the VLAN
vni_mapping = vxlan.add_vni_mapping(vni=vni, vxlan=vxlan_id, vlan=vlan_id, **session_dict)
if vni_mapping == True:
output = ("| - VNI {} has been mapped to VLAN {}".format(vni, vlan_id))
display_formatted(output, 7)
output = "| - Adding VLAN to eVPN"
display_formatted(output, 3)
# If everything went fine, we distribute the new VLAN/VNI into eVPN
evpn_vlan = evpn.add_evpn_vlan(vlan_id=vlan_id, export_route=export_route, import_route=import_route, rd=rd, **session_dict)
if evpn_vlan == True:
output = "| - VLAN has been added to eVPN"
display_formatted(output, 7)
else:
print("An issue occured")
else:
print("An issue occured")
else:
print("An issue occured")
except Exception as error:
print('Ran into exception: {}. Logging out..'.format(error))
finally:
session.logout(**session_dict)
output = "Logout from device {}".format(ip['ip'])
display_formatted(output, 3)
if __name__ == '__main__':
start_time = timeit.default_timer()
main()
print("Execution : {}".format(timeit.default_timer() - start_time))
Si nous lançons le script :
[Running] python3 "/Python/CX/pyaoscx-overlay-seq.py"
Connection to device <@IP_6300-1>
| - Creating VLAN 87
| - VLAN 87 has been created
| - Mapping it to VNI 8787
| - VNI 8787 has been mapped to VLAN 87
| - Adding VLAN to eVPN
| - VLAN has been added to eVPN
Logout from device <@IP_6300-1>
Connection to device <@IP_6300-2>
| - Creating VLAN 87
| - VLAN 87 has been created
| - Mapping it to VNI 8787
| - VNI 8787 has been mapped to VLAN 87
| - Adding VLAN to eVPN
| - VLAN has been added to eVPN
Logout from device <@IP_6300-2>
Connection to device <@IP_6300-3>
| - Creating VLAN 87
| - VLAN 87 has been created
| - Mapping it to VNI 8787
| - VNI 8787 has been mapped to VLAN 87
| - Adding VLAN to eVPN
| - VLAN has been added to eVPN
Logout from device <@IP_6300-3>
Execution : 2.188437562
L'execution s'est bien passée. Vous remarquerez que cette opération, qui peut prendre quelques minutes en CLI (Les temps de modifications de modifications de template, correction des erreurs de copier/coller, etc...), a pris 2,18 secondes via notre script.
Si nous regardons maintenant sur NetEdit :

Il est important de noter qu'aucun refresh de la page/topologie n'a été fait. L'apparition des tunnels s'est faite quelques secondes après l'application de notre script. Ceci est notamment dû au fait que NetEdit utilise des Websockets avec les équipements de manière à être extrêmement réactif.
Vous pouvez vérifier tout en cela en tapant la commande suivante sur vos équipements :
show bgp l2vpn evpn vni <vni_id>
5. Pour aller plus loin
Le module PyAOSCX est disponible sur le Github d'Aruba, accompagné de différents Worflows : PyAOSCX sur Github Aruba
Documentation en ligne : Documentation PyAOSCX