Forum Français

Reply
Highlighted
Aruba Employee

ArubaOS-CX et l'automatisation - Part 2 : Les APIs et Python

Comme nous l'avons vu dans le post précédent "ArubaOS-CX et l'automatisation - Part 1 : Les APIs et le Swagger", les API d'ArubaOS-CX sont extrêmement puissantes, et permettent d'effectuer un très grand nombre d'opérations et/ou de récupérer énormément d'informations.

Après l'utilisation des APIs d'ArubaOS-CX au travers de la WebUI, nous continuons donc cette découverte au travers de l'utilisation de scripts Python pour intéragir avec ces dernières.

 

1. Le Login / Logout

 

Comme vu dans le post précédent, l’authentification et les requêtes ne gèrent pas de notions de header – Un cookie persistant est généré à l’authentification, et est géré par l’interpréteur ou le navigateur.

Pour réaliser une requête d'authentification, nous pouvons donc utiliser la methodologie suivante :

Le module "requests" possède une fonction nommée "session", qui permet de pouvoir générer un nouvel objet, de type session, et qui possède l'ensemble des fonctions heritées du module requests, comme get, post, put ou delete, mais également de pouvoir stocker des informations réutilisables durant une même session, comme les cookies persistants.

Dans ce cadre, afin d'ouvrir une session, il est nécessaire de  :

  1. Générer un objet session.
  2. Créer un dictionnaire, incluant le login et le password
  3. Passer ce dictionnaire dans la requête, comme paramètre, et non comme contenu du corps de la requête.
  4. Stocker le retour dans l'objet session que nous venons de créer.

Ce qui donne, par exemple :

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

s = requests.session()

try:
    url_login = "https://<@IP_CX>/rest/v1/login"
    querystring = {"username":"<votre_user>","password":"<votre_password>"}
    post_login = s.post(url_login, verify=False, params=querystring)
    if post_login.status_code == 200:
        print("Login OK")
        print("Contenu de la reponse : {}".format(post_login.content))
print("Contenu du header : {}".format(post_login.headers))
else:
print("Login NOK")
print(post_login.status_code)
except Exception as error:
print('Ran into exception: {}. Logging out..'.format(error))

Ce qui vous donne comme resultat :

Login OK
Contenu de la reponse : b''
Contenu du header : {'Server': 'nginx', 'Date': 'Mon, 03 Dec 2018 14:09:23 GMT', 'Content-Length': '0', 'Connection': 'keep-alive', 'Set-Cookie': 'id=o5CDldM4a0c-gASq8w6wQQ==; Path=/; HttpOnly; Secure, user=eyJ1c2VyIjoiYWRtaW4iLCJsZXZlbCI6MTUsInR5cGUiOiJMT0NBTCIsIm1ldGhvZCI6IkxPQ0FMIn0=; Path=/; Secure', 'X-Frame-Options': 'SAMEORIGIN', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains;', 'Content-Security-Policy': "script-src 'self' 'unsafe-inline'; object-src 'none'; font-src *; media-src 'none'; form-action 'self';"}

Vous voyez ainsi que le corps de réponse est vide, et que le header ne contient aucune information susceptible d'etre utilisée comme un cookie de session - Cependant, la session est bien ouverte, le cookie étant alors inscrit dans ce nouvel objet.

Si vous incluez dans votre code :

s = requests.session()
print("Type de la variable s : {}".format(type(s)))

Vous obtiendrez :

Type de la variable s : <class 'requests.sessions.Session'>

Vous pouvez dorenavant utiliser n'importe quelle méthode du module requests, tant qu'elle est utilisée avec l'objet session.

 

Comme équivalence, avec l'utilisation de la variable "params", c'est comme si on ajoutait les paramètres dans l'URI. Pour illustrer cela, si vous ajoutez dans votre code : 

post_login = s.post(url_login, verify=False, params=querystring)
print("URI : {}".format(post_login.url))

Vous obtiendrez alors :

URI : https://<@IP_CX>/rest/v1/login?username=<votre_user>&password=<votre_password>  

Pour fermer la session, il suffit alors tout simplement d'utiliser l'objet "session" que l'on a créé, et de faire un POST sur l'URI "/rest/v1/logout" :

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

s = requests.session()

try:
    url_login = "https://<@IP_CX>/rest/v1/login"
    querystring = {"username":"<votre_user>","password":"<votre_password>"}
    post_login = s.post(url_login, verify=False, params=querystring)
    if post_login.status_code == 200:
        print("Login OK")
    else:
        print("Login NOK")
        print(post_login.status_code)
except Exception as error:
    print('Ran into exception: {}. Logging out..'.format(error))
finally:
    url_logout = "https://<@IP_CX>/rest/v1/logout"
    post_logout = s.post(url_logout, verify=False)
    if post_logout.status_code == 200:
        print("Logout OK")
    else:
        print("Logout NOK")
        print(post_logout.status_code)

Ce qui nous donne tout simplement :

 

Login OK
Logout OK

Si on regarde maintenant au niveau des logs/debug sur l'équipement :

 

Sur les logs, nous voyons bien le login et le logout, lié au process "hpe-restd" :

Screen Shot 2018-12-03 at 08.55.35.png

 Sur le debug :Screen Shot 2018-12-03 at 08.59.31.png

 

Attention :

N'oubliez pas qu'il n'y a que 2 sessions simultanées maximum possibles. Il est donc important de fermer les sessions dès que le script est terminé, mais également d'avoir une bonne gestion des exceptions ("Try... Except... Finally")

 

2. Les requêtes

 

Maintenant que nous avons vu les requêtes pour s'authentifier et se désauthentifier, il est nécessaire de voir les requêtes afin de pouvoir récupérer de l'information, ou créer de nouveaux objets.

 

Comme évoqué, la méthodologie est extrêmenent simple, et similaire aux requêtes sur les équipements AOS-Switch, puique nous utilisons les fonctions get, post, put et delete, associées à l'objet session. Le traitement se fait ensuite de la même manière.

 

Prenons d'abord une requête simple de type GET, afin d'afficher la liste des VLANs :

 

url_login = "https://<@IP_CX>/rest/v1/login".format(ip)
querystring = {"username":"<votre_user>","password":"<votre_password>"}
post_login = s.post(url_login, verify=False, params=querystring)
url_vlans = "https://<@IP_CX>/rest/v1/system/bridge/vlans"
get_vlans = s.get(url_vlans, verify=False)
print("Resultat de la commande :")
pprint.pprint(get_vlans.json())

Nous obtenons alors le résultat suivant :

 

Resultat de la commande :
['/rest/v1/system/bridge/vlans/1',
 '/rest/v1/system/bridge/vlans/490',
 '/rest/v1/system/bridge/vlans/1104',
 '/rest/v1/system/bridge/vlans/421',
 '/rest/v1/system/bridge/vlans/482',
 '/rest/v1/system/bridge/vlans/411',
 '/rest/v1/system/bridge/vlans/412',
 '/rest/v1/system/bridge/vlans/1410',
..........
]

Comme sur la partie WebUI/Swagger, cela donne un premier niveau d'information, mais cela reste difficile à exploiter.

 

Nous allons donc ajouter l'argument de profondeur(depth) à l'URI :

 

url_login = "https://<@IP_CX>/rest/v1/login"
querystring = {"username":"<votre_user>","password":"<votre_password>"}
post_login = s.post(url_login, verify=False, params=querystring)
url_vlans = "https://<@IP_CX>/rest/v1/system/bridge/vlans?depth=1"
get_vlans = s.get(url_vlans, verify=False)
print("Resultat de la commande :")
pprint.pprint(get_vlans.json())

Ce qui nous donne alors un résultat beaucoup plus détaillé :

 

Resultat de la commande :
[{'aclmac_in_statistics': {},
  'aclmac_in_status': {},
  'aclmac_in_subsystem_states': {},
  'aclmac_out_statistics': {},
  'aclmac_out_status': {},
  'aclv4_in_statistics': {},
  'aclv4_in_status': {},
  'aclv4_in_subsystem_states': {},
  'aclv4_out_statistics': {},
  'aclv4_out_status': {},
  'aclv6_in_statistics': {},
  'aclv6_in_status': {},
  'aclv6_in_subsystem_states': {},
  'aclv6_out_statistics': {},
  'aclv6_out_status': {},
  'admin': 'up',
  'flood_enabled_subsystems': [],
  'id': 2003,
.............]

Au contraire de la première requête, cela nous donne énorménent d'informations, puisque l'ensemble des informations de configuration et de statistiques sont renvoyées, et ce pour chaque VLAN.

 

Afin de simplifier un peu le retour, ajoutons maintenant une notion de filtres, de manière à obtenir uniquement les VLAN ID, Names, ainsi que le statut opérationnel :

 

url_login = "https://<@IP_CX>/rest/v1/login"
querystring = {"username":"<votre_user>","password":"<votre_password>"}
post_login = s.post(url_login, verify=False, params=querystring)
url_vlans = "https://<@IP_CX>/rest/v1/system/bridge/vlans?attributes=id%2Cname%2Coper_state%2Coper_state_reason&depth=1" 
get_vlans = s.get(url_vlans, verify=False) print("Resultat de la commande :")
pprint.pprint(get_vlans.json())

Ce qui nous donnera le résultat suivant :

 

Resultat de la commande :
[{'id': 2003,
  'name': 'VLAN2003',
  'oper_state': 'down',
  'oper_state_reason': 'no_member_port'},
 {'id': 414,
  'name': 'Contractors',
  'oper_state': 'up',
  'oper_state_reason': 'ok'},
...............
]

Les autres méthodes, de type POST, PUT et DELETE se gèrent de la même manière que des requêtes traditionnelles, en prenant en compte, comme demontré précédemment, l'ensemble des méthodes du module requests.

 

Par exemple :

# POST
payload = {
    "value1": 1,
    "value2": 2
        }
request = s.post(url, data=json.dumps(payload, verify=False))

# PUT
payload = {
    "value1": 1,
    "value2": 2
        }
request = s.put(url, data=json.dumps(payload, verify=False))

Attention :

Certains caractères spéciaux peuvent être observés dans l’URI – Ils sont utilisés afin de remplacer certains caractères qui ne passeraient pas dans une URL.

  • « %2C » : Remplace la « , », car les attributs sont gérés sous forme de tableau
  • « %3A » : Remplace le « : »
  • « %24 » : Remplace le « $ »

3. Exemple concret

 

Prenons dorénavant un exemple concret pour illustrer tout ce que nous venons de voir.

Nous allons voir un script permettant de pouvoir afficher le listing des interfaces uniquement avec un status "UP", et le remote device associé, si ce dernier sait répondre en LLDP.

 

import requests

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

s = requests.session()

url_login = "https://<@IP_CX>/rest/v1/login"
querystring = {"username":"<votre_user>","password":"<votre_password>"}
post_login = s.post(url_login, verify=False, params=querystring)

# Cette premiere requete nous permet de pouvoir recuperer le nombre d'interfaces physiques avec un LInk State a UP, et dont l'interface physique est ready - On affiche alors le resultat url_int_count = "https://<@IP_CX>/rest/v1/system/interfaces?count=true&filter=link_state%3Aup%2Cphysical_interface_state%3Aready" get_inter_count = s.get(url_int_count, verify=False) print("Il y a {} interfaces physiques UP".format(get_inter_count.json()['count']))
# Cette deuxieme requete permet de recuperer ce fameux listing d'interfaces, mais uniquement le nom. url_interfaces = "https://<@IP_CX>/rest/v1/system/interfaces?attributes=link_state%2Cname&depth=1&filter=link_state%3Aup%2Cphysical_interface_state%3Aready" get_interfaces = s.get(url_interfaces, verify=False)

# Nous faisons une boucle dans le JSON récupéré, afin de pouvoir récuperer individuellement les noms d'interfaces, ce qui nous sert à pouvoir lancer une 3e requête, permettant ainsi de pouvoir récupérer le voisin LLDP sur cette interface - On affiche alors les résultats. for int in get_interfaces.json(): local_int = int['name'].replace("/","%2F") url_lldp = "https://<@IP_CX>/rest/v1/system/interfaces/{}/lldp_neighbors?depth=1".format(local_int) get_lldp = s.get(url_lldp, verify=False) print("Nom de l'interface : {} - Statut : {} - LLDP Neighbor : {}".format(int['name'], int['link_state'], get_lldp.json()[0]["neighbor_info"]['chassis_name']))
# On se deloggue. url_logout = "https://<@IP_CX>/rest/v1/logout" post_login = s.post(url_logout, verify=False)

Cela nous permettra alors d'obtenir :

Il y a 3 interfaces physiques UP
Nom de l'interface : 1/1/1 - Statut : up - LLDP Neighbor : 3810M-1
Nom de l'interface : 1/1/3 - Statut : up - LLDP Neighbor : Aruba-3810M-24G-PoEP-1-slot
Nom de l'interface : 1/1/2 - Statut : up - LLDP Neighbor : Aruba-3810M-24G-PoEP-1-slot 

4. VSX et les API

 

VSX a amené de très grands avantages en termes de résilience de l'infrastructure, mais également en termes de simplification de l'exploitation.

Sur ce dernier point, la Synchronisation d'éléments de configuration, comme les VLAN, est bien entendu la face la plus visible de cette simplification.

Screen Shot 2018-12-03 at 15.01.10.png

 

Cependant, il existe un autre aspect, et qui est lié aux API.

En effet, VSX a amené la possibilité d'envoyer une requête sur le peer secondaire d'un cluster VSX en dirigeant la requete vers le primary.

Il suffit pour cela d'ajouter tout simplement l'element "/vsx-peer/" dans la requête.

Par exemple :

  

Peer local : https://<@IP_primaire>/rest/v1/system/bridge/vlans 
Peer VSX : https://<@IP_primaire>/vsx-peer/rest/v1/system/bridge/vlans

Si nous prenons un exemple concret, lorsque le Net Admin est connecté sur le primary, il lui est possible de passer une commande aussi bien sur le switch local que sur le Peer VSX, et ce sans avoir à ouvrir une nouvelle session sur ce dernier :

 

Switch-CX# sh vlan summary
Number of existing VLANs: 26
Number of static VLANs:   26
Number of dynamic VLANs:  0
Switch-CX# sh vlan summary vsx-peer
Number of existing VLANs: 23
Number of static VLANs:   23
Number of dynamic VLANs:  0

Si on prend un exemple concret via les API :

 

url_login = "https://<@IP_CX_Primary>/rest/v1/login"
querystring = {"username":"<votre_user>","password":"<votre_password>"}
post_login = s.post(url_login, verify=False, params=querystring)
url_vlans_primary = "https://<@IP_CX_Primary>/rest/v1/system/bridge/vlans?count=true"
get_vlans_primary = s.get(url_vlans_primary, verify=False)
print("Nombre de VLANs sur le Peer Primary")
print(get_vlans_primary.json()['count'])
url_vlans_secondary = "https://<@IP_CX_Primary>/vsx-peer/rest/v1/system/bridge/vlans?count=true"
get_vlans_secondary = s.get(url_vlans_secondary, verify=False)
print("Nombre de VLANs sur le Peer Secondary")
print(get_vlans_secondary.json()['count'])

et voici le résultat :

 

Nombre de VLANs sur le Peer Primary
26
Nombre de VLANs sur le Peer Secondary
23

Nous voyons bien ici que nous avons ouvert une seule session, sur le primaire, et avons eu la possibilité de passer la requête vers chacun des Peers VSX, grâce à l'argument "/vsx-peer/".

 

L'intérêt majeur de cette simplification est donc de pouvoir requêter les 2 peers d'un même cluster en utilisant toujours la même adresse IP, en l'occurence celle du primary, et ce malgré la séparation des plans de contrôle et de management, et au travers d'une seule et même session REST ouverte.

 

5. Ressources

 

Afin de pouvoir aller plus en détails sur les notions ci-dessus, vous pouvez utiliser les ressources suivantes :

- Chaine Youtube "Airheads Broadcasting Channel" : Python et CX

- Documentation Configuration Guide sur les API d'ArubaOS-CX

 

A bientôt.

Search Airheads
cancel
Showing results for 
Search instead for 
Did you mean: