Automatisation dans la fabric

💡
Toutes les configurations sont commit sur mon github : https://github.com/Nathan0510/Blog

Prérequis

Il faut savoir nos besoins :

  • Création du VLAN
  • Associer le VLAN à une VNI
  • Propager le VLAN dans le BGP

Quel outil allons nous l'utiliser ?

Je suis très fan des API. Le principe d'une API est d'aller interroger un équipement via une requête HTTP. Assez simple non ?

Et dans cette requête, on va pouvoir passer des arguments en paramètres comme la création d'un VLAN, etc.

Chouette tout ça ! Bon comment cela se configure sur les Arista ?

Tout ceci a été réalisé dans un environnement de pré production ! Les IPs et les Switchs ne sont pas ceux du backbone sur EVE-NG.

Configuration

Encore plus simple que ça, ça existe ?

Leaf2(config)#management api http-commands
Leaf2(config-mgmt-api-http-cmds)#no shutdown

Leaf2#sh management api http-commands
Enabled:            Yes
HTTPS server:       running, set to use port 443
URLs
------------------------------------------------
Management1 : https://192.168.1.67:443

Allons regarder de plus près l'affichage web via un navigateur :

L'interface est simple et facile à comprendre. Rentrons un show version et appuyons sur submit :

Ca à l'air de marcher mais c'est pas très beau comme ça et surtout inutilisable !

API

Ce que je veux c'est que sur une VM, je puisse interroger l'API du switch et pour ce faire, on va utiliser le paquet curl.

Avant toute chose, il faut savoir la différence entre une RESTAPI et une API JSON RPC.

La première c'est que les paramètres vont être dans l'URL, par exemple : https://leaf2/api/v2/vlan/200

La deuxième c'est que les paramètres vont être dans les données JSON dans la requête HTTP.

L'API des Arista est une API JSON RPC.

Allez, un petit exemple : on curl Leaf2 en souhaitant afficher le résultat d'un show version.

root@vm:~# curl -k -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["show version"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin

{"jsonrpc": "2.0", "id": 1, "result": [{"mfgName": "Arista", "modelName": "vEOS-lab", "hardwareRevision": "", "serialNumber": "91FE206A61F0EE1CB46FD9F2C72297B2", "systemMacAddress": "50:00:00:03:37:66", "hwMacAddress": "00:00:00:00:00:00", "configMacAddress": "00:00:00:00:00:00", "version": "4.31.0F", "architecture": "i686", "internalVersion": "4.31.0F-33804048.4310F", "internalBuildId": "91e041b1-47db-4422-b025-5ed27d4ce4a4", "imageFormatVersion": "1.0", "imageOptimization": "None", "bootupTimestamp": 1719602419.645532, "uptime": 1309.54, "memTotal": 3970556, "memFree": 2909464, "isIntlVersion": false}]}

Explication des options :

        • -k : Bypass l'erreur certificat SSL
        • -X POST : Méthode
        • -H : Header
        • -d : Données JSON

On voit que la commande est bien dans les données JSON !

Par contre, je sais pas vous mais le résultat est moche. Je vais utiliser jq pour le traitement de la réponse.

root@vm:~# curl -k -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["show version"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "mfgName": "Arista",
      "modelName": "vEOS-lab",
      "hardwareRevision": "",
      "serialNumber": "91FE206A61F0EE1CB46FD9F2C72297B2",
      "systemMacAddress": "50:00:00:03:37:66",
      "hwMacAddress": "00:00:00:00:00:00",
      "configMacAddress": "00:00:00:00:00:00",
      "version": "4.31.0F",
      "architecture": "i686",
      "internalVersion": "4.31.0F-33804048.4310F",
      "internalBuildId": "91e041b1-47db-4422-b025-5ed27d4ce4a4",
      "imageFormatVersion": "1.0",
      "imageOptimization": "None",
      "bootupTimestamp": 1719602419.6416435,
      "uptime": 1351.27,
      "memTotal": 3970556,
      "memFree": 2909840,
      "isIntlVersion": false
    }
  ]
}

C'est bien mieux comme ça, n'est ce pas ?

Admettons, je cherche uniquement à récupérer la version :

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["show version"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq -r .result[].version
  
4.31.0F

Easy ?

Bon d'abord le -r permet d'enlever les guillemets. Ensuite, on aperçoit que version est présent dans result[]. De ce fait, on rajoute ce string dans notre commande. Et on souhaite quoi ? La version. Donc on rajoute version à la fin.

Pas bien compliqué nan ?

Bon, un autre exemple : on cherche l'IP de l'interface management.

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "show ip interface brief"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq -r
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {},
    {
      "interfaces": {
        "Ethernet1": {
          "name": "Ethernet1",
          "lineProtocolStatus": "up",
          "interfaceStatus": "connected",
          "mtu": 1500,
          "ipv4Routable240": false,
          "ipv4Routable0": false,
          "interfaceAddress": {
            "ipAddr": {
              "address": "0.0.0.0",
              "maskLen": 0
            }
          },
          "nonRoutableClassEIntf": false
        },
        "Ethernet2": {
          "name": "Ethernet2",
          "lineProtocolStatus": "up",
          "interfaceStatus": "connected",
          "mtu": 1500,
          "ipv4Routable240": false,
          "ipv4Routable0": false,
          "interfaceAddress": {
            "ipAddr": {
              "address": "0.0.0.0",
              "maskLen": 0
            }
          },
          "nonRoutableClassEIntf": false
        },
        "Loopback0": {
          "name": "Loopback0",
          "lineProtocolStatus": "up",
          "interfaceStatus": "connected",
          "mtu": 65535,
          "ipv4Routable240": false,
          "ipv4Routable0": false,
          "interfaceAddress": {
            "ipAddr": {
              "address": "0.0.0.0",
              "maskLen": 0
            }
          },
          "nonRoutableClassEIntf": false
        },
        "Loopback1": {
          "name": "Loopback1",
          "lineProtocolStatus": "up",
          "interfaceStatus": "connected",
          "mtu": 65535,
          "ipv4Routable240": false,
          "ipv4Routable0": false,
          "interfaceAddress": {
            "ipAddr": {
              "address": "0.0.0.0",
              "maskLen": 0
            }
          },
          "nonRoutableClassEIntf": false
        },
        "Management1": {
          "name": "Management1",
          "lineProtocolStatus": "up",
          "interfaceStatus": "connected",
          "mtu": 1500,
          "ipv4Routable240": false,
          "ipv4Routable0": false,
          "interfaceAddress": {
            "ipAddr": {
              "address": "192.168.1.67",
              "maskLen": 24
            }
          },
          "nonRoutableClassEIntf": false
        }
      }
    }
  ]
}
root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "show ip interface brief"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq -r .result[1].interfaces.Management1.interfaceAddress.ipAddr.address

192.168.1.67

Pas grand chose qui change hormis le nombre de mot.

Par contre pourquoi le 1 ? Car result retourne deux listes :

  1. Une liste vide {}
  2. Une liste avec les informations de nos interfaces

Le 1 correspond donc au deuxième élément de la liste result.

Bon, c'est bien beau tout ça mais notre objectif reste le déploiement automatique des VLANs dans la fabric ! Allez on va s'y mettre :

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "vlan 1000", "name TEST-API"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin
  
{"jsonrpc": "2.0", "id": 1, "result": [{}, {}, {}, {}]}

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["show vlan"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq '.result[] | .vlans["1000"]'

{
  "name": "TEST-API",
  "dynamic": false,
  "status": "active",
  "interfaces": {}
}

Même logique que les autres requêtes ! On renseigne la commande dans les données JSON et on l'applique. Le deuxième curl permet de vérifier que le VLAN a bien été poussé sur le switch.

On fait la même chose pour l'interface VXLAN et le BGP :

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "interface vxlan1", "vxlan vlan 1000 vni 21000"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin
{"jsonrpc": "2.0", "id": 1, "result": [{}, {}, {}, {}]}

  
root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "show vxlan vni 21000"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin | jq '.result[1].vxlanIntfs.Vxlan1.vniBindings["21000"]'

{
  "vlan": 1000,
  "dynamicVlan": false,
  "source": "static",
  "interfaces": {
    "Vxlan1": {
      "dot1q": 1000
    }
  }
}

root@vm:~# curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "router bgp 65102", "vlan 1000", "rd 65102:21000", "route-target both 1000:21000", "redistribute learned"],"format":"json"},"id":1}' https://Leaf2/command-api --user admin:admin

Ok c'est good ! On avance bien par contre, il faudrait trouver un moyen pour ne pas avoir à taper les commandes les une par une.

BASH & SQL

Et pour ce faire, on va scripter avec Bash !

root@vm:~# cat script.sh
#/bin/bash


for HOST in Leaf1 Leaf2
do

curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "vlan '$1'", "name '$2'"],"format":"json"},"id":1}' https://$HOST/command-api --user admin:admin


curl -k -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["show vlan"],"format":"json"},"id":1}' https://$HOST/command-api --user admin:admin | jq '.result[] | .vlans["'$1'"]'

done

On commence par préciser le shebang du script. On souhaite l'exécuter en bash donc /bin/bash.

Ensuite, on fait une boucle qui parcourt Leaf1 et Leaf2. Au premier tour de la boucle, la variable HOST aura pour valeur Leaf1 et au second Leaf2.

On écrit notre commande curl. Si vous regardez bien, il y a $1, $2 et $HOST. C'est quoi ça ?

$1 et $2 vont être les paramètres renseignés en 1ère position et 2nd position lors de l'appel du script.

$HOST va être la variable HOST qu'on a définit dans la boucle.

Allez on lance le script avec 1003 comme ID VLAN et NARUTO-NINJA comme nom :

root@vm:~# ./script.sh 1003 NARUTO-NINJA
{"jsonrpc": "2.0", "id": 1, "result": [{}, {}, {}, {}]} 
{
  "name": "NARUTO-NINJA",
  "dynamic": false,
  "status": "active",
  "interfaces": {}
}
{"jsonrpc": "2.0", "id": 1, "result": [{}, {}, {}, {}]} 
{
  "name": "NARUTO-NINJA",
  "dynamic": false,
  "status": "active",
  "interfaces": {}
}

La création automatique des VLANs est terminée !!! Hé plutôt simple nan ?

Cependant, ca risque de devenir problématique à savoir si le VLAN qu'on souhaite déployer est déjà utilisé sur les switchs. Il faut bien stocker l'information quelque part.

Quoi de mieux qu'une base de données ? (MYSQL ;))

Avant de créer la database et les tables, un schéma s'impose ! Toujours plus intéressant de se poser 10 minutes avant de créer sa base de données (car quand c'est en prod, difficile de changer).

mysql> show DATABASES;
+--------------------+
| Database           |
+--------------------+
| Fabric             |
| information_schema |
| performance_schema |
+--------------------+
4 rows in set (0,00 sec)

mysql> use Fabric;
Database changed
mysql> show TABLES;
Empty set (0,00 sec)

mysql> CREATE TABLE SWITCHS (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     nom VARCHAR(255) NOT NULL,
    ->     bgp_as INT
    -> );
Query OK, 0 rows affected (0,01 sec)

mysql>
mysql> CREATE TABLE VLANS (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     vlan_number INT NOT NULL,
    ->     vni_number INT NOT NULL,
    ->     switch_id INT,
    ->     FOREIGN KEY (switch_id) REFERENCES SWITCHS(id)
    -> );
Query OK, 0 rows affected (0,01 sec)

mysql> show TABLES;
+------------------+
| Tables_in_Fabric |
+------------------+
| SWITCHS          |
| VLANS            |
+------------------+
2 rows in set (0,00 sec)

mysql> INSERT INTO SWITCHS (nom, bgp_as) VALUES
    -> ('Leaf1', 65100),
    -> ('Leaf2', 65102);
Query OK, 2 rows affected (0,00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from SWITCHS;
+----+-------+--------+
| id | nom   | bgp_as |
+----+-------+--------+
|  1 | Leaf1 |  65100 |
|  2 | Leaf2 |  65102 |
+----+-------+--------+
2 rows in set (0,00 sec)

Notre BDD est prête ! Complétons notre script bash :

#!/bin/bash

# Vérifier que le numéro de VLAN et le nom du VLAN sont fournis
if [ -z "$1" ] || [ -z "$2" ]; then
  echo "Usage: $0  "
  exit 1
fi

# Variables
VLAN_NUMBER=$1
VLAN_NAME=$2
VNI_NUMBER="20$1"

for HOST in Leaf1 Leaf2
do

# Récupérer l'AS BGP du switch
BGP_AS=$(mysql -A -sN -D Fabric -se "SELECT bgp_as FROM SWITCHS WHERE nom='${HOST}';")


SWITCH_NAME=$HOST

# TEST POUR VOIR SI VLAN DEJA USE

add_vlan=$(mysql -A -sN -D Fabric -se "SELECT vlan_number
FROM VLANS
WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = '{HOST}');")

if echo "$add_vlan" | grep -q "\<$VLAN_NUMBER\>"
then
    echo "Le VLAN $VLAN_NUMBER existe déjà pour le switch $SWITCH_NAME."
    exit 1
fi

# TEST POUR VOIR SI VNI DEJA USE

add_vni=$(mysql -A -sN -D Fabric -se "SELECT vni_number
FROM VLANS
WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = '${HOST}');")

if echo "$add_vni" | grep -q "\<$VNI_NUMBER\>"
then
    echo "La VNI $VNI_NUMBER existe déjà pour le switch $SWITCH_NAME."
    exit 1
fi


# CREER ET MAJ SQL

curl -k -s -o /var/log/fabric/$HOST.log -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "vlan '$VLAN_NUMBER'", "name '$VLAN_NAME'"],"format":"json"},"id":1}' https://$HOST/command-api --user admin:admin


curl -k -s -o /var/log/fabric/$HOST.log -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "interface vxlan1", "vxlan vlan '$VLAN_NUMBER' vni '$VNI_NUMBER'"],"format":"json"},"id":1}' https://$HOST/command-api --user admin:admin


curl -k -s -o /var/log/fabric/$HOST.log -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"runCmds","params":{"version":1,"cmds":["enable", "configure terminal", "router bgp '$BGP_AS'", "vlan '$VLAN_NUMBER'", "rd '$BGP_AS':'$VNI_NUMBER'", "route-target both '$VLAN_NUMBER':'$VNI_NUMBER'", "redistribute learned"],"format":"json"},"id":1}' https://$HOST/command-api --user admin:admin

# Récupérer l'ID du switch
SWITCH_ID=$(mysql -A -sN -D Fabric -se "SELECT id FROM SWITCHS WHERE nom='${SWITCH_NAME}';")

# Insérer le VLAN et le VNI dans la table VLANS
mysql -A -sN -D Fabric << "EOF
INSERT INTO VLANS (vlan_number, vni_number, switch_id) VALUES (${VLAN_NUMBER}, ${VNI_NUMBER}, ${SWITCH_ID});
EOF"

echo "VLAN $VLAN_NUMBER et VNI $VNI_NUMBER ont été ajoutés au switch $HOST."

done

Oh le bordel ! Pas tant que ça avec mes beaux commentaires 😄

Et si on teste :

root@vm:~# ./test.sh 3018 TEST-SQL
VLAN 3018 et VNI 203018 ont été ajoutés au switch Leaf1.
VLAN 3018 et VNI 203018 ont été ajoutés au switch Leaf2.


mysql> SELECT vlan_number FROM VLANS WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = 'Leaf1');
+-------------+
| vlan_number |
+-------------+
|        3018 |
+-------------+

mysql> SELECT vlan_number FROM VLANS WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = 'Leaf2');
+-------------+
| vlan_number |
+-------------+
|        3018 |
+-------------+

Je suis trop le GOAT du DEVOPS ?

Et si on reteste la commande :

root@tacacs:~# ./test.sh 3018 TEST-SQL
Le VLAN 3018 existe déjà pour le switch Leaf1.

Nickel, j'ai mon code d'erreur et je quitte le code sans exécuter les commandes SQL et API.

Mais tout faire en CLI, c'est pas dingue, n'est ce pas ?

Hé bah allons donc coder notre frontend en PHP/HTML !

FRONTEND

Le frontend est l'aspect que le client peut voir. La plupart du temps, on a aucune connaissance du backend (moi c'est du Bash/Sql par exemple).

Je récupère avec un formulaire HTML le numéro du VLAN et le nom de ce dernier. Le nom du switch ne sert à rien étant donné que je fais une boucle avec tous les switchs (d'ailleurs il faudrait que je supprime).

On remplit les informations :

Et si on repousse avec les mêmes données :

Ca à l'air de bien fonctionner ma connerie.

Et backend, ca donne quoi ?

mysql> SELECT vlan_number FROM VLANS WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = 'Leaf1');
+-------------+
| vlan_number |
+-------------+
|        3035 |
+-------------+
3 rows in set (0.00 sec)

mysql> SELECT vlan_number FROM VLANS WHERE switch_id = (SELECT id FROM SWITCHS WHERE nom = 'Leaf2');
+-------------+
| vlan_number |
+-------------+
|        3035 |
+-------------+
3 rows in set (0.00 sec) 

J'ai fait du tri entre le moment où je poussais le script en CLI et en GUI !

Et les switchs dans tout ça ? C'est bien poussé dessus ?

Leaf1#sh run | s vlan 3035
vlan 3035
   name TEST
interface Vxlan1
   vxlan vlan 3035 vni 203035
router bgp 65100
   vlan 3035
      rd 65100:203035
      route-target both 3035:203035
      redistribute learned

EASY ?

Suite du code

A terme, je ne souhaite plus passer par le CLI des switchs pour pousser de la configuration. Tout doit se faire depuis un point de contrôle unique : le site que je suis en train de coder.

Je sais que c'est assez rudimentaire. Pour ma défense, je n'ai pas fait d'études dans le développement mais dans le réseau. L'éducation secondaire tend à s'adapter au monde de demain (un monde où le réseau, la virtualisation et le devops seront le même) mais a beaucoup de retard. Il y a quatre ans, on nous apprenait toujours les classes A, B et C :( !

A l'heure où j'écris cet épisode, j'ai réussi à coder la récupération des macs sur chaque switch en fonction du VLAN (information reprise en GET en PHP), récupérer la configuration d'une interface (L2 access, L2 Trunk ou L3). J'avance tranquillement tout en alimentant ce blog !

Bien que j'adore faire moi même, je compte installer CloudVision Portal d'Arista (équivalent à l'APIC chez Cisco). Ensuite, il existe un provider pour terraform. Je pousserai la configuration comme ça. Par contre, CVP demande 22Go de RAM et 11 core...

Je termine sur un petit mot de fin : Kishimoto le goat, Oda la fraude