Python Tutorial: Get Temperature and other environmental data from a Dell PowerEdge Server using pysnmp

Python Tutorial: Get Temperature and other environmental data from a Dell PowerEdge Server using pysnmp

This python tutorial will teach you how to query the Dell PowerEdge temperature sensors via SNMP. Once OpenManage software is installed on a Dell server, a ton of information is made available via SNMP including…

  • Chassis Temperature
  • Fan(s) Speed
  • Chassis Intrusion Switch
  • Hard Drive Health
  • Power Supply Status, Voltages, and Consumption

Prerequisites

1.) Install the pysnmp package
C:\> pip install --upgrade pysnmp
Collecting pysnmp
...
Installing collected packages: pysmi, pysnmp
Successfully installed pysmi-0.0.6 pysnmp-4.3.1

2.) A Dell PowerEdge Server running Windows Server 2008 or newer with Dell Open Manage Installed.

3.) You will also need to install and configure SNMP support on the target Windows Server machine.

If you search for pysnmp example code you will find two type, Synchronous, and Asynchronous. The following code is an example of a synchronous SNMP Get found in the old pysnmp manual

http://pysnmp.sourceforge.net/examples/current/v3arch/oneliner/manager/cmdgen/get-v2c.html

from pysnmp.entity.rfc3413.oneliner import cmdgen
import time

SNMP_HOST = '192.168.1.60'
SNMP_PORT = 161
SNMP_COMMUNITY = 'public'


cmdGen = cmdgen.CommandGenerator()

errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
cmdgen.CommunityData(SNMP_COMMUNITY),
cmdgen.UdpTransportTarget((SNMP_HOST, SNMP_PORT)),
'1.3.6.1.4.1.674.10892.1.300.10.1.8.1',
'1.3.6.1.4.1.674.10892.1.300.10.1.9.1'
)

# Check for errors and print out results
if errorIndication:
  print(errorIndication)
else:
  if errorStatus:
    print('%s at %s' % (
      errorStatus.prettyPrint(),
      errorIndex and varBinds[int(errorIndex)-1] or '?'
      )
    )
  else:
    for name, val in varBinds:
      print('%s = %s' % (name.prettyPrint(), val.prettyPrint()))

SNMPv2-SMI::enterprises.674.10892.1.300.10.1.8.1 = b'Dell Inc.'
SNMPv2-SMI::enterprises.674.10892.1.300.10.1.9.1 = b'PowerEdge R720'

Throughout the rest of this tutorial, we will stick with the Synchronous type code. We are only fetching a few data points and there is no need to add the complexity of performing Asynchronous operations.

Next, we will create a snmpget() function that will encapsulate the above code and make it reusable.

def snmpget(oid):

  from pysnmp.entity.rfc3413.oneliner import cmdgen

  global SNMP_HOST
  global SNMP_PORT
  global SNMP_COMMUNITY

  cmdGen = cmdgen.CommandGenerator()

  errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
    cmdgen.CommunityData(SNMP_COMMUNITY),
    cmdgen.UdpTransportTarget((SNMP_HOST, SNMP_PORT)),
    oid
  )

  # Check for errors and print out results
  if errorIndication:
    print(errorIndication)
  else:
    if errorStatus:
      print('%s at %s' % (
        errorStatus.prettyPrint(),
        errorIndex and varBinds[int(errorIndex)-1] or '?'
       )
     )
    else:
      for name, val in varBinds:
        #print('%s = %s' % (name.prettyPrint(), val.prettyPrint()))
        return val
 

With everything now contained inside the snmpget() function we can call the function for each OID we want to query.

answer = snmpget('1.3.6.1.4.1.674.10892.1.300.10.1.8.1')
print(answer)

Dell Inc.
Now that we are getting the data, we will apply some formatting.

print( 'Make: ' + snmpget('1.3.6.1.4.1.674.10892.1.300.10.1.8.1') )
print( 'Model: ' + snmpget('1.3.6.1.4.1.674.10892.1.300.10.1.9.1') )


Make: Dell Inc.
Model: PowerEdge R720

Next, lets fetch some data about the power supply amperage and total watts consumed. You may get different results if your server does not have redundant(2) power supplies.

print( snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1') + ': ' +
      str(snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.6.1.1')) + ' AMPS')
print( snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.8.1.2') + ': ' +
      str(snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.6.1.2')) + ' AMPS')
print( snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3') + ': ' +
      str(snmpget('1.3.6.1.4.1.674.10892.1.600.30.1.6.1.3')) + ' Watts')


PS1 Current 1: 16 AMPS
PS2 Current 2: 0 AMPS
System Board Pwr Consumption: 196 Watts

So far, we have called snmpget() individually to get data. Lets modify snmpget to accept a list of OIDs and return a list of return values.
We will progressively modify our snmpget() function to accomplish this. We add two additional arguments to the function oid2, oid3. While this is not ideal the getCmd() wants each addition oid as an additional command line argument.
Lets focus on the return values. Instead of iterating over varBinds we will extract the values and return a simple list or singular value if the list’s length is 1.

def snmpget(oid, oid2='', oid3=''):
    
    from pysnmp.entity.rfc3413.oneliner import cmdgen
    
    global SNMP_HOST
    global SNMP_PORT
    global SNMP_COMMUNITY
    
    cmdGen = cmdgen.CommandGenerator()

    errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
        cmdgen.CommunityData(SNMP_COMMUNITY),
        cmdgen.UdpTransportTarget((SNMP_HOST, SNMP_PORT)),
        oid,
        oid2,
        oid3
    )
    
    # Predefine our results list  
    results = []
    
    # Check for errors and print out results
    if errorIndication:
        print(errorIndication)
    else:
        if errorStatus:
            print('%s at %s' % (
                errorStatus.prettyPrint(),
                errorIndex and varBinds[int(errorIndex)-1] or '?'
                )
            )
        else:
            for name, val in varBinds:
                results.append( val )
                
        if len(results) == 1:
            return results[0]
        else:
            return results
 
results = snmpget( '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.2', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3' )

for ans in results:
    print(ans)
    


PS1 Current 1
PS2 Current 2
System Board Pwr Consumption

The function now returns a list of values but our approach to accepting additional arguments is not very flexible. As you can see we get an error when we try to request anything other than 3 oids.

results = snmpget( '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3' )


---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
in ()
----> 1 results = snmpget( '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1', '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3' )


in snmpget(oid, oid2, oid3)
14 oid,
15 oid2,
---> 16 oid3
17 )
...

As you see, this code raises an error.

We need to modify the function definition to accept a variable number of arguments.

The function getCmd() also uses this same technique. We can pass along these additional OIDs to the getCmd() function but we must prefix the more_oids variable with an asterisk *.

def snmpget(oid, *more_oids):
    
    from pysnmp.entity.rfc3413.oneliner import cmdgen
    
    global SNMP_HOST
    global SNMP_PORT
    global SNMP_COMMUNITY
    
    cmdGen = cmdgen.CommandGenerator()

    errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
        cmdgen.CommunityData(SNMP_COMMUNITY),
        cmdgen.UdpTransportTarget((SNMP_HOST, SNMP_PORT)),
        oid,
        *more_oids
    )

    # Predefine our results list    
    results = []
    
    # Check for errors and print out results
    if errorIndication:
        print(errorIndication)
    else:
        if errorStatus:
            print('%s at %s' % (
                errorStatus.prettyPrint(),
                errorIndex and varBinds[int(errorIndex)-1] or '?'
                )
            )
        else:
            for name, val in varBinds:
                results.append( val )
                
        if len(results) == 1:
            return results[0]
        else:
            return results
 

Now when we call snmpget() with any number of oids.

# get 6 oid values in one call.
results = snmpget( '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.2', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.1', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.2', \
                   '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.3', \
                 )

for ans in results:
    print(ans)
    
print('----------------')

# get 1 oid value
result = snmpget( '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1' )

print(result)
 


PS1 Current 1
PS2 Current 2
System Board Pwr Consumption
16
0
196
----------------
PS1 Current 1

Our snmpget() function is rather functional but we are still referencing the global variables for host,port,community defined back at the top of this tutorial.

We can make this code even more reusable by encapsulating everything into a Class.

class SNMPClient:
    # This is the SNMPClient constructor
    def __init__(self, host, port=161, community='public'):
        
        self.host = host
        self.port = port
        self.community = community

    def snmpget(self, oid, *more_oids):

        from pysnmp.entity.rfc3413.oneliner import cmdgen

        cmdGen = cmdgen.CommandGenerator()

        
        errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
            cmdgen.CommunityData(self.community),
            cmdgen.UdpTransportTarget((self.host, self.port)),
            oid,
            *more_oids
        )

        # Predefine our results list    
        results = []

        # Check for errors and print out results
        if errorIndication:
            print(errorIndication)
        else:
            if errorStatus:
                print('%s at %s' % (
                    errorStatus.prettyPrint(),
                    errorIndex and varBinds[int(errorIndex)-1] or '?'
                    )
                )
            else:
                for name, val in varBinds:
                    results.append( val )

            if len(results) == 1:
                return results[0]
            else:
                return results
 

We supply the IP, Port, and Community string to the class’s constructor.

myClient = SNMPClient('192.168.1.60', 161, 'public')

results = myClient.snmpget('1.3.6.1.4.1.674.10892.1.300.10.1.8.1', \
                           '1.3.6.1.4.1.674.10892.1.300.10.1.9.1', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.1', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.2', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.8.1.3', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.1', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.2', \
                           '1.3.6.1.4.1.674.10892.1.600.30.1.6.1.3' )

for ans in results:
    print(ans)
 


Dell Inc.
PowerEdge R720
PS1 Current 1
PS2 Current 2
System Board Pwr Consumption
16
0
196

Now that we have a working SNMP Client that we can use to query the server lets look at some of the interesting data presented by the Dell OpenManage SNMP extensions.


# Cooling Device AKA Fan Location Name
# 1.3.6.1.4.1.674.10892.1.700.12.1.8.1.1

# Cooling Fan RPMs
# 1.3.6.1.4.1.674.10892.1.700.12.1.6.1.1

# Cooling Fan Status (3 is OK)
# 1.3.6.1.4.1.674.10892.1.700.12.1.5.1.1

# Cooling Unit Status (All Fans considers (3 is OK)
# 1.3.6.1.4.1.674.10892.1.700.10.1.8.1.1

# Temp Probe Location(s)
# 1.3.6.1.4.1.674.10892.1.700.20.1.8.1.1
# 1.3.6.1.4.1.674.10892.1.700.20.1.8.1.2

# Temp Reading (Value is Celius 1/10 Degree)
# 1.3.6.1.4.1.674.10892.1.700.20.1.6.1.1
# 1.3.6.1.4.1.674.10892.1.700.20.1.6.1.2

When we query the system’s temperature probes the value is returned in Celsius at a resolution of 1/10 of a degree.

So a value of 234 is 23.4 Celsius.

To get this value we simply divide by 10.

Additionally, I want the temperature reading in Fahrenheit so we use the formula to convert Celsius to Fahrenheit.

T(F) = T(C) x 9/5 + 32

myClient = SNMPClient('192.168.1.60', 161, 'public')

results = myClient.snmpget('1.3.6.1.4.1.674.10892.1.700.20.1.8.1.1', \
                           '1.3.6.1.4.1.674.10892.1.700.20.1.6.1.1', \
                           '1.3.6.1.4.1.674.10892.1.700.20.1.8.1.2', \
                           '1.3.6.1.4.1.674.10892.1.700.20.1.6.1.2')

temp1 = results[1]
# Divide by 10
temp1_in_c = temp1 / 10
# Convert Celsius to Fahrenheit
temp1_in_f = temp1_in_c * (9/5) + 32
# Print the results
print(results[0] +": "+ str( temp1_in_f ) + 'F') 


temp2 = results[3]
# Divide by 10
temp2_in_c = temp2 / 10
# Convert Celsius to Fahrenheit
temp2_in_f = temp2_in_c * (9/5) + 32
# Print the results
print(results[2] +": "+ str( temp2_in_f ) + 'F')
 


System Board Inlet Temp: 69F
System Board Exhaust Temp: 95F

I hope you find this python example useful and educational. You are free to use the above code how you see fit.

More Python Goodness

One Reply to “Python Tutorial: Get Temperature and other environmental data from a Dell PowerEdge Server using pysnmp”

  1. Hello buddy, interesting article. I already know these stuff (the SNMP Get), however it is well written for someone who has never done it.
    Can you give any advice for SNMP Set v2 with oid (no object names)? I have gotten sick googline, searching , testing and in the end doing nothing?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.