La gestion d'un parc d'équipements réseau peut s'avérer complexe et chronophage, surtout lorsqu'il s'agit de collecter régulièrement des informations vitales comme la version du système d'exploitation, le temps de fonctionnement, l'état des alimentations, des ventilateurs, ou l'utilisation du CPU et de la mémoire. Cet article présente une approche automatisée pour interroger de multiples équipements réseau en parallèle, en utilisant Python, la bibliothèque Netmiko pour la connectivité SSH, et le multithreading pour accélérer le prcoessus. Les données collectées sont ensuite structurées et exportées vers un fichier Excel pour une analyse facilitée.
Outils et Technologies
- Python : Le langage de programmation central pour orchestrer l'automatisation.
- Netmiko : Une bibliothèque Python qui simplifie la connexion SSH aux équipements réseau et l'exécution de commandes, en prenant en charge différents types d'appareils (Cisco, Juniper, H3C, etc.).
- Multithreading (
threading) : Permet d'exécuter des tâches concurrentes. Chaque équipement est interrogé dans son propre thread, réduisant ainsi considérablement le temps total d'exécution. - Openpyxl : Une bibliothèque pour la lecture et l'écriture de fichiers Excel (
.xlsx). Elle est utilisée ici pour formater et sauvegarder les résultats de l'inventaire. - TextFSM : Intégré à Netmiko, il permet de transformer la sortie non structurée des commandes CLI en données structurées (listes de dictionnaires), facilitant leur traitement.
Structure du Ficheir de Configuration des Équipements
Pour la liste des équipements à interroger, un fichier texte (par exemple, ip_list.txt) est utilisé. Chaque ligne de ce fichier représente un équipement et contient ses informations de connexion, séparées par le délimiteur ---. Le format attendu est le suivant :
num_serie---nom_equipement---adresse_ip---utilisateur---mot_de_passe---type_appareil
Par exemple :
1---DevName-1---192.168.56.11---admin---password---h3c_comware
2---DevName-2---192.168.56.12---admin---password---h3c_comware
Logique du Script
Le script suit les étapes principales ci-dessous :
- Chargement des Équipements : Le script lit le fichier
ip_list.txtet parse chaque ligne pour créer une liste de dictionnaires, chaque dictionnaire contenant les détails de connexion d'un équipement. - Collecte Concurrentielle : Pour chaque équipement, un nouveau thread est lancé. Ce thread établit une connexion SSH via Netmiko et exécute une série de commandes prédéfinies (par exemple,
display version,display power,display fan,display cpu,display memory). - Traitement des Sorties : La sortie de chaque commande est automatiquement parsée par TextFSM en données structurées. Ces données sont combinées avec les informations de base de l'équipement (numéro de série, nom, IP) et stockées dans un dictionnaire global partagé entre les threads, chaque clé correspondant à une commande et sa valeur étant une liste de résultats.
- Exportation vers Excel : Une fois toutes les données collectées par les threads, le script principal génère un fichier Excel. Pour chaque type de commande (par exemple, "Version", "Power"), une feuille de calcul distincte est créée. Les en-têtes sont automatiquement déterminés à partir des clés des données TextFSM. Des styles sont appliqués, y compris un formatage conditionnel pour mettre en évidence les états critiques (par exemple, un temps de fonctionnement nul, un statut "non normal" pour l'alimentation ou les ventilateurs, ou une utilisation élevée du CPU/mémoire). Les largeurs de colonne sont ajustées pour une meilleure lisibilité.
Exemple de Code Python
Le script suivant illustre la mise en œuvre de cette logique :
import threading
import time
import os
import json
from collections import defaultdict
from openpyxl import Workbook
from openpyxl.styles import PatternFill, Font, Border, Side
from openpyxl.utils import get_column_letter
from netmiko import ConnectHandler, NetmikoAuthenticationException, NetmikoTimeoutException
# --- Styles globaux pour le rapport Excel ---
HEADER_FILL = PatternFill(start_color='ADD8E6', end_color='ADD8E6', fill_type='solid') # Bleu clair
ALERT_FILL = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid') # Rouge clair
THIN_BORDER = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
class CollecteurReseau:
"""
Gère la collecte d'informations sur les équipements réseau en utilisant Netmiko et le multithreading.
"""
def __init__(self, chemin_fichier_equipements):
self.chemin_fichier_equipements = chemin_fichier_equipements
self.equipements = self._charger_equipements()
# Dictionnaire pour stocker les résultats: {nom_feuille: [liste_de_dictionnaires_de_resultats]}
self.resultats = defaultdict(list)
self.verrou_resultats = threading.Lock() # Verrou pour l'accès concurrentiel aux résultats
def _charger_equipements(self):
"""
Charge la liste des équipements à partir du fichier texte spécifié.
Le format attendu par ligne est: num_serie---nom_equipement---adresse_ip---utilisateur---mot_de_passe---type_appareil
"""
liste_equipements = []
try:
with open(self.chemin_fichier_equipements, 'r', encoding='utf-8') as f:
for ligne in f:
ligne = ligne.strip()
if not ligne or ligne.startswith('#'): # Ignorer les lignes vides ou commentées
continue
parties = ligne.split('---')
if len(parties) >= 6:
liste_equipements.append({
's_num': parties[0],
'nom_appareil': parties[1],
'ip': parties[2],
'username': parties[3],
'password': parties[4],
'device_type': parties[5]
})
else:
print(f"Ligne ignorée (format incorrect): {ligne}")
except FileNotFoundError:
print(f"Erreur: Fichier '{self.chemin_fichier_equipements}' introuvable.")
return liste_equipements
def _executer_commandes_sur_appareil(self, config_appareil, mappage_commandes):
"""
Connecte à un appareil et exécute les commandes spécifiées, stockant les résultats.
"""
ip = config_appareil['ip']
try:
print(f"Connexion à l'équipement {ip}...")
net_connect = ConnectHandler(**config_appareil)
print(f"Connecté avec succès à {ip}.")
# Informations communes à ajouter à chaque enregistrement de résultat
prefixe_commun = {
'num_serie': config_appareil['s_num'],
'nom_appareil': config_appareil['nom_appareil'],
'ip': ip
}
for commande, nom_feuille in mappage_commandes.items():
print(f"Exécution de '{commande}' sur {ip}...")
# Utilise TextFSM pour parser la sortie en un objet Python structuré
output = net_connect.send_command(commande, use_textfsm=True)
# Normaliser la sortie pour qu'elle soit toujours une liste de dictionnaires
if isinstance(output, dict):
output = [output]
elif not isinstance(output, list):
output = [] # Gérer les types de sortie inattendus
for item in output:
# Combiner les informations communes avec la sortie spécifique de la commande
enregistrement = {**prefixe_commun, **item}
with self.verrou_resultats:
self.resultats[nom_feuille].append(enregistrement)
net_connect.disconnect()
except NetmikoAuthenticationException:
print(f"Échec d'authentification pour {ip}. Vérifiez les identifiants.")
except NetmikoTimeoutException:
print(f"Délai d'attente dépassé pour la connexion à {ip}.")
except Exception as e:
print(f"Une erreur est survenue lors de la connexion ou de l'exécution sur {ip}: {e}")
def collecter_toutes_les_donnees(self, commandes_a_executer):
"""
Lance des threads pour collecter les données de tous les équipements en parallèle.
"""
threads = []
for appareil in self.equipements:
thread = threading.Thread(target=self._executer_commandes_sur_appareil, args=(appareil, commandes_a_executer))
threads.append(thread)
thread.start()
for thread in threads:
thread.join() # Attendre la fin de tous les threads
print("Collecte de données terminée pour tous les équipements.")
return self.resultats
def exporter_vers_excel(self, nom_fichier_sortie, donnees_collectees):
"""
Exporte les données collectées vers un fichier Excel, avec une feuille par type de commande.
"""
wb = Workbook()
# Supprimer la feuille par défaut créée par Openpyxl
if 'Sheet' in wb.sheetnames:
wb.remove(wb['Sheet'])
print(f"Écriture des données dans '{nom_fichier_sortie}'...")
for nom_feuille, liste_donnees in donnees_collectees.items():
if not liste_donnees:
print(f"Aucune donnée pour '{nom_feuille}', feuille ignorée.")
continue
ws = wb.create_sheet(title=nom_feuille)
# Déterminer les en-têtes à partir des clés du premier dictionnaire de données
en_tetes = list(liste_donnees[0].keys())
ws.append(en_tetes) # Ajouter les en-têtes comme première ligne
# Appliquer le style aux en-têtes
for col_idx, en_tete in enumerate(en_tetes, 1):
cellule_entete = ws.cell(row=1, column=col_idx)
cellule_entete.font = Font(bold=True)
cellule_entete.fill = HEADER_FILL
cellule_entete.border = THIN_BORDER
# Écrire les données et appliquer le style
for ligne_donnees in liste_donnees:
valeurs_ligne = [ligne_donnees.get(en_tete, '') for en_tete in en_tetes]
ws.append(valeurs_ligne)
# Appliquer le style général et le formatage conditionnel
for row_idx, ligne in enumerate(ws.iter_rows(min_row=1), 1):
for col_idx, cellule in enumerate(ligne, 1):
cellule.border = THIN_BORDER
# Logique de formatage conditionnel
if nom_feuille == 'Version' and en_tetes[col_idx-1] == 'uptime_short' and '0 week' in str(cellule.value).lower():
cellule.fill = ALERT_FILL
elif nom_feuille in ['Power', 'Fan'] and en_tetes[col_idx-1].endswith('_status') and 'normal' not in str(cellule.value).lower():
cellule.fill = ALERT_FILL
elif nom_feuille == 'CPU' and en_tetes[col_idx-1] == 'cpu_total_utilization':
try:
cpu_usage = float(str(cellule.value).strip('%').replace(',', '.'))
if cpu_usage > 80: # Seuil d'alerte pour le CPU
cellule.fill = ALERT_FILL
except ValueError:
pass
elif nom_feuille == 'Memory' and en_tetes[col_idx-1] == 'memory_usage_percent':
try:
mem_usage = float(str(cellule.value).strip('%').replace(',', '.'))
if mem_usage > 80: # Seuil d'alerte pour la mémoire
cellule.fill = ALERT_FILL
except ValueError:
pass
# Ajuster automatiquement la largeur des colonnes
for col_idx, en_tete in enumerate(en_tetes, 1):
max_longueur = 0
colonne_lettre = get_column_letter(col_idx)
for cellule in ws[colonne_lettre]:
try:
if len(str(cellule.value)) > max_longueur:
max_longueur = len(str(cellule.value))
except TypeError: # Gérer les cellules vides ou non-texte
pass
# Ajouter un peu de marge
ws.column_dimensions[colonne_lettre].width = (max_longueur + 2)
wb.save(nom_fichier_sortie)
print(f"Fichier Excel '{nom_fichier_sortie}' créé avec succès.")
def main():
"""
Fonction principale pour exécuter le processus de collecte et d'exportation.
"""
# Chemin vers le fichier de liste des équipements
# Ajustez ce chemin si 'ip_list.txt' n'est pas dans le répertoire parent
current_dir = os.path.dirname(os.path.abspath(__file__))
fichier_liste_equipements = os.path.join(current_dir, '..', 'ip_list.txt')
# Si le fichier est dans le même répertoire:
# fichier_liste_equipements = os.path.join(current_dir, 'ip_list.txt')
# Définir les commandes à exécuter et le nom de la feuille Excel correspondante
commandes_reseau = {
'display version': 'Version',
'display power': 'Power',
'display fan': 'Fan',
'display cpu': 'CPU',
'display memory': 'Memory',
}
# Nom du fichier Excel de sortie avec un horodatage
nom_fichier_excel_sortie = f"Rapport_Inventaire_Reseau_{time.strftime('%Y%m%d_%H%M%S')}.xlsx"
collecteur = CollecteurReseau(fichier_liste_equipements)
if not collecteur.equipements:
print("Aucun équipement à interroger. Fin du script.")
return
donnees_collectees = collecteur.collecter_toutes_les_donnees(commandes_reseau)
collecteur.exporter_vers_excel(nom_fichier_excel_sortie, donnees_collectees)
if __name__ == '__main__':
main()
Exemple de Sortie Console
L'exécution du script proudira une sortie similaire à celle-ci sur la console, indiquant la progression de la connexion et de l'exécution des commandes :
Chargement des équipements depuis le fichier ../ip_list.txt...
Connexion à l'équipement 192.168.56.12...
Connexion à l'équipement 192.168.56.11...
Connecté avec succès à 192.168.56.12.
Exécution de 'display version' sur 192.168.56.12...
Connecté avec succès à 192.168.56.11.
Exécution de 'display version' sur 192.168.56.11...
... (messages pour d'autres équipements et commandes) ...
Exécution de 'display memory' sur 192.168.56.12...
Exécution de 'display memory' sur 192.168.56.11...
Collecte de données terminée pour tous les équipements.
Écriture des données dans 'Rapport_Inventaire_Reseau_20231027_103000.xlsx'...
Fichier Excel 'Rapport_Inventaire_Reseau_20231027_103000.xlsx' créé avec succès.