11.3. OpenView's Extensible AgentBefore 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: 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:$ cp /opt/OV/prg_samples/eagent/snmpd.extend /etc/SnmpAgent.d/ $ /etc/rc2.d/S98SnmpExtAgt stop $ /etc/rc2.d/S98SnmpExtAgt start [58]On Solaris and HP-UX machines this file is located in /var/adm/snmpd.log. 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:$ snmpwalk sunserver1 .1.3.6.1.4.1.4242.2.2.0 4242.2.2.0 : OCTET STRING- (ascii): Mail queue is empty 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: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 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.ip OBJECT IDENTIFIER ::= { mib-2 4 } ipForwarding OBJECT-TYPE ... ::= { ip 1 } ipDefaultTTL OBJECT-TYPE ... ::= { ip 2 } Figure 11-2. mauro subtreeYou 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: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: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 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:READ-COMMAND: /some/fs/somecommand.pl READ-COMMAND-TIMEOUT: 10 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:$ snmpwalk sunserver2 -c public .1.3.6.1.4.1.2789 mauro.sysInfo.psZombieNum.0 : INTEGER: 0 mauro.sysInfo.prtDiagExitC.0 : INTEGER: 2 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:$ 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 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:fmail OBJECT IDENTIFIER ::= { mauro 4 } 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.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 } 11.3.1. TablesTables 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: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:1 Tweety Bird Chirp 2 2 Madison Dog Bark 4 3 "Big Ben" Bear Grrr 5
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.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 [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 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:$ 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 Now let's go back and verify that the variable has been updated:[63]$ snmpset -c private sunserver2 \ mauro.other.animalTable.animalEntry.animalDanger.2 integer "5" mauro.other.animalTable.animalEntry.animalDanger.2 : INTEGER: severe-Pain [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. 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:$ snmpget sunserver2 \ mauro.other.animalTable.animalEntry.animalDanger.2 mauro.other.animalTable.animalEntry.animalDanger.2 : INTEGER: severe-Pain 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:1 "Tweety" "Bird" "Chirp" 2 2 "Madison" "Dog" "Bark" 5 3 "Big Ben" "Bear" "Grrr" 5 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: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 } 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.FILE-COMMAND: /opt/local/get_animal_status.pl FILE-COMMAND-FREQUENCY: 1200 FILE-NAME: /opt/local/animal.db" Copyright © 2002 O'Reilly & Associates. All rights reserved. |
|