SNMP (Perl for System Administration) Book Home Perl for System AdministrationSearch this book

10.3. SNMP

Let's move away from security and towards more general monitoring topics. In the previous section we looked at a method for monitoring a specific network service. The Simple Network Management Protocol (SNMP) takes a quantum leap forward by offering a general way to remotely monitor and configure network devices and networked computers. Once you master the basics of SNMP, you can use it to keep tabs on (and often configure) practically every device on your network.

Truth be told, the Simple Network Management Protocol isn't particularly simple. There's a respectable learning curve associated with this subject. If you aren't already familiar with SNMP, see Appendix E, "The Twenty-Minute SNMP Tutorial", for a tutorial on it.

10.3.1. Using SNMP from Perl

One way we could use SNMP from Perl is to call command-line programs like the UCD-SNMP ones used for demonstration purposes in Appendix E, "The Twenty-Minute SNMP Tutorial". It would be a straightforward process, no different any of the examples of calling external programs we've seen earlier in this book. Since there's nothing new to learn there, we won't spend any time with this technique. I will offer one caveat: if you are using SNMPv1 or SNMPv2C, chances are you'll have to put the community name on the command line. If this program runs on a multiuser box, anyone who can list the process table may be able to see this community name and steal the keys to the kingdom. This threat is present in our command-line examples in Appendix E, "The Twenty-Minute SNMP Tutorial", but it becomes more acute with automated programs that repeatedly make external program calls like this. For demonstration purposes only, the following examples also take the target hostname and community name string on the command line. You should change that for production code.

If we don't call an external program to perform SNMP operations from Perl, our other choice is to use a Perl SNMP module. There are at least three separate but similar modules available: Net::SNMP by David M. Town, SNMP_Session.pm written by Simon Leinen, and the "SNMP Extension Module v3.1.0 for the UCD SNMPv3 Library" (which we'll call just call SNMP because of the way it is loaded) by G.S. Marzot. All of these modules implement SNMPv1. Net::SNMP and SNMP offer some SNMPv2 support. Only SNMP offers any SNMPv3 support.

The most significant difference between these three modules besides their level of SNMP support is their reliance on libraries external to the core Perl distribution. The first two (Net::SNMP and SNMP_Session.pm) are implemented in Perl alone, while SNMP needs to be linked against a separate pre-built UCD-SNMP library. The main drawback to using SNMP is this added dependency and build step (presuming you can build the UCD-SNMP library on your platform).

The plus side of depending on the UCD-SNMP library is the extra power it provides to the module. For instance, SNMP can parse Management Information Base (MIB) description files and print raw SNMP packet dumps for debugging, two functions the other modules do not provide. There are other modules that can help reduce this disparity in functionality (for instance, SNMP::MIB::Compiler by Fabien Tassin can handle MIB parsing tasks), but if you are looking for one module to do the whole job, SNMP is your best bet.

Let's start with a small Perl example. If we need to know the number of interfaces a particular device has, we could query the interfaces.ifNumber variable. Using Net::SNMP, it is this easy:

use Net::SNMP;

# requires a hostname and a community string as its arguments
($session,$error) = Net::SNMP->session(Hostname => $ARGV[0],
                                       Community => $ARGV[1]);

die "session error: $error" unless ($session);

# iso.org.dod.internet.mgmt.mib-2.interfaces.ifNumber.0 = 
#   1.3.6.1.2.1.2.1.0
$result = $session->get_request("1.3.6.1.2.1.2.1.0");

die "request error: ".$session->error unless (defined $result);

$session->close;

print "Number of interfaces: ".$result->{"1.3.6.1.2.1.2.1.0"}."\n";

When pointed at a workstation with an Ethernet and a loopback interface, it will print Numberofinterfaces:2; a laptop with Ethernet, loopback, and PPP interfaces returns Number of interfaces: 3; and a small router returns Number of interfaces: 7.

One key thing to notice is the use of Object Identifiers (OIDs) instead of variable names. Both Net::SNMP and SNMP_Session.pm handle SNMP protocol interactions only. They make no pretense of handling the peripheral SNMP-related tasks like parsing SNMP MIB descriptions. For this functionality you will have to look to other modules such as SNMP::MIB::Compiler or SNMP_util.pm by Mike Mitchell for use with SNMP_Session.pm (not to be confused with SNMP::Util by Wayne Marquette, for use with the SNMP module).

If you want to use textual identifiers instead of numeric OIDs without coding in the mapping yourself or using an additional module, your only choice is to use the SNMP module, which has a built-in MIB parser. Let's do a table walk of the Address Resolution Protocol (ARP) table of a machine using this module:

use SNMP;

# requires a hostname and a community string as its arguments
$session = new SNMP::Session(DestHost => $ARGV[0], Community => $ARGV[1],
                             UseSprintValue => 1);

die "session creation error: $SNMP::Session::ErrorStr" unless 
  (defined $session);

# set up the data structure for the getnext command
$vars = new SNMP::VarList(['ipNetToMediaNetAddress'],
                          ['ipNetToMediaPhysAddress']);

# get first row
($ip,$mac) = $session->getnext($vars);
die $session->{ErrorStr} if ($session->{ErrorStr});

# and all subsequent rows
while (!$session->{ErrorStr} and 
       $$vars[0]->tag eq "ipNetToMediaNetAddress"){
    print "$ip -> $mac\n";
    ($ip,$mac) = $session->getnext($vars);
};

Here's an example of the output this produces:

192.168.1.70 -> 8:0:20:21:40:51
192.168.1.74 -> 8:0:20:76:7c:85
192.168.1.98 -> 0:c0:95:e0:5c:1c

This code looks similar to the previous Net::SNMP example. We'll walk through it to highlight the differences:

use SNMP;

$session = new SNMP::Session(DestHost => $ARGV[0], Community => $ARGV[1],
                             UseSprintValue => 1);

After loading the SNMP module, we create a session object just like we did in the Net::SNMP example. The additional UseSprintValue => 1 argument just tells the SNMP module to pretty-print the return values. If we didn't do this, the Ethernet addresses listed above would be printed in an encoded form.

# set up the data structure for the getnext command
$vars = new SNMP::VarList(['ipNetToMediaNetAddress'],
                          ['ipNetToMediaPhysAddress']);

SNMP is willing to use simple strings like sysDescr.0 with its commands, but it prefers to use a special object it calls a "Varbind." It uses these objects to store return values from queries. For example, the code we're looking at calls the getnext( ) method to send a get-next-request, just like in the IP route table example in Appendix E, "The Twenty-Minute SNMP Tutorial". Except this time, SNMP will store the returned indices in a Varbind for us so we don't have to keep track of them by hand. With this module, you can just hand the Varbind back to the getnext method each time you want the next value.

Varbinds are simply anonymous Perl arrays with four elements: obj, iid , val, and type. For our purposes, we only need to worry about obj and iid. The first element, obj, is the object you are querying. obj can be specified in one of several formats. In this case, we are using a leaf identifier format, i.e., specifying the leaf of the tree we are concerned with. IpNetToMediaNetAddress is the leaf of the tree:

.iso.org.dod.internet.mgmt.mib-2.ip.ipNetToMediaTable.ipNetToMediaEntry.ipNetToMediaNetAddress

The second element in a Varbind is the iid, or instance identifier. In our previous discussions, we've always used a 0 here (e.g., system.sysDescr.0), because we've only seen objects that have a single instance. Shortly we'll see examples where the iid can be something other than 0. For instance, later on we'll want to refer to a particular network interface on a multi-interface Ethernet switch. obj and iid are the only two parts of a Varbind you need to specify for a get. getnext does not need an iid since it will return the next instance by default.

The line of code above uses VarList( ), which creates a list of two Varbinds, each with just the obj element filled in. We feed this list to the getnext( ) method:

# get first row
($ip,$mac) = $session->getnext($vars);
die $session->{ErrorStr} if ($session->{ErrorStr});

getnext( ) returns the values it received back from our request and updates the Varbind data structures accordingly. Now it is just a matter of calling getnext( ) until we fall off the end of the table:

while (!$session->{ErrorStr} and 
       $$vars[0]->tag eq "ipNetToMediaNetAddress"){
        print "$ip -> $mac\n";
       ($ip,$mac) = $session->getnext($vars);
};

For our final SNMP example, let's return to the world of security. We'll pick a task that would be tricky, or at least annoying, to do well with the command-line SNMP utilities.

Here's the scenario: you've been asked to track down a misbehaving user on your switched Ethernet network. The only info you have is the Ethernet address of the machine that user is on. It's not an Ethernet address you have on file (which could be kept in our host database from Chapter 5, "TCP/IP Name Services" if we extended it), and you can't sniff your switched net, so you are going to have to be a little bit clever about tracking this machine down. Your best bet in this case may be to ask one or all of your Ethernet switches if they've seen that address on one of their ports.

Just to make this example more concrete so we can point at specific MIB variables, we'll say that your network consists of several Cisco Catalyst 5500 switches. The basic methodology we're going to use to solve this problem will apply to other products and other vendors as well. Any switch or vendor-specific information will be noted as we go along. Let's walk through this problem step by step.

As before, first we have to go search through the correct MIB module files. With a little jumpstart from Cisco's tech support, we realize we'll need to access four separate objects:

Why four different tables? Each table has a piece to contribute to the answer, but no one table has the specific information we seek. The first table provides us with a list of the VLANS (Virtual Local Area Networks), or virtual "network segments," on the switch. Cisco has chosen to keep separate tables for each VLAN on a switch, so we will need to query for information one VLAN at a time. More on this in a moment.

The second table provides us with a list of Ethernet addresses and the number of the switch's bridge port on which each address was last seen. Unfortunately, a bridge port number is an internal reckoning for the switch; it does not correspond to the name of a physical port on that switch. We need to know the physical port name, i.e., from which card and port the machine with that Ethernet address last spoke, so we have to dig further.

There is no table that maps bridge port to physical port name (that would be too easy), but the dot1dBasePortTable can provide a bridge port to interface number mapping. Once we have the interface number, we can look it up in ifXTable and retrieve the port name.

Figure 10-1 shows a picture of the four-layer deference necessary to perform our desired task.

figure

Figure 10.1. The set of SNMP queries needed to find the port name on a Cisco 5000

Here's the code to put these four tables together to dump the information we need:

use SNMP;

# These are the extra MIB module files we need, found in the same 
# directory as this script
$ENV{'MIBFILES'}=
  "CISCO-SMI.my:FDDI-SMT73-MIB.my:CISCO-STACK-MIB.my:BRIDGE-MIB.my";

# Connect and get the list of VLANs on this switch
$session = new SNMP::Session(DestHost => $ARGV[0], 
                             Community => $ARGV[1]);
die "session creation error: $SNMP::Session::ErrorStr" unless 
  (defined $session);

# enterprises.cisco.workgroup.ciscoStackMIB.vlanGrp.vlanTable.vlanEntry 
# in CISCO-STACK-MIB
$vars = new SNMP::VarList(['vlanIndex']);
                          
$vlan = $session->getnext($vars);
die $session->{ErrorStr} if ($session->{ErrorStr});

while (!$session->{ErrorStr} and $$vars[0]->tag eq "vlanIndex"){

    # VLANS 1000 and over are not "real" ON A CISCO CATALYST 5XXX
    # (this limit is likely to be different on different switches)
    push(@vlans,$vlan) if $vlan < 1000;

    $vlan = $session->getnext($vars);
};

undef $session,$vars;

# for each VLAN, query for the bridge port, the interface number 
# associated with that port, and then the interface name for that 
# port number
foreach $vlan (@vlans){
    # note our use of "community string indexing" as part 
    # of the session setup
    $session = new SNMP::Session(DestHost => $ARGV[0], 
                                 Community => $ARGV[1]."@".$vlan,
                                 UseSprintValue => 1);

    die "session creation error: $SNMP::Session::ErrorStr" 
      unless (defined $session);
  
    # from transparent forwarding port table at 
    # dot1dBridge.dot1dTp.dot1dTpFdbTable.dot1dTpFdbEntry 
    # in RFC1493 BRIDGE-MIB
    $vars = new SNMP::VarList(['dot1dTpFdbAddress'],['dot1dTpFdbPort']);

    ($macaddr,$portnum) = $session->getnext($vars);
    die $session->{ErrorStr} if ($session->{ErrorStr});

    while (!$session->{ErrorStr} and 
           $$vars[0]->tag eq "dot1dTpFdbAddress"){

        # dot1dBridge.dot1dBase.dot1dBasePortTable.dot1dBasePortEntry
        # in RFC1493 BRIDGE-MIB
        $ifnum = 
          (exists $ifnum{$portnum}) ? $ifnum{$portnum} :
            ($ifnum{$portnum} = 
               $session->get("dot1dBasePortIfIndex\.$portnum"));

        # from ifMIB.ifMIBObjects.ifXTable.ifXEntry in RFC1573 IF-MIB
        $portname = 
          (exists $portname{$ifnum}) ? $portname{$ifnum} :
            ($portname{$ifnum}=$session->get("ifName\.$ifnum"));            

        print "$macaddr on VLAN $vlan at $portname\n";

        ($macaddr,$portnum) = $session->getnext($vars);
    };

    undef $session, $vars, %ifnum, %portname;
}

If you've read Appendix E, "The Twenty-Minute SNMP Tutorial", most of this code will look familiar. Here are some comments on the new stuff:

$ENV{'MIBFILES'}=
  "CISCO-SMI.my:FDDI-SMT73-MIB.my:CISCO-STACK-MIB.my:BRIDGE-MIB.my";

This code sets the MIBFILES environment variable for the UCD-SNMP package library. When set, this variable instructs the library to parse the listed set of additional files for MIB object definitions. The only strange MIB module file in that list is FDDI-SMT73-MIB.my. This is included because CISCO-STACK-MIB.my has the following statement at the top to include certain definitions from other MIB entries:

IMPORTS
        MODULE-IDENTITY, OBJECT-TYPE, Integer32, IpAddress, TimeTicks,
        Counter32, Counter64, NOTIFICATION-TYPE
                FROM SNMPv2-SMI
        DisplayString, RowStatus
                FROM SNMPv2-TC
        fddimibPORTSMTIndex, fddimibPORTIndex
                FROM FDDI-SMT73-MIB
        OwnerString
                FROM IF-MIB
        MODULE-COMPLIANCE, OBJECT-GROUP
                FROM SNMPv2-CONF
        workgroup
                FROM CISCO-SMI;

Even though we don't reference any objects that use fddimibPORTSMTIndex or fddimibPORTIndex, we still (by choice) include that file in the file list to keep the MIB parser from complaining. All of the other MIB definitions in this IMPORTS statement are included either in our parse list or the library's default list. You often need to look for the IMPORTS section of a MIB module to see that module's dependencies when going MIB groveling.

Moving on in our code, here's another strange statement:

$session = new SNMP::Session(DestHost => $ARGV[0], 
                             Community => $ARGV[1]."@".$vlan,
                             UseSprintValue => 1);

Instead of just passing on the community name as provided by the user, we're appending something of the form @VLAN-NUMBER. In Cisco parlance, this is "community string indexing." When dealing with VLANs and bridging, Cisco devices keep track of several "instances" of the MIB, one for each VLAN. Our code makes the same queries once per each VLAN found on the switch:

$ifnum =  
         (exists $ifnum{$portnum}) ? $ifnum{$portnum} :
            ($ifnum{$portnum} = 
                $session->get("dot1dBasePortIfIndex\.$portnum"));

Two comments on this piece of code. First, for variety's sake, we're using a simple string argument to get( ). We could easily have used something more Varbind-ish:

($ifnum{$portnum}=$session->get(['dot1dBasePortIfIndex',$portnum]));

Second, note that we're doing some very simple caching here. Before we actually perform a get( ), we look in a simple hash table (%ifnum) to see if we've already made this query. If we haven't, we make the query and populate the hash table with the result. At the end of each VLAN pass, we delete the cache hash (undef%ifnum) to prevent previous VLAN information from providing false information.

This is a good technique to remember when programming SNMP code. It is important to query as little and as seldom as possible if you want to be kind to your network and network devices. A device may have to take horsepower away from its usual tasks to respond to your slew of queries if you are not prudent.

Here's an excerpt from our code in action:

"00 10 1F 2D F8 FB " on VLAN 1 at 1/1
"00 10 1F 2D F8 FD " on VLAN 1 at 1/1
"08 00 36 8B A9 03 " on VLAN 115 at 2/18
"08 00 36 BA 16 03 " on VLAN 115 at 2/3
"08 00 36 D1 CB 03 " on VLAN 115 at 2/15

It's not hard to see how this program could be enhanced. Besides prettier or more orderly output, it could save state between runs. Each time it ran, the program could let you know how things have changed: new addresses appearing, ports being changed, etc. One quick caveat: most switches are of the "learning" variety, so they will age out entries for addresses that they haven't heard from in a while. This just means that your program will need to run at least as often as the standard port aging time.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.