rss logo

Superviser les serveurs Dell PowerEdge avec Python, iDRAC et SNMPv3

SnmpTDell, script Python de supervision des serveurs Dell PowerEdge via iDRAC et SNMPv3

Si, comme moi, vous devez administrer une douzaine de millions de serveurs Dell PowerEdge (j’exagère peut-être un tout petit peu), vous souhaitez certainement vous assurer qu’ils sont tous en bon état. En effet, plus le nombre de serveurs augmente, plus le risque de panne matérielle est élevé et plus leur surveillance manuelle devient difficile. C’est pourquoi j’ai développé mon propre script de supervision (sans l’aide de l’IA ! La qualité du code devrait suffire à le prouver… 😅).

J’utilisais auparavant check_openmanage, un plugin Nagios écrit en Perl et développé par l’université d’Oslo. Malheureusement, il n’a pas été mis à jour depuis longtemps et son site d’origine semble avoir disparu d’Internet. De plus, son utilisation nécessitait l’installation de Dell OpenManage, ce qui pouvait s’avérer assez compliqué sous Linux, notamment sur les distributions non officiellement prises en charge par Dell.

Faute de solution de remplacement adaptée, j’ai décidé de développer mon propre outil : un script Python qui envoie des requêtes SNMP directement aux interfaces iDRAC des serveurs pour surveiller leur état matériel. Pour cela, j’ai utilisé PySNMP, une implémentation multiplateforme du protocole SNMP entièrement écrite en Python. Il n’est donc pas nécessaire d’installer Dell OpenManage ni un agent de supervision supplémentaire sur le système d’exploitation des serveurs. Une interface iDRAC correctement configurée, avec SNMP activé, suffit.

Mes principales exigences lors du développement de ce script étaient d’utiliser exclusivement SNMPv3 et de permettre son exécution sous Windows comme sous Linux, sans installer Dell OpenManage ni aucun agent supplémentaire sur les serveurs supervisés.

Architecture de supervision

L’architecture est simple : le script Python SnmpTDell s’exécute sur une machine Windows ou Linux pouvant joindre les interfaces iDRAC des serveurs Dell PowerEdge à superviser.

Le script communique directement avec chaque interface iDRAC en SNMPv3, sur le port UDP 161, pour récupérer les informations sur l’état matériel. La supervision passant par iDRAC, aucun agent supplémentaire n’est nécessaire sur les serveurs supervisés.

Architecture de supervision Dell PowerEdge : un script Python interroge trois interfaces iDRAC en SNMPv3 sur le port UDP 161
Architecture de supervision matérielle des serveurs Dell PowerEdge avec SnmpTDell, Python, iDRAC et SNMPv3.

Installer Python sous Windows ou Linux

Comme indiqué dans l’introduction, le script Python SnmpTDell fonctionne sous Windows et sous Linux. Bien que Python soit installé par défaut sur de nombreuses distributions Linux, une installation manuelle peut être nécessaire. Sous Windows, vous devrez généralement installer Python avant d’exécuter le script.

Windows

Pour installer Python sous Windows, rendez-vous sur la page officielle de téléchargement de Python et téléchargez la dernière version de Python Install Manager. Lancez son programme d’installation, puis utilisez ce gestionnaire pour installer Python.

Page officielle de téléchargement de Python Install Manager pour Windows
Téléchargement de Python Install Manager depuis le site officiel de Python.
  • Une fois Python installé, ouvrez un terminal Windows et installez les paquets pysnmp et cryptography nécessaires :
C:\Users\user> py -m pip install pysnmp cryptography

Installer Python sous Debian

La procédure suivante repose sur Debian 13, mais elle devrait également fonctionner sur la plupart des distributions Linux, moyennant quelques adaptations.

  • Installez le paquet python3-venv pour permettre la création d’environnements virtuels Python :
root@host:~# apt update && apt install python3-venv
  • Avec un compte utilisateur standard, créez un environnement virtuel Python dédié au script SnmpTDell :
john@host:~$ python3 -m venv .snmptdell
  • Activez l’environnement virtuel Python :
john@host:~$ source .snmptdell/bin/activate
  • Installez les paquets pysnmp et cryptography nécessaires dans l’environnement virtuel :
(.snmptdell) john@host:~$ python3 -m pip install pysnmp cryptography

Configurer SNMPv3 sur Dell iDRAC

Nous allons maintenant configurer SNMPv3 sur l’interface Dell iDRAC. Pour cela, nous créerons un compte utilisateur dédié, que notre script Python de supervision utilisera pour récupérer les informations sur l’état matériel.

Pour des raisons de sécurité, je recommande d’utiliser un compte dédié disposant uniquement des privilèges nécessaires. Cela limite les conséquences d’une éventuelle compromission des identifiants. Notre script ne faisant que consulter des informations de supervision, il n’est pas nécessaire de lui accorder des droits d’administration.

  • Connectez-vous à l’interface web iDRAC en HTTPS, puis accédez à iDRAC Settings > Users (paramètres iDRAC > utilisateurs) :
Interface web Dell iDRAC9 Enterprise : menu iDRAC Settings et option Users pour configurer un compte SNMPv3
Dell iDRAC9 Enterprise : accès à iDRAC Settings > Users pour configurer un compte dédié à la supervision SNMPv3.
  • Dans Local Users (utilisateurs locaux), cliquez sur Add (ajouter) pour créer un compte dédié à la supervision SNMPv3 :
Dell iDRAC9 Enterprise : bouton Add dans Local Users pour créer un compte de supervision SNMPv3
Création d’un compte utilisateur local dans Dell iDRAC9 Enterprise pour la supervision SNMPv3.
  • Configurez le compte dédié à la supervision SNMPv3 avec les paramètres suivants :
    • User Name : saisissez un nom d’utilisateur dédié, par exemple snmptdell.
    • Password : définissez un mot de passe robuste.
    • User Role : sélectionnez None et laissez tous les privilèges utilisateur iDRAC décochés.
    • IPMI User Privileges : réglez les niveaux de privilèges LAN et Serial Port sur No Access et désactivez Serial Over LAN.
    • SNMP v3 : sélectionnez Enabled.
    • Authentication Type : sélectionnez SHA.
    • Privacy Type : sélectionnez AES.
    • Enable Passphrase : sélectionnez Enabled et définissez des phrases secrètes robustes pour l’authentification et le chiffrement.
  • Une fois la configuration terminée, cliquez sur Save (enregistrer) pour créer le compte.
Compte SNMPv3 Dell iDRAC9 dédié à la supervision avec authentification SHA, chiffrement AES et privilèges restreints
Configuration d’un compte de supervision SNMPv3 dédié dans Dell iDRAC9, avec authentification SHA, chiffrement AES et privilèges restreints.

💡 Note de sécurité : les algorithmes d’authentification et de chiffrement SNMPv3 disponibles dépendent de la génération d’iDRAC et de la version de son micrologiciel. Lorsque cela est possible, privilégiez des algorithmes plus robustes, comme SHA-256 plutôt que SHA-1. Le script SnmpTDell peut tester automatiquement plusieurs combinaisons d’authentification et de chiffrement SNMPv3.

Le script Python SnmpTDell.py

Les OID SNMP de Dell iDRAC

Comme expliqué dans l’introduction, le script Python SnmpTDell utilise SNMPv3 pour récupérer des informations matérielles auprès des interfaces Dell iDRAC. Chaque information est associée à un identifiant d’objet (OID, Object Identifier), qui permet au script d’interroger un composant précis et d’en récupérer l’état.

Le tableau suivant répertorie les OID SNMP référencés dans le script : informations système, BIOS, batteries, disques physiques et alimentations.

Pour en savoir plus sur ces OID et leurs valeurs possibles, consultez le guide de référence SNMP officiel de Dell iDRAC.

Les OID SNMP de Dell iDRAC
Nom Identifiant d’objet (OID) Description Remarques / Valeurs
Informations système
systemServiceTag 1.3.6.1.4.1.674.10892.5.1.3.2.0 Renvoie le Service Tag Dell du serveur. Identifiant unique utilisé par le support Dell et pour l’identification de la garantie.
systemFQDN 1.3.6.1.4.1.674.10892.5.1.3.1.0 Renvoie le nom de domaine complet (FQDN) du serveur. Permet d’identifier le serveur supervisé.
systemExpressServiceCode 1.3.6.1.4.1.674.10892.5.1.3.3.0 Renvoie le code de service express Dell. Autre identifiant utilisé par le support Dell.
systemModelName 1.3.6.1.4.1.674.10892.5.1.3.12.0 Renvoie le nom du modèle du serveur. Permet d’identifier le modèle Dell PowerEdge.
globalSystemStatus 1.3.6.1.4.1.674.10892.5.2.1.0 Renvoie l’état matériel global du serveur. 1=Autre, 2=Inconnu, 3=OK, 4=Non critique, 5=Critique, 6=Irrécupérable.
Batteries et BIOS
batteryState 1.3.6.1.4.1.674.10892.5.5.1.20.130.15.1.4.1 Renvoie l’état actuel de la batterie du contrôleur de stockage. 1=Inconnu, 2=Prête, 3=Défaillante, 4=Dégradée, 5=Absente, 6=En charge, 7=Sous le seuil. Instance 1.
Version du BIOS 1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1 Renvoie la version du micrologiciel BIOS installé. Permet d’identifier la version du BIOS installée.
Date de publication du BIOS 1.3.6.1.4.1.674.10892.5.4.300.50.1.7.1.1 Renvoie la date de publication du BIOS installé. Utile pour le suivi des versions du BIOS.
État de la pile CMOS 1.3.6.1.4.1.674.10892.5.4.200.10.1.52.1 Renvoie des informations sur l’état de la pile CMOS. Permet de détecter d’éventuels problèmes de pile CMOS. La disponibilité dépend du modèle du serveur.
Disques physiques et virtuels
physicalDiskProductID 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.6 Renvoie le modèle ou l’identifiant produit d’un disque physique. Nécessite un index d’instance de disque.
physicalDiskNumber 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.1 Identifie une entrée dans la table des disques physiques. Permet d’identifier les instances de disque. Ne correspond pas nécessairement à l’emplacement physique du disque.
physicalDiskComponentStatus 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.24 Renvoie l’état matériel d’un disque physique. 1=Autre, 2=Inconnu, 3=OK, 4=Non critique, 5=Critique, 6=Irrécupérable.
physicalDiskSmartAlertIndication 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.31 Indique si un disque physique a signalé une alerte de panne prédictive. Permet de détecter les signes d’une éventuelle panne de disque avant qu’elle ne survienne.
physicalDiskSerialNo 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.7 Renvoie le numéro de série d’un disque physique. Utile pour identifier un disque précis lors du diagnostic d’une panne matérielle.
physicalDiskRevision 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.8 Renvoie la révision du micrologiciel d’un disque physique. Permet d’identifier la version du micrologiciel du disque.
Alimentations
powerSupplyStatus 1.3.6.1.4.1.674.10892.5.4.600.12.1.5 Renvoie l’état matériel d’un bloc d’alimentation (PSU). 1=Autre, 2=Inconnu, 3=OK, 4=Non critique, 5=Critique, 6=Irrécupérable. Nécessite un index d’instance.

SnmpTDell.py : script Python de supervision SNMPv3

Copiez le script Python de supervision ci-dessous dans un fichier nommé SnmpTDell.py, sur la machine Windows ou Linux qui communiquera avec les interfaces iDRAC des serveurs Dell PowerEdge à superviser.

#! /usr/bin/python
import asyncio
from pysnmp.hlapi.v3arch.asyncio import *
import re
# Modules used to test host reachability with ping
import subprocess
import platform

# Script version
v_script = "20261110"
# Device information
auth_proto = usmHMACSHAAuthProtocol  # or usmHMACMD5AuthProtocol
priv_proto = usmAesCfb128Protocol      # or usmAesCfb128Protocol, usmAesCfb192Protocol, etc.

# Supported SNMP authentication and privacy protocols
SNMP_PROTOCOLS = [
    ("SHA512", "AES256", usmHMAC384SHA512AuthProtocol, usmAesCfb256Protocol),
    ("SHA1",   "AES128", usmHMACSHAAuthProtocol, usmAesCfb128Protocol),
    ("MD5",    "DES",    usmHMACMD5AuthProtocol,       usmDESPrivProtocol),
    ("SHA256", "AES256", usmHMAC192SHA256AuthProtocol, usmAesCfb256Protocol),
    ("SHA256", "AES128", usmHMAC192SHA256AuthProtocol, usmAesCfb128Protocol),
]

# Device information
liste_oid = { "servicetag": '1.3.6.1.4.1.674.10892.5.1.3.2.0', "nom_serveur": '1.3.6.1.4.1.674.10892.5.1.3.1.0', "servicecode": '1.3.6.1.4.1.674.10892.5.1.3.3.0', "modele": '1.3.6.1.4.1.674.10892.5.1.3.12.0', "statut_global": '1.3.6.1.4.1.674.10892.5.2.1.0', "status_battery": '1.3.6.1.4.1.674.10892.5.5.1.20.130.15.1.4.1', "bios_version": '1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1', "bios_releasedate": '1.3.6.1.4.1.674.10892.5.4.300.50.1.7.1.1', "cmos_battery": '1.3.6.1.4.1.674.10892.5.4.200.10.1.52.1' }
liste_oid_disques = { "nom_disque": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.6', "emplacement_disque": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.1', "status_disques": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.24', "status_smart": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.31', "status_vd": '1.3.6.1.4.1.674.10892.5.5.1.20.140.1.1.4', "sn_disques": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.7', "modele_disques": '1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.8' }
liste_oid_alim = { "ps_status": '1.3.6.1.4.1.674.10892.5.4.600.12.1.5' }
resultats = []

class Disques(object):
        "Disk information container"

class Alimentations(object):
        "Power supply information container"

# Server class definition
class Serveur(object):
        def __init__(self, disques, alimentations, servicetag):
                self.disques = disques
                self.alimentations = alimentations
                self.servicetag = servicetag

        def afficher_info(self):
                print(f"Brand: {self.marque}, Model: {self.modele}")


# Detect the supported SNMP authentication and privacy protocol combination
async def detect_snmp_protocols(host, user, authkey, privkey, test_oid='1.3.6.1.4.1.674.10892.5.1.3.2.0', port=161):
    for auth_name, priv_name, auth_proto, priv_proto in SNMP_PROTOCOLS:
        #print(f"Test {auth_name} + {auth_proto} + {priv_name} + {priv_proto} on {host}...")

        try:
            errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(
                SnmpEngine(),
                UsmUserData(
                    user,
                    authKey=authkey,
                    privKey=privkey,
                    authProtocol=auth_proto,
                    privProtocol=priv_proto,
                ),
                await UdpTransportTarget.create(
                    (host, port),
                    timeout=2,
                    retries=0
                ),
                ContextData(),
                ObjectType(ObjectIdentity(test_oid))
            )

            if errorIndication:
                #print(f"  Failed: {errorIndication}")
                continue

            if errorStatus:
                print(f"  SNMP error: {errorStatus.prettyPrint()}")
                continue

            #print(f"  OK : {auth_name} + {auth_proto} + {priv_name} + {priv_proto}")

            return auth_proto, priv_proto

        except Exception as e:
            print(f"  Exception : {e}")

    return None, None

# Execute SNMP GET and BULK WALK requests
async def snmp(host, port, user, authkey, privkey, auth_proto, priv_proto, start_oid, type="walk"):
    resultats = [] # Reset the results list
    if type=="get":
        #async def run_snmp_get():
            iterator = await get_cmd(
            SnmpEngine(),
            UsmUserData(
                user,
                authKey=authkey,
                privKey=privkey,
                authProtocol=auth_proto,
                privProtocol=priv_proto,
            ),
            await UdpTransportTarget.create((host, port)),
            ContextData(),
            ObjectType(ObjectIdentity(start_oid)),
                        lookupMib=False, # Return numeric OIDs (e.g. 1.3.6.1.4.1.674) instead of MIB names
            )

            errorIndication, errorStatus, errorIndex, varBinds = iterator

            if errorIndication:
                print(errorIndication)

            elif errorStatus:
                print(
                "{} at {}".format(
                errorStatus.prettyPrint(),
                errorIndex and varBinds[int(errorIndex) - 1][0] or "?",
                )
            )

            else:
                for varBind in varBinds:
                    #print(" = ".join([x.prettyPrint() for x in varBind]))
                    return (" = ".join([x.prettyPrint() for x in varBind]))

    if type=="walk":
            snmpEngine = SnmpEngine()
            errorIndication, errorStatus, errorIndex, varBindTable = await bulk_cmd(
                    snmpEngine,
                    UsmUserData(user,
                    authKey=authkey,
                    privKey=privkey,
                    authProtocol=auth_proto,
                    privProtocol=priv_proto,
                    ),
                    await UdpTransportTarget.create((host, port)),
                    ContextData(),
                    0,
                    150,
                    #*varBinds,
                    ObjectType(ObjectIdentity(start_oid)),
                    lookupMib=False, # Return numeric OIDs (e.g. 1.3.6.1.4.1.674) instead of MIB names
            )

            if errorIndication:
                    print(errorIndication)
            elif errorStatus:
                    print(
                        f"{errorStatus.prettyPrint()} at {varBinds[int(errorIndex) - 1][0] if errorIndex else '?'}"
                    )
            else:
                    for varBind in varBindTable:
                        # Workaround: filter BULK results by the requested OID prefix to avoid unrelated entries
                        TEST = str(start_oid + '.').split('10892')[1] # Extract the part after the enterprise branch 10892
                        if TEST in str(" = ".join([x.prettyPrint() for x in varBind])): # Keep only entries matching the requested OID branch
                           #print(" = ".join([x.prettyPrint() for x in varBind]))
                           #return varBind
                           #return (" = ".join([x.prettyPrint() for x in varBind]))
                           resultats.append(" = ".join([x.prettyPrint() for x in varBind]))
            return resultats

            varBinds = varBindTable

def check_host(host, user, authkey, privkey, port=161, auth_proto=usmHMACSHAAuthProtocol, priv_proto=usmAesCfb128Protocol):
    "Check iDRAC reachability with ping, then SNMP."
    # Ping test   
    # Prepare the ping command
    system = platform.system().lower() # Detect the operating system
    ip = host
    if system == "windows":
        cmd = ["ping", "-n", "1", "-w", "100", ip]
    else:
        cmd = ["ping", "-c", "1", "-W", "1", ip]
    
    try:
        result = subprocess.run(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=2
        )
    except subprocess.TimeoutExpired:
        print("IP address is not responding; iDRAC is unreachable")
        return "pingko" # Return immediately
    else:
        # Check for a failed response
        if result.returncode != 0:
            print("IP address is not responding; iDRAC is unreachable")
            return "pingko" # Return immediately

    # Check the SNMP connection
    try:
        test_conn=asyncio.run(snmp(host, 161, user, authkey, privkey, auth_proto, priv_proto, liste_oid["servicetag"], type="get"))
    except:
        print("SNMP is not responding; iDRAC is unreachable")
        return "snmpko" # Return immediately
    else:
        # Check for an empty SNMP response
        if test_conn is None:
            print("SNMP is not responding; iDRAC is unreachable")
            return "snmpko" # Return immediately


def get_info(host, user, authkey, privkey, port=161, auth_proto=usmHMACSHAAuthProtocol, priv_proto=usmAesCfb128Protocol):
    "Retrieve server hardware information through SNMP."
    dst, subj = "", "SERVER STATUS OK"
    index_alims = []
    index_disques = []
    #########
    # SERVER #
    #########
    for nom, oid in liste_oid.items():
        res = asyncio.run(snmp(host, port, user, authkey, privkey, auth_proto, priv_proto, oid, type="get"))
        #snmp_walk(host, port, user, authkey, privkey, auth_proto, priv_proto, oid)
        for element in res:
            valeur = str(res).replace(' = ','=').split('=')[1]
            #oid = str(res).replace(' = ','=').split('=')[0]
            if nom == "servicetag":
                ObjDisques = Disques() # Create the disk object
                ObjAlims = Alimentations() # Create the power supply object
                ObjServeur = Serveur(ObjDisques, ObjAlims, servicetag=valeur) # Create the server object
            else:
                setattr(ObjServeur, nom, valeur)


    #########
    # DISKS  #
    #########
    res = asyncio.run(snmp(host, port, user, authkey, privkey, auth_proto, priv_proto, liste_oid_disques["emplacement_disque"]))
    for disque_id in res:
        emplacement_disque = str(disque_id).replace(' = ','=').split('=')[1] # Retrieve the disk identifier for each entry
        index_disques.append(emplacement_disque)
        for i in "nom_disque", "status_disques", "status_smart", "modele_disques", "sn_disques":
            res = asyncio.run(snmp(host, port, user, authkey, privkey, auth_proto, priv_proto, liste_oid_disques[i] + "." + emplacement_disque, type="get"))
            setattr(ObjServeur.disques, i + "_" + emplacement_disque, str(res).replace(' = ','=').split('=')[1])

     
    messages = {
        6: "WARNING",
        5: "ERROR",
        4: "WARNING",
        3: "OK",
        2: "WARNING",
        1: "UNKNOWN"
    }
    smart = {
        0: "OK",
        1: "ERROR"
    }
    batterie = {
        1: "UNKNOWN",
        2: "OK",
        3: "ERROR"
    }
    cmos = {
        1: "OTHER",
        2: "UNKNOWN",
        3: "OK",
        5: "ERROR"
    }
    print("SnmpTDell version:", v_script)
    print("Server", ObjServeur.nom_serveur + " " + ObjServeur.modele, "health status:", messages.get(int(ObjServeur.statut_global), "UNKNOWN"))
    subj = "Server " + ObjServeur.nom_serveur + " " + ObjServeur.modele + " health status: " + messages.get(int(ObjServeur.statut_global), "UNKNOWN")
    print("* Service Tag : " + ObjServeur.servicetag.replace(" ",""))
    print("* Service Code : " + ObjServeur.servicecode.replace(" ",""))
    print("* BIOS version : " + ObjServeur.bios_version.replace(" ",""))
    print("* BIOS release date: " + ObjServeur.bios_releasedate.replace(" ",""))
    if ObjServeur.cmos_battery == "No Such Instance currently exists at this OID":
        print("* CMOS battery status: Not available")
    else:
        print("* CMOS battery status: " + cmos.get(int(ObjServeur.cmos_battery.replace(" ","")), "UNKNOWN"))
    if ObjServeur.status_battery == "No Such Instance currently exists at this OID":
        print("* Storage controller battery status: No battery detected on this server")
    else:
        print("* Storage controller battery status: " + batterie.get(int(ObjServeur.status_battery.replace(" ","")), "UNKNOWN"))


    #######
    # PSUs  #
    #######
    res = asyncio.run(snmp(host, port, user, authkey, privkey, auth_proto, priv_proto, liste_oid_alim["ps_status"], type="walk"))
    for alim_id in res:
        emplacement_alim = str(alim_id).replace(' = ','=').split('=')[1] # Retrieve the power supply status
        index_alims.append(str(alim_id).replace(' = ','=').split('=')[0][-1]) # Store the power supply identifiers
        setattr(ObjServeur.alimentations, "ps_status_" + str(alim_id).replace(' = ','=').split('=')[0][-1], emplacement_alim)

    print("* POWER SUPPLIES:")
    for alim_id in index_alims:
        print(" - Power supply " + alim_id + ": " + messages.get(int(getattr(ObjServeur.alimentations, f"ps_status_{alim_id}")), "UNKNOWN"))
        #getattr(ObjServeur.alimentations, f"ps_status_{alim_id}"))

    #########
    # DISKS  #
    #########
    print("* DISKS:")
    for disque_id in index_disques:
        print(" - Disk " + disque_id + ":", 
        getattr(ObjServeur.disques, f"nom_disque_{disque_id}") + ",",
        "SN :",
        getattr(ObjServeur.disques, f"sn_disques_{disque_id}") + ",",
        "Model:",
        getattr(ObjServeur.disques, f"modele_disques_{disque_id}") + ",",
        "SMART status:",
        #getattr(ObjServeur.disques, f"status_smart_{disque_id}") + ",",
        smart.get(int(getattr(ObjServeur.disques, f"status_smart_{disque_id}")), "UNKNOWN") + ",", \
        "Health status:", \
        messages.get(int(getattr(ObjServeur.disques, f"status_disques_{disque_id}")), "UNKNOWN"))
        #getattr(ObjServeur.disques, f"status_disques_{disque_id}"))
        getattr(ObjServeur.disques, f"nom_disque_{disque_id}") + ", " + \
        "SN: " + getattr(ObjServeur.disques, f"sn_disques_{disque_id}") + ", " + \
        "Model: " + getattr(ObjServeur.disques, f"modele_disques_{disque_id}") + ", " + \
        "SMART status: " + smart.get(int(getattr(ObjServeur.disques, f"status_smart_{disque_id}")), "UNKNOWN") + ", " + \
        "Health status: " + messages.get(int(getattr(ObjServeur.disques, f"status_disques_{disque_id}")), "UNKNOWN") + "\n"
    return subj

### READ CONFIGURATION FILE
with open('./conf.ini', 'r') as fichier:
    host = user = authkey = privkey = ""
    flag_mail = False
    for ligne in fichier:
        #print(ligne)
        if (re.search(r"\[.*\]", ligne) and flag_mail == False): # A new iDRAC IP address was found
            host = str(re.search(r"\[(.*?)\]", ligne).group(1))
            print("---------------------- " + host + " ----------------------")
            flag_mail = True
            continue # Continue to the next line
        if (re.search(r"\[.*\]", ligne) and flag_mail == True): # A new iDRAC IP address was found (multiple servers in the configuration)
            #resultat = check_host(host, user, authkey, privkey) # Check ping and SNMP connectivity
            auth_proto, priv_proto = asyncio.run(detect_snmp_protocols(host=host,user=user,authkey=authkey,privkey=privkey)) # Detect authentication and privacy protocols
            resultat = check_host(host, user, authkey, privkey, auth_proto=auth_proto, priv_proto=priv_proto)
            if resultat not in ("snmpko","pingko"):
                #corps, subj = get_info(host=host,user=user,authkey=authkey,privkey=privkey)
                subj = get_info(host=host,user=user,authkey=authkey,privkey=privkey, auth_proto=auth_proto, priv_proto=priv_proto)
                #print("BODY: " + corps + " SUBJECT: " + subj)
            if resultat == "snmpko":
                print("iDRAC unreachable")
            if resultat == "pingko":
                print("iDRAC unreachable")
            host = str(re.search(r"\[(.*?)\]", ligne).group(1))
            print("---------------------- " + host + " ----------------------")
        if re.search(r".* = .*", ligne):
            # Read the client name
            if str(ligne.replace(' = ','=').split('=')[0]) == 'client':
                client = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])
            # Read the authentication key
            if str(ligne.replace(' = ','=').split('=')[0]) == 'authkey':
                authkey = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])
            # Read the privacy (encryption) key
            if str(ligne.replace(' = ','=').split('=')[0]) == 'privkey':
                privkey = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])
            # Read the SNMP username
            if str(ligne.replace(' = ','=').split('=')[0]) == 'user':
                user = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])
            # Read the email settings
            if str(ligne.replace(' = ','=').split('=')[0]) == 'mail':
                mail = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])
            # Read the email settings
            if str(ligne.replace(' = ','=').split('=')[0]) == 'smail':
                sender = str(re.findall(r"'(.*?)'", str(ligne.replace(' = ','=').split('=')[1]))[0])

    auth_proto, priv_proto = asyncio.run(detect_snmp_protocols(host=host,user=user,authkey=authkey,privkey=privkey)) # Detect authentication and privacy protocols
    resultat = check_host(host, user, authkey, privkey, auth_proto=auth_proto, priv_proto=priv_proto) # Check ping and SNMP connectivity
    if resultat not in ("snmpko","pingko"):
        subj = get_info(host=host,user=user,authkey=authkey,privkey=privkey, auth_proto=auth_proto, priv_proto=priv_proto)
    if resultat == "snmpko":
        print("iDRAC unreachable")
    if resultat == "pingko":
        print("iDRAC unreachable")
  • Le script lit un fichier de configuration conf.ini contenant les adresses IP des interfaces iDRAC et les identifiants SNMPv3 des serveurs Dell PowerEdge à superviser. Chaque serveur possède sa propre section de configuration, ce qui permet de superviser plusieurs serveurs avec un seul script :
[192.168.1.100]
authkey = 'd0ntus31t' # Authentication passphrase
privkey = 'd0ntus31t' # Encryption passphrase
user = 'snmptdell' # SNMPv3 username

[192.168.1.101]
authkey = 'SnmpTDellR0CKS' # Authentication passphrase
privkey = 'SnmpTDellR0CKS' # Encryption passphrase
user = 'snmptdell' # SNMPv3 username

[192.168.1.102]
authkey = 'd0nuts31t' # Authentication passphrase
privkey = 'd0nuts31t' # Encryption passphrase
user = 'snmptdell' # SNMPv3 username

Exécuter le script Python SnmpTDell

Maintenant que le script SnmpTDell.py est enregistré et que le fichier de configuration conf.ini est créé, nous pouvons l’exécuter !

Windows

  • Ouvrez un terminal Windows, placez-vous dans le répertoire contenant le script SnmpTDell.py et le fichier de configuration conf.ini, puis exécutez la commande suivante :
C:\Users\user> py SnmpTDell.py

Linux

  • Activez l’environnement virtuel Python créé précédemment :
john@host:~$ source .snmptdell/bin/activate
  • Placez-vous dans le répertoire contenant le script SnmpTDell.py et le fichier de configuration conf.ini, puis lancez le script :
(.snmptdell) john@host:~$ python3 SnmpTDell.py

Résultats de la supervision

Que vous exécutiez SnmpTDell sous Windows ou sous Linux, vous devriez obtenir un résultat similaire à celui ci-dessous. Il présente l’état matériel des serveurs Dell PowerEdge, notamment les informations du BIOS, des batteries, des alimentations et des disques physiques :

---------------------- 192.168.1.100 ----------------------
SnmpTDell version: 20261110
Server SERVER01 PowerEdge R740 health status: OK
* Service Tag : DEADBEE5
* Service Code : 45142555000
* BIOS version : 2.22.1
* BIOS release date : ...
* CMOS battery status: OK
* Storage controller battery status: OK
* POWER SUPPLIES:
 - Power supply 1: OK
 - Power supply 2: OK
* DISKS:
 - Disk 1: ..., SN: ..., Model: ..., SMART status: OK, Health status: OK

---------------------- 192.168.1.101 ----------------------
IP address is not responding; iDRAC is unreachable
iDRAC unreachable

---------------------- 192.168.1.102 ----------------------
[…]

💡 Remarque : les informations affichées peuvent varier selon le modèle Dell PowerEdge, la version du micrologiciel iDRAC et les composants matériels installés. Certains OID SNMP peuvent ne pas être disponibles sur tous les serveurs.

Créer un exécutable autonome avec PyInstaller

Comme nous l’avons vu, l’exécution du script Python SnmpTDell nécessite l’installation de Python et de bibliothèques supplémentaires, telles que PySNMP et cryptography. Pour éviter d’installer ces dépendances sur chaque machine devant exécuter le script, nous pouvons utiliser PyInstaller afin de les regrouper dans un exécutable autonome.

Notre outil de supervision devient ainsi plus facile à distribuer et à exécuter sur d’autres machines, sans installation séparée de Python. Nous allons voir comment créer des exécutables SnmpTDell pour Windows et pour Linux.

💡 Remarque : PyInstaller doit être installé et exécuté dans le même environnement Python que le script et ses dépendances (PySNMP et cryptography). Par ailleurs, PyInstaller n’est pas un compilateur croisé : vous devez générer l’exécutable Windows sous Windows et l’exécutable Linux sous Linux.

Commencez par télécharger le magnifique fichier .ico : SnmpTDell.ico, qui servira d’icône à l’application sous Windows.

Windows

  • Ouvrez un terminal Windows, placez-vous dans le répertoire contenant le script SnmpTDell.py et l’icône SnmpTDell.ico, puis installez PyInstaller :
C:\Users\user> py -m pip install pyinstaller
  • Créez l’exécutable autonome SnmpTDell.exe avec l’option --onefile pour tout regrouper dans un seul fichier et l’option --icon pour ajouter notre magnifique icône :
C:\Users\user> py -m PyInstaller --onefile --icon SnmpTDell.ico SnmpTDell.py
  • Une fois la génération terminée, vous devriez retrouver l’exécutable SnmpTDell.exe dans le répertoire dist :
Exécutable Windows SnmpTDell généré avec PyInstaller dans le répertoire dist
SnmpTDell.exe généré par PyInstaller, prêt à superviser les serveurs Dell PowerEdge en SNMPv3.

Copiez le fichier de configuration conf.ini dans le même répertoire que SnmpTDell.exe, puis ouvrez un terminal dans ce répertoire et lancez l’exécutable :

C:\Users\user\dist> SnmpTDell.exe

Linux

  • Installez d’abord les paquets nécessaires à la création de l’exécutable SnmpTDell pour Linux. Les commandes suivantes sont prévues pour Debian 13 :
root@host:~# apt update && apt install binutils python3-dev python3-venv
  • Activez l’environnement virtuel Python créé précédemment :
john@host:~$ source .snmptdell/bin/activate
  • Installez PyInstaller dans l’environnement virtuel :
(.snmptdell) john@host:~$ python3 -m pip install pyinstaller
  • Placez-vous dans le répertoire contenant le script SnmpTDell.py, puis créez un exécutable autonome avec l’option --onefile :
(.snmptdell) john@host:~$ python3 -m PyInstaller --onefile SnmpTDell.py
  • Une fois la génération terminée, vous devriez retrouver l’exécutable SnmpTDell dans le répertoire dist. Copiez le fichier de configuration conf.ini dans ce répertoire, puis lancez l’exécutable :
(.snmptdell) john@host:~$ cp conf.ini dist/
(.snmptdell) john@host:~$ cd dist
(.snmptdell) john@host:~/dist$ ./SnmpTDell

Conclusion

Nous avons vu comment configurer les interfaces Dell iDRAC pour accepter les requêtes SNMPv3 et comment utiliser le script Python SnmpTDell pour superviser l’état matériel de nos serveurs Dell PowerEdge depuis une machine Windows ou Linux.

Cette solution permet de superviser plusieurs serveurs et de contrôler leur état matériel, notamment celui des disques physiques, des alimentations et des batteries, sans installer Dell OpenManage ni aucun agent supplémentaire sur les serveurs supervisés. Nous pouvons également créer un exécutable autonome avec PyInstaller pour faciliter le déploiement de l’outil sur d’autres machines.