Tables (for Novices) using NetSNMP

This is a simple tutorial for implementing tables using NetSNMP. It is aimed at novices, but it does assume that you have read, understood and studied the basic tutorials especially the dynamically loadable object tutorial and the .

The tutorial comes with a very simple MIB (called SIMPLE-TABLE-MIB.my) that defines a table (indexed by an integer called devNumber) and 2 columns: blocksRead and blocksWritten. The structure of the table can be seen by running:

$ snmptranslate SIMPLE-TABLE-MIB::simpleTable -Tp
+--simpleTable(1)
   |
   +--simpleTableEntry(1)
      |  Index: devNumber
      |
      +-- ---- Unsigned  devNumber(1)
      +-- -R-- Unsigned  blocksRead(2)
      +-- -R-- Unsigned  blocksWritten(3)

The first step is to either copy the SIMPLE-TABLE-MIB.my to a place where the agent can find it (/usr/local/share/snmp/mibs in my system) or tell the agent where to find the MIB by changing the mibDirs and mibs lines in your snmp.conf file. You can see where the agent expects to find MIBs by running:

net-snmp-config --default-mibdirs

(More information on this can be provided by the Using MIBs tutorial).

Let us make a first attempt of implementing this table using the MIBs-for-Dummies API (MfD). For more information on how this API works please visit MFD: IF-MIB page where the MfD data structures are explained.

Create a directory of your choice and cd into it.

Now create the template code by calling the mib2c program:

$ mib2c -c mib2c.mfd.conf SIMPLE-TABLE-MIB::simpleTable
Defaults for simpleTable...
writing to -
There are no defaults for simpleTable. Would you like to

  1) Accept hard-coded defaults
  2) Set defaults now [DEFAULT]

Select your choice : 

Select 1 to accept hard-coded defaults and mib2c should generate the .c and header files for you. The next step is to decide on the build method: if you are writing a subagent then execute this command:

$ mib2c -c subagent.m2c SIMPLE-TABLE-MIB::simpleTable

The below will also generate a Makefile for you:

$ mib2c -c mfd-makefile.m2m SIMPLE-TABLE-MIB::simpleTable

In this tutorial we'll assume that you are writing a dynamically loadable object, so the above steps are optional. Since we are creating a dynamically loadable object, we need to create a Makefile that looks like the below (change paths where appropriate):

CC = gcc
CFLAGS = -I. `net-snmp-config --cflags` -fPIC
LDFLAGS= `net-snmp-config --libs` `net-snmp-config \
         --agent-libs`

DL = simpleTable.so
OBJECTS = simpleTable_data_access.o simpleTable_data_get.o \
          simpleTable_data_set.o simpleTable_interface.o \
          simpleTable.o

all: $(DL)

$(DL): $(OBJECTS)
	$(CC) $(CCFLAGS) $(LDFLAGS) -shared -o $@ $(OBJECTS)

$(OBJECTS): %.o :%.c
	$(CC) -c $(CFLAGS) $< -o $@

clean:
	rm *.o

Now we have a Makefile let's try and compile the code by typing:

$ make

I get the following output:

gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable_data_access.c -o simpleTable_data_access.o
simpleTable_data_access.c: In function 'simpleTable_container_load':
simpleTable_data_access.c:311: error: 'blocksRead' undeclared (first use in this function)
simpleTable_data_access.c:311: error: (Each undeclared identifier is reported only once
simpleTable_data_access.c:311: error: for each function it appears in.)
simpleTable_data_access.c:318: error: 'blocksWritten' undeclared (first use in this function)
make: *** [simpleTable_data_access.o] Error 1

So what's going on here? Shouldn't the code compile cleanly? The answer is no it shouldn't and you need to do some work to tell the code how and where to get the data from. The best starting point is to read the file simpleTable-README-FIRST.txt. If you are feeling really lazy and don't want to review all the changes (not recommended) you should at least implement the minimum, which is do all the mandatory changes (marked with 'M'):

:230:M: Implement simpleTable get routines. (simpleTable_data_get.c:18:)
:240:M: Implement simpleTable mapping routines (if any). (simpleTable_data_get.c:19:)
:350:M: Implement simpleTable data load (simpleTable_data_access.c:173:)
:351:M: |-> Load/update data in the simpleTable container. (simpleTable_data_access.c:244:)
:352:M: |   |-> set indexes in new simpleTable rowreq context. (simpleTable_data_access.c:280:)
:380:M: Free simpleTable container data. (simpleTable_data_access.c:364:)

In fact, since our table is so simple and only contains integers we can skip the implementations of the get and mapping routines - mib2c has already done the work for us. But we still need to load the data, so we need to visit simpleTable_data_access.c. A good advice is to actually spend some time reading the comments in the generated code. Mib2c includes a lot of comments to show you what the intended behaviour of the code is.

Let us move on and start editing simpleTable_data_get.c -- this is where you specify where your data comes from. It may come from a file, a database, the operating system, and so on. To keep things simple, we'll first generate some random data inside the program itself. Therefore, open the file and remove any references that are made to FILE. So you need to remove (or comment) these lines:


222     FILE *filep;
223     char line[MAX_LINE_SIZE];


227     /*
228     ***************************************************
229     ***             START EXAMPLE CODE              ***
230     ***---------------------------------------------***/
231     /*
232      * open our data file.
233      */
234     filep = fopen("/etc/dummy.conf", "r");
235     if(NULL ==  filep) {
236         return MFD_RESOURCE_UNAVAILABLE;
237     }
238 
239     /*
240     ***---------------------------------------------***
241     ***              END  EXAMPLE CODE              ***
242     ***************************************************/

233     while( 1 ) {
234     /*
235     ***************************************************
236     ***             START EXAMPLE CODE              ***
237     ***---------------------------------------------***/
238     /*
239      * get a line (skip blank lines)
240      */
241     do {
242         if (!fgets(line, sizeof(line), filep)) {
243             /* we're done */
244             fclose(filep);
245             filep = NULL;
246         }
247     } while (filep && (line[0] == '\n'));
248 
249     /*
250      * check for end of data
251      */
252     if(NULL == filep)
253         break;
254 
255     /*
256      * parse line into variables
257      */
258     /*
259     ***---------------------------------------------***
260     ***              END  EXAMPLE CODE              ***
261     ***************************************************/

295     ***************************************************
296     ***             START EXAMPLE CODE              ***
297     ***---------------------------------------------***/
298     if(NULL != filep)
299         fclose(filep);
300     /*
301     ***---------------------------------------------***
302     ***              END  EXAMPLE CODE              ***
303     ***************************************************/
304 

Next you need to declare some variables that will hold your data. A variable for devNumber which is the table index has already been declared for you so you need to do the other two:

215	u_long devNumber;
216	u_long blocksRead;
217	u_long_blocksWritten;	

Next you need to load the data -- to keep things simple we just generate some data.

     	/*
228      * TODO:351:M: |-> Load/update data in the simpleTable container.
229      * loop over your simpleTable data, allocate a rowreq context,
230      * set the index(es) [and data, optionally] and insert into
231      * the container.
232      */
233         int i;
234         // Let's insert 4 rows
235         for (i=0; i < 4; i++)
236         {
237         /* Table Index */
238         devNumber = i;          
239         /* Blocks Read */
240         blocksRead = 2<

You also need to delete the continue statement from this block of code:

    }
        if(MFD_SUCCESS != simpleTable_indexes_set(rowreq_ctx
                               , devNumber
               )) {
            snmp_log(LOG_ERR,"error setting index while loading "
                     "simpleTable data.\n");
            simpleTable_release_rowreq_ctx(rowreq_ctx);
		/* Delete the below */
            //continue;
        }

Compile again with 'make' and you shouldn't get any errors:

$ make
gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable_data_access.c -o simpleTable_data_access.o
gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable_data_get.c -o simpleTable_data_get.o
gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable_data_set.c -o simpleTable_data_set.o
gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable_interface.c -o simpleTable_interface.o
gcc -c -I. `net-snmp-config --cflags` -fPIC simpleTable.c -o simpleTable.o
gcc  `net-snmp-config --libs` `net-snmp-config --agent-libs` -shared -o simpleTable.so simpleTable_data_access.o simpleTable_data_get.o simpleTable_data_set.o simpleTable_interface.o simpleTable.o

Now it's time to test your work. First add a line that loads the shared object to your snmpd.conf. For example:

# Load simpleTable .so
dlmod simpleTable /path/to/so/simpleTable.so

Restart the agent in the foreground and with debugging turned on:

# /usr/local/sbin/snmpd -Dverbose:simpleTable -f
No log handling enabled - turning on stderr logging
registered debug token verbose:simpleTable, 1
Turning on AgentX master support.
verbose:simpleTable:init_simpleTable: called
verbose:simpleTable:initialize_table_simpleTable: called
verbose:simpleTable:simpleTable_init_data: called
verbose:simpleTable:simpleTable_container_init: called
NET-SNMP version 5.5
verbose:simpleTable:simpleTable_container_load: called
verbose:simpleTable:simpleTable_rowreq_ctx_init: called
verbose:simpleTable:simpleTable_indexes_set: called
verbose:simpleTable:simpleTable_indexes_set_tbl_idx: called
verbose:simpleTable:simpleTable_index_to_oid: called
verbose:simpleTable:simpleTable_rowreq_ctx_init: called
verbose:simpleTable:simpleTable_indexes_set: called
verbose:simpleTable:simpleTable_indexes_set_tbl_idx: called
verbose:simpleTable:simpleTable_index_to_oid: called
verbose:simpleTable:simpleTable_rowreq_ctx_init: called
verbose:simpleTable:simpleTable_indexes_set: called
verbose:simpleTable:simpleTable_indexes_set_tbl_idx: called
verbose:simpleTable:simpleTable_index_to_oid: called
verbose:simpleTable:simpleTable_rowreq_ctx_init: called
verbose:simpleTable:simpleTable_indexes_set: called
verbose:simpleTable:simpleTable_indexes_set_tbl_idx: called
verbose:simpleTable:simpleTable_index_to_oid: called
verbose:simpleTable:simpleTable_container_load: inserted 4 records

Test it:

$ snmptable -v 2c -c public localhost SIMPLE-TABLE-MIB::simpleTable 

SNMP table: SIMPLE-TABLE-MIB::simpleTable

 blocksRead blocksWritten
          2    4294967293
          4    4294967291
          8    4294967287
         16    4294967279

Now you got the basic functionality, let's try and read the numbers from the file. At this stage, all you need to do is change the loop so the numbers are read from a file. If you want to create some semi-realistic information per time instance instead of per device use this command:

iostat -d 5 10 | perl -wanle '/sda/ and print "$. @F[-2,-1]"'

If you are not too pedantic with contiguous index numbers, stick the output in a file for example /tmp/iostat.txt. Essentially all you have to change is the file name in simpleTable_data_access.c:

236     filep = fopen("/tmp/iostat.txt", "r");

And add a line that gets the data (shown in lines 267-270 below). You also need to declare variables for blocksRead, blocksWritten.

241     /*
242     ***---------------------------------------------***
243     ***              END  EXAMPLE CODE              ***
244     ***************************************************/
245     /*
246      * TODO:351:M: |-> Load/update data in the simpleTable container.
247      * loop over your simpleTable data, allocate a rowreq context,
248      * set the index(es) [and data, optionally] and insert into
249      * the container.
250      */
251     while( 1 ) {
252     /*
253     ***************************************************
254     ***             START EXAMPLE CODE              ***
255     ***---------------------------------------------***/
256     /*
257      * get a line (skip blank lines)
258      */
259     do {
260         if (!fgets(line, sizeof(line), filep)) {
261             /* we're done */
262             fclose(filep);
263             filep = NULL;
264         }
265     } while (filep && (line[0] == '\n'));
266         // Get data from the file
267         sscanf(line, "%d %d %d\n",
268                 &devNumber,
269                 &blocksRead,
270                 &blocksWritten);
271 
272     /*
273      * check for end of data
274      */
275     if(NULL == filep)
276         break;
277 
278     /*
279      * parse line into variables
280      */
281     /*
282     ***---------------------------------------------***
283     ***              END  EXAMPLE CODE              ***
284     ***************************************************/

If you are implementing more complex tables with read-write columns and other types, more work is required. Two good examples to look at are:

A little advice on other data types:

If you are using Counter64: You need to use the U64 type, which is struct containing low and high 32-bits of your 64-bit counter. Assumming you have your data stored in a 64-bit variable (uint64_t, long long, whatever) you can do the following to populate your Counter64:

U64 u_processedPackets;
	u_processedPackets.low = 	processedPackets & 0xffffffff;;
        u_processedPackets.high = 	processedPackets >> 32;

If you are using InetAddress: InetAddress must be exactly 4 bytes (if you are using IPv4), you have the option of using the inet_addr function (from <arpa/inet.h>) to give you a 4-byte in_addr_t containing the IP address in bytes. Once you get the address in the appropriate 4-byte format, you need to edit the file named xxxxxx_data_get.c, find the int xxxxx_get function that represent the extraction of the IP address and remove the return MFD_SKIP; line after you performed any necessary checks to ensure the address in the right format.

If you are using DateAndTime: DateAndTime must be either 8 or 11 bytes. So you have a choice of either setting manually year, month, day, hour, minutes, seconds, deci_seconds and calling netsnmp_dateandtime_set_buf_from_vars passing these values or using a char* variable that gets assigned from calling the date_n_time function. Of course this assumes that you have the date and time in UNIX time format.

    time_t dm; // Get the dateandtime from some source that'll return in UNIX format
    // Convert date to u_char
    size_t len;

    dateModified = date_n_time(&dm, &len);    

As with IP addresses, once you get the DateAndTime in the appropriate format, you need to edit the file named xxxxxx_data_get.c, find the int xxxxx_get function that represent the extraction of the DateAndTime and remove the return MFD_SKIP; line after you performed any necessary checks to ensure the DateAndTime in the right format.


Disclaimer: I'm not affiliated with NetSNMP nor am I an NetSNMP expert. I'm just documenting the procedures that worked for me. If these procedures are incorrect please let me know by emailing perez dot angela7 at gmail dot com.