rss logo

How to Monitor Dell PowerEdge Servers Using Python and SNMPv3

SnmpTDell Python script for Dell PowerEdge server monitoring using iDRAC and SNMPv3

If, like me, you have to manage a dozen million (I might be exaggerating just a tiny bit here) Dell PowerEdge servers, you probably want to make sure they're all in good health. Indeed, the more servers you manage, the more likely you are to encounter hardware failures, and the harder it becomes to monitor everything manually. That's why I've developed my own monitoring script (without the help of AI! The quality of the code should be proof enough... 😅).

Historically, I used check_openmanage, a Perl-based Nagios plugin developed by the University of Oslo. Unfortunately, it hasn't been updated in quite some time, and its original website seems to have disappeared from the Internet. Moreover, it required Dell OpenManage to be installed, which could be quite a headache on Linux distributions, especially those not officially supported by Dell.

Since I couldn't find a suitable replacement, I decided to develop my own solution: a Python script that sends SNMP requests directly to the servers' iDRAC interfaces to monitor their hardware health. To achieve this, I used PySNMP, a cross-platform, pure-Python SNMP engine implementation. This means there's no need to install Dell OpenManage or any additional monitoring agent on the servers' operating systems. All you need is a properly configured iDRAC interface with SNMP enabled.

My main requirements when developing this script were to use SNMPv3 exclusively and to make it run on both Windows and Linux, without requiring Dell OpenManage or any additional agent on the monitored servers.

Monitoring Architecture

The architecture is pretty straightforward: the SnmpTDell Python script runs on a Windows or Linux machine that can reach the iDRAC interfaces of the Dell PowerEdge servers we want to monitor.

The script communicates directly with each iDRAC interface using SNMPv3 over UDP port 161 to retrieve hardware health information. Since the monitoring is performed through iDRAC, there's no need to install any additional agent on the monitored servers.

Dell PowerEdge monitoring architecture showing a Python script communicating with three iDRAC interfaces using SNMPv3 over UDP port 161
Dell PowerEdge hardware monitoring architecture using SnmpTDell, Python, iDRAC and SNMPv3.

Install Python on Windows or Linux

As mentioned in the introduction, the SnmpTDell Python script can run on either Windows or Linux machines. While Python is already installed by default on many Linux distributions, you may need to install it manually. On Windows, you'll generally need to install Python before running the script.

Windows

To install Python on Windows, visit the official Python download page and download the latest Python Install Manager. Run the installer, then use it to install Python.

Official Python download page showing the Python Install Manager for Windows
Download the Python Install Manager from the official Python website.
  • Once Python is installed, open a Windows terminal and install the required pysnmp and cryptography packages:
C:\Users\user> py -m pip install pysnmp cryptography

Install Python on Debian

The following procedure is based on Debian 13, but it should also work on most Linux distributions with a few adjustments.

  • Install the python3-venv package to enable the creation of Python virtual environments:
root@host:~# apt update && apt install python3-venv
  • As a regular user, create a dedicated Python virtual environment for the SnmpTDell script:
john@host:~$ python3 -m venv .snmptdell
  • Activate the Python virtual environment:
john@host:~$ source .snmptdell/bin/activate
  • Install the required pysnmp and cryptography packages inside the virtual environment:
(.snmptdell) john@host:~$ python3 -m pip install pysnmp cryptography

Configure SNMPv3 on Dell iDRAC

We now need to configure SNMPv3 on the Dell iDRAC interface. To do so, we'll create a dedicated user account that will be used by our Python monitoring script to retrieve hardware health information.

For security reasons, I recommend using a dedicated account with only the minimum required privileges. This way, if the credentials are compromised, the potential impact will be limited. Since our script only retrieves monitoring information, there's no need to grant administrative privileges.

  • Connect to the iDRAC web interface using HTTPS and navigate to iDRAC Settings > Users:
Dell iDRAC9 Enterprise web interface showing the iDRAC Settings menu and Users option for SNMPv3 account configuration
Dell iDRAC9 Enterprise: navigate to iDRAC Settings > Users to configure a dedicated SNMPv3 monitoring account.
  • Under Local Users, click Add to create a new dedicated SNMPv3 monitoring account:
Dell iDRAC9 Enterprise Local Users settings showing the Add button to create a dedicated SNMPv3 monitoring user
Creating a new local user account in Dell iDRAC9 Enterprise for SNMPv3 monitoring.
  • Configure the dedicated SNMPv3 monitoring account with the following settings:
    • User Name: Enter a dedicated username, such as snmptdell.
    • Password: Set a strong password.
    • User Role: Select None and leave all iDRAC user privileges unchecked.
    • IPMI User Privileges: Set both LAN and Serial Port privilege levels to No Access and disable Serial Over LAN.
    • SNMP v3: Select Enabled.
    • Authentication Type: Select SHA.
    • Privacy Type: Select AES.
    • Enable Passphrase: Select Enabled and configure strong authentication and privacy passphrases.
  • Once everything is configured, click Save to create the account.
Dell iDRAC9 SNMPv3 user configuration showing a dedicated monitoring account with SHA authentication, AES encryption and restricted privileges
Configuring a dedicated Dell iDRAC9 SNMPv3 monitoring account with SHA authentication, AES encryption and restricted user privileges.

💡 Security note: The available SNMPv3 authentication and encryption algorithms depend on the iDRAC generation and firmware version. Whenever supported, prefer stronger algorithms such as SHA-256 over SHA-1. The SnmpTDell script can automatically test several SNMPv3 authentication and encryption combinations.

Python Script SnmpTDell.py

Dell iDRAC SNMP OIDs

As explained in the introduction, the SnmpTDell Python script uses SNMPv3 to retrieve hardware information from Dell iDRAC interfaces. Each piece of information is identified by an Object Identifier (OID), which allows the script to query specific hardware components and retrieve their status.

The following table lists the SNMP OIDs referenced in the script, including system information, BIOS, batteries, physical disks, and power supplies.

For more information about these OIDs and their possible values, you can refer to the official Dell iDRAC SNMP Reference Guide.

Dell iDRAC SNMP OIDs
Name Object ID (OID) Description Notes / Values
System Information
systemServiceTag 1.3.6.1.4.1.674.10892.5.1.3.2.0 Returns the Dell Service Tag of the server. Unique identifier used for Dell support and warranty identification.
systemFQDN 1.3.6.1.4.1.674.10892.5.1.3.1.0 Returns the fully qualified domain name (FQDN) of the server. Used to identify the monitored server.
systemExpressServiceCode 1.3.6.1.4.1.674.10892.5.1.3.3.0 Returns the Dell Express Service Code. Alternative identifier used by Dell support.
systemModelName 1.3.6.1.4.1.674.10892.5.1.3.12.0 Returns the server model name. Used to identify the Dell PowerEdge model.
globalSystemStatus 1.3.6.1.4.1.674.10892.5.2.1.0 Returns the overall hardware health status of the server. 1=Other, 2=Unknown, 3=OK, 4=Non-critical, 5=Critical, 6=Non-recoverable.
Battery and BIOS
batteryState 1.3.6.1.4.1.674.10892.5.5.1.20.130.15.1.4.1 Returns the current state of the storage controller battery. 1=Unknown, 2=Ready, 3=Failed, 4=Degraded, 5=Missing, 6=Charging, 7=Below threshold. Instance 1.
BIOS version 1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1 Returns the installed system BIOS firmware version. Useful for identifying the installed BIOS version.
BIOS release date 1.3.6.1.4.1.674.10892.5.4.300.50.1.7.1.1 Returns the release date of the installed system BIOS. Useful for tracking BIOS firmware versions.
CMOS battery status 1.3.6.1.4.1.674.10892.5.4.200.10.1.52.1 Returns status information associated with the CMOS battery. Used to detect potential CMOS battery issues. Availability may vary by server model.
Physical and Virtual Disks
physicalDiskProductID 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.6 Returns the model or product identifier of a physical disk. Requires a disk instance index.
physicalDiskNumber 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.1 Identifies an entry in the physical disk table. Used to identify disk instances. Does not necessarily represent the physical drive bay.
physicalDiskComponentStatus 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.24 Returns the hardware health status of a physical disk. 1=Other, 2=Unknown, 3=OK, 4=Non-critical, 5=Critical, 6=Non-recoverable.
physicalDiskSmartAlertIndication 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.31 Indicates whether a physical disk has reported a predictive failure alert. Used to detect potential disk failures before they occur.
physicalDiskSerialNo 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.7 Returns the serial number of a physical disk. Useful for identifying a specific disk when investigating hardware failures.
physicalDiskRevision 1.3.6.1.4.1.674.10892.5.5.1.20.130.4.1.8 Returns the firmware revision of a physical disk. Used to identify the installed disk firmware version.
Power Supplies
powerSupplyStatus 1.3.6.1.4.1.674.10892.5.4.600.12.1.5 Returns the hardware health status of a power supply unit (PSU). 1=Other, 2=Unknown, 3=OK, 4=Non-critical, 5=Critical, 6=Non-recoverable. Requires an instance index.

SnmpTDell.py - Python SNMPv3 Monitoring Script

Copy the following Python monitoring script into a file named SnmpTDell.py on the Windows or Linux machine that will communicate with the iDRAC interfaces of the Dell PowerEdge servers you want to monitor.

#! /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")
  • The script reads a conf.ini configuration file containing the iDRAC IP addresses and SNMPv3 credentials of the Dell PowerEdge servers we want to monitor. Each server has its own configuration section, allowing us to monitor multiple servers with a single 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

Run the SnmpTDell Python Script

Now that we have saved the SnmpTDell.py script and created the conf.ini configuration file, we're ready to run it!

Windows

  • Open a Windows terminal, navigate to the directory containing the SnmpTDell.py script and the conf.ini configuration file, then execute the following command:
C:\Users\user> py SnmpTDell.py

Linux

  • Activate the Python virtual environment created earlier:
john@host:~$ source .snmptdell/bin/activate
  • Navigate to the directory containing the SnmpTDell.py script and the conf.ini configuration file, then run the script:
(.snmptdell) john@host:~$ python3 SnmpTDell.py

Monitoring Results

Whether you're running SnmpTDell on Windows or Linux, you should see an output similar to the following, displaying the Dell PowerEdge server hardware health status, including BIOS information, batteries, power supplies, and physical disks:

---------------------- 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 ----------------------
[…]

💡 Note: The information displayed may vary depending on the Dell PowerEdge model, iDRAC firmware version, and installed hardware components. Some SNMP OIDs may not be available on all servers.

Create a Standalone Executable with PyInstaller

As we've seen, running the SnmpTDell Python script requires installing Python and some additional libraries, such as PySNMP and cryptography. To avoid having to install these dependencies on every machine where we want to run the script, we can use PyInstaller to package everything into a standalone executable.

This makes our monitoring tool easier to distribute and run on other machines without requiring a separate Python installation. We'll see how to create executable versions of SnmpTDell for both Windows and Linux.

💡 Note: PyInstaller should be installed and executed in the same Python environment where the script and its dependencies (PySNMP and cryptography) are already installed. Also, PyInstaller is not a cross-compiler: you must build the Windows executable on Windows and the Linux executable on Linux.

First, download the magnificent .ico file: SnmpTDell.ico, which we'll use as the application icon on Windows.

Windows

  • Open a Windows terminal, navigate to the directory containing the SnmpTDell.py script and the SnmpTDell.ico icon, then install PyInstaller:
C:\Users\user> py -m pip install pyinstaller
  • Create the standalone SnmpTDell.exe executable using the --onefile option to package everything into a single file and --icon to add our magnificent application icon:
C:\Users\user> py -m PyInstaller --onefile --icon SnmpTDell.ico SnmpTDell.py
  • Once the build is complete, you should find the SnmpTDell.exe executable in the dist directory:
SnmpTDell Windows executable generated with PyInstaller in the dist directory
SnmpTDell.exe generated by PyInstaller, ready to monitor Dell PowerEdge servers using SNMPv3.

Copy the conf.ini configuration file into the same directory as SnmpTDell.exe, then open a terminal in that directory and run the executable:

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

Linux

  • First, install the required packages to build the SnmpTDell Linux executable. The following commands are intended for Debian 13:
root@host:~# apt update && apt install binutils python3-dev python3-venv
  • Activate the Python virtual environment created earlier:
john@host:~$ source .snmptdell/bin/activate
  • Install PyInstaller inside the virtual environment:
(.snmptdell) john@host:~$ python3 -m pip install pyinstaller
  • Navigate to the directory containing the SnmpTDell.py script and create a standalone executable using the --onefile option:
(.snmptdell) john@host:~$ python3 -m PyInstaller --onefile SnmpTDell.py
  • Once the build is complete, you should find the SnmpTDell executable in the dist directory. Copy the conf.ini configuration file into this directory, then run the executable:
(.snmptdell) john@host:~$ cp conf.ini dist/
(.snmptdell) john@host:~$ cd dist
(.snmptdell) john@host:~/dist$ ./SnmpTDell

Conclusion

We've seen how to configure Dell iDRAC interfaces to accept SNMPv3 requests and how to use the SnmpTDell Python script to monitor the hardware health of our Dell PowerEdge servers from either a Windows or Linux machine.

With this solution, we can monitor multiple servers, check their hardware health, including physical disks, power supplies, and batteries, without installing Dell OpenManage or any additional agent on the monitored servers. We can also package the script into a standalone executable using PyInstaller, making it easier to deploy on other machines.