Book HomeEssential SNMPSearch this book

11.3. OpenView's Extensible Agent

Before you start playing around with OpenView's extensible agent, make sure that you have its master agent (snmpdm) configured and running properly. You must also obtain an enterprise number, because extending the OpenView agent requires writing your own MIB definitions, and the objects you define must be part of the enterprises subtree.[57] Chapter 2, "A Closer Look at SNMP" describes how to obtain an enterprise number.

[57]Do not use my enterprise number. Obtaining your own private enterprise number is easy and free. Using my number will only confuse you and others later in the game.

MIBs are written using the SMI, of which there are two versions: SMIv1, defined in RFCs 1155 and 1212; and SMIv2, defined in RFCs 2578, 2579, and 2580. RFC 1155 notes that "ASN.1 constructs are used to define the structure, although the full generality of ASN.1 is not permitted." While OpenView's extensible agent file snmpd.extend uses ASN.1 to define objects, it requires some additional entries to create a usable object. snmpd.extend also does not support some of the SNMPv2 SMI constructs. In this chapter, we will discuss only those constructs that are supported.

By default, the configuration file for the extensible agent in the Unix version of OpenView is /etc/SnmpAgent.d/snmp.extend. To jump right in, copy the sample file to this location and then restart the agent:

$ cp /opt/OV/prg_samples/eagent/snmpd.extend /etc/SnmpAgent.d/
$ /etc/rc2.d/S98SnmpExtAgt stop
$ /etc/rc2.d/S98SnmpExtAgt start
You should see no errors and get an exit code of 0 (zero). If errors occur, check the snmpd.log file.[58] If the agent starts successfully, try walking one of the objects monitored by the extensible agent. The following command checks the status of the mail queue:

[58]On Solaris and HP-UX machines this file is located in /var/adm/snmpd.log.

$ snmpwalk sunserver1  .1.3.6.1.4.1.4242.2.2.0
4242.2.2.0 : OCTET STRING- (ascii):     Mail queue is empty
We're off to a good start. We have successfully started and polled the extensible agent.

The key to OpenView's snmpd.extend file is the DESCRIPTION. If this seems a little weird, it is! Executing commands from within the DESCRIPTION section is peculiar to this agent, not part of the SNMP design. The DESCRIPTION tells the agent where to look to read, write, and run files. You can put a whole slew of parameters within the DESCRIPTION, but we'll tackle only a few of the more common ones. Here's the syntax for the snmpd.extend file:

your-label-here DEFINITIONS ::= BEGIN

-- insert your comments here

enterprise-name  OBJECT IDENTIFIER ::= { OID-label(1) OID-label{2) 3 }
subtree-name1    OBJECT IDENTIFIER ::= { OID-label(3) 4 }
subtree-name2    OBJECT IDENTIFIER ::= { OID-label(123) 56 }

data-Identifier[59] OBJECT-TYPE
    SYNTAX Integer | Counter | Gauge | DisplayString[60] 
    ACCESS read-only | read-write
    STATUS mandatory | optional | obsolete | deprecated[61] 
    DESCRIPTION
        "
         Enter Your Description Here
         READ-COMMAND: /your/command/here passed1 passed2
         READ-COMMAND-TIMEOUT: timeout_in_seconds (defaults to 3)
         FILE-COMMAND: /your/file-command/here passed1 passed2
         FILE-COMMAND-FREQUENCY: frequency_in_seconds (defaults to 10)
         FILE-NAME: /your/filename/here
        "
    ::= { parent-subtree-name subidentifier }

END
We can glean some style guidelines from RFC 2578. While there are many guidelines, some more useful than others, one thing stands out: case does matter. Much of ASN.1 is case sensitive. All ASN.1 keywords and macros should be in uppercase: OBJECT-TYPE, SYNTAX, DESCRIPTION, etc. Your data-Identifiers (i.e., object names) should start in lowercase and contain no spaces. If you have read any of the RFC MIBs or done any polling, you should have noticed that all the object names obey this convention. Try to use descriptive names and keep your names well under the 64-character limit; RFC 2578 states that anything over 32 characters is not recommended. If you define an object under an existing subtree, you should use this subtree-name, or parent-name, before each new object-name you create. The ip subtree in mib-2 (RFC 1213) provides an example of good practice:

ip           OBJECT IDENTIFIER ::= { mib-2 4 }

ipForwarding OBJECT-TYPE
...
::= { ip 1 }

ipDefaultTTL OBJECT-TYPE
...
::= { ip 2 }
This file starts by defining the ip subtree. The names of objects within that subtree start with ip and use ip as the parent-subtree-name. As useful as this recommended practice is, there are times when it isn't appropriate. For example, this practice makes it difficult to move your objects to different parents while you are building a MIB file.

Here's a working snmpd.extend file that contains three definitions: psZombieNum, prtDiagExitC, and whosOnCall. I have placed all these objects within my own private enterprise (2789, which I have named mauro).
Figure 11-2 shows this portion of my private subtree.

Figure 11-2

Figure 11-2. mauro subtree

You can now walk the tree and see what my new objects look like; my tree starts at the OID .1.3.6.1.4.1.2789, which is equivalent to .iso.org.dod.internet.private.enterprises.mauro. I can organize my own subtree any way I want, so I've split it into two branches beneath mauro: mauro.sysInfo (2789.3) will hold information about the status of the system itself (psZombieNum and prtDiagExitC ), and mauro.other (2789.255 ) will hold additional information (whosOnCall ). If you look further down, you can see the three leaf nodes we define in this file:

SampleExt DEFINITIONS ::= BEGIN

-- comments appear here behind the dashes

internet        OBJECT IDENTIFIER ::= { iso(1) org(3) dod(6) 1 }
enterprises     OBJECT IDENTIFIER ::= { internet(1) private(4) 1 }
mauro           OBJECT IDENTIFIER ::= { enterprises(1) 2789 }

-- Now that we have defined mauro, let's define some objects

sysInfo         OBJECT IDENTIFIER ::= { mauro 3 }
other           OBJECT IDENTIFIER ::= { mauro 255 }

psZombieNum OBJECT-TYPE
  SYNTAX  INTEGER
  ACCESS  read-only
  STATUS  mandatory
  DESCRIPTION
    "Search through ps and return the number of zombies.
    READ-COMMAND: VALUE=`ps -ef | grep -v grep | grep -c  \<defunct\>`; echo $VALUE
    "
 ::= { sysInfo 0 }

prtDiagExitC OBJECT-TYPE
    SYNTAX  INTEGER
    ACCESS  read-only
    STATUS  mandatory
    DESCRIPTION
        "On Solaris, prtdiag shows us system diagnostic information. The 
         manpage states that if this command exits with a non-zero value,
         we have a problem. This is a great polling mechanism for some
         systems.
         READ-COMMAND: /usr/platform/`uname -m`/sbin/prtdiag > /dev/null; echo $?"
    ::= { sysInfo 1 }

whosOnCall OBJECT-TYPE
    SYNTAX  OctetString
    ACCESS  read-write
    STATUS  mandatory
    DESCRIPTION
        "This file contains the name of the person who will be on call
         today. The helpdesk uses this file. Only the helpdesk and
         managers should update this file. If you are sick or unable to
         be on call please contact your manager and/or the helpdesk.
         FILE-NAME: /opt/local/oncall/today.txt"
    ::= { other 0 }

END
The first two objects, psZombieNum and prtDiagExitC, both use the READ-COMMAND in the DESCRIPTION. This tells the agent to execute the named command and send any output the command produces to the NMS. By default, the program must complete within three seconds and have an exit value of 0 (zero). You can increase the timeout by adding a READ-COMMAND-TIMEOUT:

READ-COMMAND: /some/fs/somecommand.pl
READ-COMMAND-TIMEOUT: 10
This tells the agent to wait 10 seconds instead of 3 for a reply before killing the process and returning an error.

The last object, whosOnCall, uses a FILE-NAME in the DESCRIPTION. This tells the agent to return the first line of the file, program, script, etc. specified after FILE-NAME. Later we will learn how to manipulate this file.

Now that we've created a MIB file with our new definitions, we need to load the new MIB into OpenView. This step isn't strictly necessary, but it's much more convenient to work with textual names than to deal with numeric IDs. To do this, use xnmloadmib, discussed in Chapter 6, "Configuring Your NMS". After we load the MIB file containing our three new objects, we should see their names in the MIB browser and be able to poll them by name.

Once you have copied the MIB file into the appropriate directory and forced the extensible agent, extsubagt, to reread its configuration (by using kill -HUP), try walking the new objects using OpenView's snmpwalk program:

$ snmpwalk sunserver2 -c public .1.3.6.1.4.1.2789
mauro.sysInfo.psZombieNum.0 : INTEGER: 0
mauro.sysInfo.prtDiagExitC.0 : INTEGER: 2
Notice anything strange about our return values? We didn't get anything for whosOnCall. Nothing was returned for this object because we haven't created the oncall.txt file whose contents we're trying to read. We must first create this file and insert some data into the file. There are two ways of doing this. Obviously, you can create the file with your favorite text editor. But the clever way is to use snmpset:

$ snmpset -c private sunserver2 \
.1.3.6.1.4.1.2789.255.0.0 octetstring "david jones"
mauro.Other.whosOnCall.0 : OCTET STRING- (ascii):        david jones
This command tells the SNMP agent to put david jones in the file /opt/local/oncall/today.txt. The filename is defined by the FILE-NAME: /opt/local/oncall/today.txt command that we wrote in the extended MIB. The additional .0 at the end of the OID tells the agent we want the first (and only) instance of whosOnCall. (We could have used .iso.org.dod.internet.private.enterprises.mauro.other.whosOnCall.0 instead of the numeric OID.) Furthermore, the snmpset command specifies the datatype octetstring, which matches the OctetString syntax we defined in the MIB. This datatype lets us insert string values into the file. Finally, we're allowed to set the value of this object with snmpset because we have read-write access to the object, as specified in the MIB.

If you choose to use an editor to create the file, keep in mind that anything after the first line of the file is ignored. If you want to read multiple lines you have to use a table; tables are covered in the next section.

Now let's add another object to the MIB for our extended agent. We'll use a modification of the example OpenView gives us. We'll create an object named fmailListMsgs (2) that summarizes the messages in the mail queue. This object will live in a new subtree, named fmail (4), under the private mauro subtree. So the name of our object will be mauro.fmail.fmailListMsgs or, in numeric form, .1.3.6.1.4.1.2789.4.2. First, we need to define the fmail branch under the mauro subtree. To do this, add the following line to snmpd.extend:

fmail           OBJECT IDENTIFIER ::= { mauro 4 }
We picked 4 for the branch number, but we could have chosen any number that doesn't conflict with our other branches (3 and 255). After we define fmail we can insert the definition for fmailListMsgs into snmpd.extend, placing it before the END statement:

fmailListMsgs OBJECT-TYPE
    SYNTAX DisplayString
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
        "List of messages on the mail queue.
         READ-COMMAND: /usr/lib/sendmail -bp
         READ-COMMAND-TIMEOUT: 10"
    ::= { fmail 2 }
When polled, fmailListMsgs runs the command sendmail -bp, which prints a summary of the mail queue. When all this is done, you can use your management station or a tool such as snmpget to read the value of mauro.fmail.fmailListMsgs and see the status of the outgoing mail queue.

11.3.1. Tables

Tables allow the agent to return multiple lines of output (or other sets of values) from the commands it executes. At its most elaborate, a table allows the agent to return something like a spreadsheet. We can retrieve this spreadsheet using snmpwalk -- a process that's significantly easier than issuing separate get operations to retrieve the data one value at a time. One table we've already seen is .iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable, which is defined in MIB-II and contains information about all of a device's interfaces.

Every table contains an integer index, which is a unique key that distinguishes the rows in the table. The index starts with 1, for the first row, and increases by one for each following row. The index is used as an instance identifier for the columns in the table; given any column, the index lets you select the data (i.e., the row) you want. Let's look at a small table, represented by the text file animal.db:

1       Tweety        Bird    Chirp   2
2       Madison       Dog     Bark    4
3       "Big Ben"     Bear    Grrr    5
Our goal is to make this table readable via SNMP, using OpenView's extensible agent. This file is already in the format required by the agent. Each column is delimited by whitespace; a newline marks the end of each row. Data that includes an internal space is surrounded by quotes. OpenView doesn't allow column headings in the table, but we will want to think about the names of the objects in each row. Logically, the column headings are nothing more than the names of the objects we will retrieve from the table. In other words, each row of our table consists of five objects:

animalIndex
An index that specifies the row in the table. The first row is 1, as you'd expect for SNMP tables. The SYNTAX for this object is therefore INTEGER.

animalName
The animal's name. This is a text string, so the SYNTAX of this object will be DisplayString.

animalSpecies
The animal's species (another text string, represented as a DisplayString).

animalNoise
The noise the animal makes (another DisplayString).

animalDanger
An indication of how dangerous the animal is. This is another INTEGER, whose value can be from 1 to 6. This is called an "enumerated integer"; we're allowed to assign textual mnemonics to the integer values.

At this point, we have just about everything we need to know to write the MIB that allows us to read the table. For example, we know that we want an object named animalNoise.2 to access the animalNoise object in the second row of the table; this object has the value Bark. It's easy to see how this notation can be used to locate any object in the table. Now let's write the MIB definition for the table.

TableExtExample DEFINITIONS ::= BEGIN

internet        OBJECT IDENTIFIER ::= { iso(1) org(3) dod(6) 1 }
enterprises     OBJECT IDENTIFIER ::= { internet(1) private(4) 1 }
mauro           OBJECT IDENTIFIER ::= { enterprises(1) 2789 }
other           OBJECT IDENTIFIER ::= { mauro 255 }

AnimalEntry ::=
    SEQUENCE {
    animalIndex INTEGER,
    animalName DisplayString,
    animalSpecies DisplayString,
    animalNoise DisplayString,
    animalDanger INTEGER
    }

animalTable OBJECT-TYPE
    SYNTAX SEQUENCE OF AnimalEntry
    ACCESS not-accessible
    STATUS mandatory
    DESCRIPTION
        "This is a table of animals that shows:
         Name
         Species
         Noise
         Danger Level
         FILE-NAME: /opt/local/animal.db"
    ::= { other 247 }

animalEntry OBJECT-TYPE
    SYNTAX AnimalEntry
    ACCESS not-accessible
    STATUS mandatory
    DESCRIPTION
        "List of animalNum"
    INDEX { animalIndex }
    ::= { animalTable 1 }

animalIndex OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
        "The unique index number we will use for each row"
    ::= { animalEntry 1 }

animalName OBJECT-TYPE
    SYNTAX DisplayString
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
        "My pet name for each animal"
    ::= { animalEntry 2 }

animalSpecies OBJECT-TYPE
    SYNTAX DisplayString
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
        "The animal's species"
    ::= { animalEntry 3 }

animalNoise OBJECT-TYPE
    SYNTAX DisplayString
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
        "The noise or sound the animal makes"
    ::= { animalEntry 4 }

animalDanger OBJECT-TYPE
    SYNTAX INTEGER {
        no-Danger(1),
        can-Harm(2),
        some-Damage(3),
        will-Wound(4),
        severe-Pain(5),
        will-Kill(6)
    }
    ACCESS read-write
    STATUS mandatory
    DESCRIPTION
        "The level of danger that we may face with the particular animal"
    ::= { animalEntry 5 }

END
The table starts with a definition of the animalTable object, which gives us our DESCRIPTION and tells the agent where the animal.db file is located. The SYNTAX is SEQUENCE OF AnimalEntry. AnimalEntry (watch the case) gives us a quick view of all our columns. You can leave AnimalEntry out, but we recommend that you include it since it documents the structure of the table.

The table is actually built from animalEntry elements -- because object names are case sensitive, this object is different from AnimalEntry. animalEntry tells us what object we should use for our index or key; the object used as the key is in brackets after the INDEX keyword.

The definitions of the remaining objects are similar to the definitions we've already seen. The parent-subtree for all of these objects is animalEntry, which effectively builds a table row from each of these objects. The only object that's particularly interesting is animalDanger, which uses an extension of the INTEGER datatype. As we noted before, this object is an enumerated integer, which allows us to associate textual labels with integer values. The values you can use in an enumerated type should be a series of consecutive integers, starting with 1.
[62] For example, the animalDanger object defines six values, ranging from 1 to 6, with strings like no-danger associated with the values.

[62]Some SNMPv1 SMI-compliant MIB compilers will not allow an enumerated type of 0 (zero).

You can save this table definition in a file and use the xnmloadmib command to load it into OpenView. Once you've done that and created the animal.db file with a text editor, you can walk the table:

$ snmpwalk sunserver1 .1.3.6.1.4.1.mauro.other.animalTable
animalEntry.animalIndex.1 : INTEGER: 1
animalEntry.animalIndex.2 : INTEGER: 2
animalEntry.animalIndex.3 : INTEGER: 3
animalEntry.animalName.1 : DISPLAY STRING-(ascii): Tweety
animalEntry.animalName.2 : DISPLAY STRING-(ascii): Madison
animalEntry.animalName.3 : DISPLAY STRING-(ascii): Big Ben
animalEntry.animalSpecies.1 : DISPLAY STRING-(ascii): Bird 
animalEntry.animalSpecies.2 : DISPLAY STRING-(ascii): Dog
animalEntry.animalSpecies.3 : DISPLAY STRING-(ascii): Bear
animalEntry.animalNoise.1 : DISPLAY STRING-(ascii): Chirp
animalEntry.animalNoise.2 : DISPLAY STRING-(ascii): Bark
animalEntry.animalNoise.3 : DISPLAY STRING-(ascii): Grrr
animalEntry.animalDanger.1 : INTEGER: can-Harm
animalEntry.animalDanger.2 : INTEGER: will-Wound
animalEntry.animalDanger.3 : INTEGER: severe-Pain
snmpwalk goes through the table a column at a time, reporting all the data in a column before proceeding to the next. This is confusing -- it would be easier if snmpwalk read the table a row at a time. As it is, you have to hop from line to line when you are trying to read a row; for example, to find out everything about Tweety, you need to look at every third line (all the .1 items) in the output.

Two more things are worth noticing in the snmpwalk output. The first set of values that snmpwalk reports are the index values (animalIndex). It then appends each index value to each OID to perform the rest of the walk. Second, the animalDanger output reports strings, such as can-Harm, rather than integers. The conversion from integers to strings takes place because we defined the animalDanger object as an enumerated integer, which associates a set of possible values with strings.

Of course, just reading a table doesn't do a whole lot of good. Let's say that we need to update this file periodically to reflect changes in the animals' behavior. The animalDanger object has an ACCESS of read-write, which allows us to set its value and update the database file using our SNMP tools. Imagine that the dog in row 2 turns very mean. We need to turn its danger level to 5 (severe-Pain). We could edit the file by hand, but it's easier to issue an snmpset:

$ snmpset -c private sunserver2 \
mauro.other.animalTable.animalEntry.animalDanger.2 integer "5"
mauro.other.animalTable.animalEntry.animalDanger.2 : INTEGER: severe-Pain
Now let's go back and verify that the variable has been updated:
[63]

[63]We could already deduce that the set was successful when snmpset didn't give us an error. This example does, however, show how you can snmpget a single instance within a table.

$ snmpget sunserver2 \
mauro.other.animalTable.animalEntry.animalDanger.2
mauro.other.animalTable.animalEntry.animalDanger.2 : INTEGER: severe-Pain
Once the snmpset is complete, check the file to see how it has changed. In addition to changing the dog's danger level, it has enclosed all strings within quotes:

1 "Tweety" "Bird" "Chirp" 2
2 "Madison" "Dog" "Bark" 5
3 "Big Ben" "Bear" "Grrr" 5
There are even more possibilities for keeping the file up-to-date. For example, you could use a system program or application to edit this file. A cron job could kick off every hour or so and update the file. This strategy would let you generate the file using a SQL query to a database such as Oracle. You could then put the query's results in a file and poll the file with SNMP to read the results. One problem with this strategy is that you must ensure that your application and SNMP polling periods are in sync. Make sure you poll the file after Oracle has updated it, or you will be viewing old data.

An effective way to ensure that the file is up-to-date when you read it is to use FILE-COMMAND within the table's definition. This tells the agent to run a program that updates the table before returning any values. Let's assume that we've written a script named get_animal_status.pl that determines the status of the animals and updates the database accordingly. Here's how we'd integrate that script into our table definition:

animalTable OBJECT-TYPE
    SYNTAX   SEQUENCE OF AnimalEntry
    ACCESS   not-accessible
    STATUS   mandatory
    DESCRIPTION
        "This is a table of animals that shows:
         Name
         Species
         Noise
         Danger Level
         FILE-COMMAND: /opt/local/get_animal_status.pl
         FILE-NAME: /opt/local/animal.db"
    ::= { other 247 }
The command must finish within 10 seconds or the agent will kill the process and return the old values from the table. By default, the agent runs the program specified by FILE-COMMAND only if it has not gotten a request in the last 10 seconds. For example, let's say you issue two snmpget commands, two seconds apart. For the first snmpget, the agent runs the program and returns the data from the table with any changes. The second time, the agent won't run the program to update the data -- it will return the old data, assuming that nothing has changed. This is effectively a form of caching. You can increase the amount of time the agent keeps its cache by specifying a value, in seconds, after FILE-COMMAND-FREQUENCY. For example, if you want to update the file only every 20 minutes (at most), include the following commands in your table definition:

       FILE-COMMAND: /opt/local/get_animal_status.pl
       FILE-COMMAND-FREQUENCY: 1200
       FILE-NAME: /opt/local/animal.db"
This chapter has given you a brief introduction to three of the more popular extensible SNMP agents on the market. While a thorough treatment of every configurable option for each agent is beyond the scope of this chapter, it should help you to understand how to use extensible agents. With an extensible agent, the possibilities are almost endless.



Library Navigation Links

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