NetSNMP subagent development manual

This article from  ------

The OpenHPI Project

----------------

Chapter 1. Introduction

1.1. The Basics

1.1.1. Purpose of this Document

This document is intended to serve as the developer manual for writing SNMP sub-agents using NetSNMP 5.0.x API.

1.1.2. Document Scope

This guide covers the entire process of building a SNMP sub-agent using the NetSNMP 5.0.x API. This includes, but is not limited to, and overview of NetSNMP API function calls, NetSNMP interfaces, and usage scenarios. It is a living document, and as such will evolve.

1.1.3. Terminology

Acronym Description
HPI Hardware Platform Interface
Sub-agent The SNMP backend.


Chapter 2. Getting the pieces

2.1. Pre-requisities

To understand this tutorial its important to have the following under hand:



2.1.1. Generation of skeleton code

In order to implement a new sub-agent, three files are necessary:

  • MIB definition file.

  • C header file

  • C implementation file



Note

The MIB definition file we refer to in this tutorial is the NET-SNMP-TUTORIAL-MIB.txt.

Note

The C header and implementation file can be generated automaticly from the MIB definition file using the mib2c tool.

It generates the corresponding the C and header file (or files) with the skeleton code. This chapter will explain in details how to use the mib2c tool. The next chapter will explain the generated C and header file (or files).

2.1.2. mib2c

From the mib2c manpage. The mib2c tool is designed to take a portion of the MIB tree (as defined by a MIB file) and generate the template C code necessary to implement the corresponding MIB module.

The mib2c tool uses the MIB definition file to produce the two C code files. Thus, mib2c generates a template that you can edit to add logic necessary to obtain information from the operating system or application to complete the module.

MIBNODE is the top level mib node you want to generate code for. You must give mib2c a mib node (e.g., netSnmpIETFWGTable), not a mib file on the command line. (this is the most common mistake).

The mib2c tool accepts both SMIv1 and SMIv2 MIBs.

mib2c needs to be able to find and load a MIB file in order to generate C code for the MIB. To enable mib2c to find the MIB file, set the MIBS environment variable to include the MIB file you are using. An example of setting this environment variable is:

$ export MIBS=+NET-SNMP-TUTORIAL-MIB


or

$ export MIBS=ALL


The first example ensures that mib2c finds the NET-SNMP- TUTORIAL-MIB mib, in addition to the default MIB modules. The default list of MIB modules is set when the suite is first configured and built and basically corresponds to the list of modules that the agent supports. The second example ensures that mib2c finds all MIBs in the search location for MIB files. The default search location for MIB files is /usr/share/snmp/mibs. This search location can be modified by the MIBDIRS environment variable.

Both the MIB files to be loaded and the MIB file search location can also be configured in the snmp.conf file. Please see snmp.conf(5) for more information.

The generated *.c and *.h files will be created in the current working directory.

2.1.3. How should the sub-agent be generated ?

The second parameter to mib2c tool is the CONFIGFILE which is used when generating code. These files will be searched for first in the current directory and then in the /usr/share directory (which is where the default mib2c configuration files can be found). Running mib2c without the -c CONFIGFILE option will display a description of the valid val­ ues for CONFIGFILE, that is, the available config files, including new ones that you might author.

For example,

$ mib2c netSnmpIETFWGTable



will display a description of the currently avail­ able values for CONFIGFILE.

The following values are supported for CONFIGFILE:

mib2c.scalar.conf
mib2c.int_watch.conf
mib2c.iterate.conf
mib2c.create-dataset.conf
mib2c.array-user.conf
mib2c.column_defines.conf
mib2c.column_enums.conf



2.1.3.1. Generating code for scalar objects

If you're writing code for some scalars, run:

$ mib2c -c mib2c.scalar.conf MIBNODE



If you want to magically "tie" integer variables to integer scalars, use:

$ mib2c -c mib2c.int_watch.conf MIBNODE


2.1.3.2. Generating code for tables

If your table data is kept somewhere else (e.g. it's in the kernel and not in the memory of the agent itself) and you need to "iterate" over it to find the right data for the SNMP row being accessed. See the agent/mibgroup/mibII/vacm_con­ text.c file for an example:

$ mib2c -c mib2c.iterate.conf MIBNODE



If your table data is kept in the agent (i.e. it's not located in an external source) and is purely data driven (i.e. you do not need to perform any work when a set occurs). See the agent/mib­ group/examples/data_set.c file for an example of such a table:

$ mib2c -c mib2c.create-dataset.conf MIBNODE



If your table data is kept in the agent (i.e. it's not located in an external source), and you can keep your data sorted by the table index but you do need to perform work when a set occurs:

$ mib2c -c mib2c.array-user.conf MIBNODE


2.1.3.3. Generating header file definitions

To generate just a header with a define for each column number in your table:

$ mib2c -c mib2c.column_defines.conf MIBNODE



To generate just a header with a define for each enum for any column containing enums:

$ mib2c -c mib2c.column_enums.conf MIBNODE


2.1.3.4. Generating code for the 4.x line of code (the older API)

$ mib2c -c mib2c.old-api.conf MIBNODE


2.1.3.5. Example

Warning

If you get a warning message, such as:

you didn't give me a valid OID to start with at /usr/bin/mib2c line 115.

do define the environment variable MIBS to be ALL or the name of your MIB.
$export MIBS=ALL



For our implementation of the netSnmpIETFWGTable we picked the mib2c.array-user.conf configuration file.

$ mib2c -c mib2c.array-user.conf netSnmpIETFWGTable
writing to netSnmpIETFWGTable.h
writing to netSnmpIETFWGTable.c
running indent on netSnmpIETFWGTable.c
running indent on netSnmpIETFWGTable.h


Chapter 3. Understanding the sub-agent

3.1. The minimum

There are two ways of explaining the details of the generated sub-agent code. The first is to explain in detail every piece of the code and let the reader make the connection. The other (which we will use) is to comb through the code explaining what it does and provide working examples and explanation.

To have just a working, compilable code, you need two extra files that are not generated by mib2c. They are the makefile and the sub-agent startup code.

3.1.1. Makefile

Makefile. A good template is available from NET-SNMP Tutorial. I have modified the template to compile the generated C code.

OBJS2=example-demon.o netSnmpIETFWGTable.o
TARGETS=example-demon

CFLAGS=-I. `net-snmp-config --cflags`
BUILDLIBS=`net-snmp-config --libs`
BUILDAGENTLIBS=`net-snmp-config --agent-libs`

# shared library flags (assumes gcc)
DLFLAGS=-fPIC -shared

all: $(TARGETS)

example-demon: $(OBJS2)
$(CC) -o example-demon $(OBJS2) $(BUILDAGENTLIBS)
netSnmpIETFWGTable.o: netSnmpIETFWGTable.c Makefile
$(CC) $(CFLAGS) $(DLFLAGS) -c -o netSnmpIETFWGTable.o \
netSnmpIETFWGTable.c
$(CC) $(CFLAGS) $(DLFLAGS) -o netSnmpIETFWGTable.so \
netSnmpIETFWGTable.o

3.1.2. Sub-agent daemon code

The daemon will use the generated sub-agent code. This example daemon code has been taken from NET-SNMP Tutorial

#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>

#include <signal.h>

#include <netSnmpIETFWGTable.h>

static int keep_running;

RETSIGTYPE
stop_server(int a) {
keep_running = 0;
}

int
main (int argc, char **argv) {
int agentx_subagent=1; /* change this if you want to
be a SNMP master agent */

/* print log errors to stderr */
snmp_enable_stderrlog();
//snmp_set_do_debugging(1);
/* we're an agentx subagent? */
if (agentx_subagent) {
/* make us a agentx client. */
netsnmp_ds_set_boolean(NETSNMP_DS_APPLICATION_ID,
NETSNMP_DS_AGENT_ROLE, 1);
}

DEBUGMSG(("Before agent library init","\n"));
/* initialize the agent library */
init_agent("example-demon");

/* initialize mib code here */

/* mib code: nit_netSnmpIETFWGTable from init_netSnmpIETFWGTable.c */
init_netSnmpIETFWGTable();

/* example-demon will be used to read example-demon.conf files. */
init_snmp("example-demon");

/* If we're going to be a snmp master agent, initial the ports */
if (!agentx_subagent)
init_master_agent(); /* open the port to listen on
(defaults to udp:161) */

/* In case we recevie a request to stop (kill -TERM or kill -INT) */
keep_running = 1;
signal(SIGTERM, stop_server);
signal(SIGINT, stop_server);

/* you're main loop here... */
while(keep_running) {
/* if you use select(), see snmp_select_info() in snmp_api(3) */
/* --- OR --- */
agent_check_and_process(1); /* 0 == don't block */
}

/* at shutdown time */
snmp_shutdown("example-demon");
return 1;
}

Note

The only change from the original source file code was replacing init_nstAgentSubagentObject(); with init_netSnmpIETFWGTable();.

For right now we will skip explanation of these two files and concentrate on the sub-agent generated code.



Chapter 4. Header file

4.1. Header files

The header file generated by the 'mib2c' tool consist of five elements:

  • A context structure. It is a one to one mapping of the leaf nodes (columnar objects) in a SEQUENCE table to a C structure, with the first variable being the index tuple.

  • Functions for manipulating the context structure. These include generic functions for sorting, getting an item, checking for correct boundary conditions, and generating the index tuple.

  • Object IDentifier for the table.

  • Textual names of the leaf nodes (columnar objects) using #define.

  • #define enabling/disabling different functions of the sub-agent table manipulations.

4.1.1. Context structure

The context structure is used exclusivly by the NetSNMP API for manipulating rows of leaf nodes (columnar objects).

The first named variables of the structure is the index variable.

      typedef struct netSnmpIETFWGTable_context_s {
netsnmp_index index; /** THIS MUST BE FIRST!!! */
....

The netsnmp_index is defined in net-snmp/types.h as

    typedef struct netsnmp_index_s {
int len;
oid *oids;
} netsnmp_index;

The oid is defined as unsigned long (look in net-snmp/library/asn1.h). The len is the number of entries in the *oids array, not the length of the OID in the bytes.

This netsnmp_index structure is used to place the index tuple of the row. Not the full OID (as in fully expanded OID of the table), but just the sub-set of numbers uniquely defining the row alsa known as index value.

4.1.1.1. Example of using netsnmp_index

The example uses the IETF WG table structure to set the index tuple to the string "fodo."

         netsnmp_index *index;
oid index_oid[5];

index = SNMP_MALLOC_TYPEDEF(netsnmp_index);
index_oid[0] = 4; // The first value is the length of the string.
index_oid[1] ='f';
index_oid[2] ='o';
index_oid[3] ='d';
index_oid[4] ='o';

index->oids = index_oid;
index->len = 5;

This example will generate the index tuple, nothing else.

4.1.1.2. Rest of context structure

The rest of the generated structure contains columnar (leaf nodes) mapped to C types. The generator assumes the most general type of the object - therefore a TruthValue textual convention in a MIB will map to an ASN_INTEGER type - in C language this will be unsigned long. If you don't remember what textual conventions are and what their basics data types are, consult the SNMP-v2-TC MIB file.

Note

The length of the data (if the columnar node is defined as OCTET STRING(SIZE(1..10)) is NOT taken under consideration and has to be manually changed from the maximum length. As in with :

       /** OCTETSTR = ASN_OCTET_STR */
char nsIETFWGName[65535];
long nsIETFWGName_len;

Where nsIETFWGName is mapped to the columnar node nsIETFWGName of string size greater than one but less than thirty-two.
nsIETFWGName OBJECT-TYPE
SYNTAX OCTET STRING (SIZE(1..32))
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION
"The name of the IETF Working Group this table describes."
::= { netSnmpIETFWGEntry 1 }
Please consult NET-SNMP-EXAMPLES-MIB.txt for the rest of the object if interested.

Subsequent examples will demonstrate how to fully leverage the rest of the context structure. Please consult Section 5.2.6.2.

4.1.2. Functions

The 'mib2c' places the function prototypes right behind the context structure and also behind the textual names of the leaf nodes.

void init_netSnmpIETFWGTable(void);
void initialize_table_netSnmpIETFWGTable(void);
const netSnmpIETFWGTable_context *
netSnmpIETFWGTable_get_by_idx(netsnmp_index *);
const netSnmpIETFWGTable_context *
netSnmpIETFWGTable_get_by_idx_rs(netsnmp_index *,
int row_status);
int netSnmpIETFWGTable_get_value(netsnmp_request_info *,
netsnmp_index *, netsnmp_table_request_info *);

The first set of declared functions are used to:

  • Initialize the table. This function will be fully explained in Section 5.1

  • Helper functions for retrieving the secondary index value. What is a secondary index value? It is the second index in a index tuple. For detail information, please look in Section 5.1.7.1 and Section 5.2.6.2.

    This function allows for the sub-agent to check (or reshape) of the column objects in a row before delievering to the user.

  • Helper functions for retrieving the data value of the columnar object in a specific row. The column number, and row is passed in to this function.

The next set of functions are controlled by a set of #define statements. These operations are for writing, deleting, creating, and retrieving secondary index of rows.

#ifdef netSnmpIETFWGTable_SET_HANDLING

int netSnmpIETFWGTable_extract_index( netSnmpIETFWGTable_context * ctx, netsnmp_index * hdr );

void netSnmpIETFWGTable_set_reserve1( netsnmp_request_group * );
void netSnmpIETFWGTable_set_reserve2( netsnmp_request_group * );
void netSnmpIETFWGTable_set_action( netsnmp_request_group * );
void netSnmpIETFWGTable_set_commit( netsnmp_request_group * );
void netSnmpIETFWGTable_set_free( netsnmp_request_group * );
void netSnmpIETFWGTable_set_undo( netsnmp_request_group * );

netSnmpIETFWGTable_context *
netSnmpIETFWGTable_duplicate_row( netSnmpIETFWGTable_context* );

netsnmp_index *
netSnmpIETFWGTable_delete_row( netSnmpIETFWGTable_context* );

int netSnmpIETFWGTable_can_delete(netSnmpIETFWGTable_context *undo_ctx,
netSnmpIETFWGTable_context *row_ctx,
netsnmp_request_group * rg);


#ifdef netSnmpIETFWGTable_ROW_CREATION
netSnmpIETFWGTable_context *
netSnmpIETFWGTable_create_row( netsnmp_index* );
#endif
#endif

#ifdef netSnmpIETFWGTable_IDX2
netSnmpIETFWGTable_context *
netSnmpIETFWGTable_get( const char *name, int len );
#endif

There are three sets of functions:

  • For SNMP SET operations. These operations are used to write and delete (if appropiate) rows. Consult Section 5.2.5 and Section 5.2.7 for more details.

  • For creation of rows. Consult Section 5.2.6 for more details.

  • Manipulating secondary (or more) index tuple. Consult Section 5.1.7.1.

4.1.3. Object IDentifier of the table

The 'mib2c' tool also extracts the OID of the table. This sequence of numbers is used by the sub-agent when registering the table.

4.1.4. Textual names for leaf nodes

The leaf nodes object names (from the MIB) are translated to a list of #define in the format of:

#define COLUMN_<name of the leaf node> <columnar index value>

for each of the leaf nodes.

#define COLUMN_NSIETFWGNAME 1
#define COLUMN_NSIETFWGCHAIR1 2
#define COLUMN_NSIETFWGCHAIR2 3

These textual names are used in the generated C source code instead of the numerical values for developer convenience.

4.1.5. Enabling and disabling operations

The three set of #define statements disable or enable sub-agent operations.

  • Commenting out netSnmpIETFWGTable_SET_HANDLING will make the sub-agent not handle SNMP SET requests.

  • Commenting out netSnmpIETFWGTable_ROW_CREATION will not allow the sub-agent to create rows.

  • Commenting out netSnmpIETFWGTable_IDX2 disables the handling of secondary (or more) of index.

These #define statements also exist in the generated C source code to properly disable/enable certain functions. For the purpose of this tutorial assume that all of the #define have to be enabled except netSnmpIETFWGTable_IDX2 (consult Section 5.1.8 for details on the usage of this switch).



Chapter 5. C code

Reading this chapter from top to bottom is the best approach.

5.1. Initializiation process of sub-agent

The sub-agent is initialized by the call

   33    /* mib code: nit_netSnmpIETFWGTable from netSnmpIETFWGTable.c */
34 init_netSnmpIETFWGTable();

from the sub-agent daemon code. This call in the generated code calls the function which sets up the skeleton of the table. It does not add any actual values in the columns.

/************************************************************
*
* Initialize the netSnmpIETFWGTable table by defining its contents
and how it's structured
*/
void
initialize_table_netSnmpIETFWGTable(void)
{
netsnmp_table_registration_info *table_info;

if(my_handler) {
snmp_log(LOG_ERR,
"initialize_table_netSnmpIETFWGTable_handler called again\n");
return;
}

memset(& cb, 0x00, sizeof(cb));

/** create the table structure itself */
table_info = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info);

/* if your table is read only, it's easiest to change the
HANDLER_CAN_RWRITE definition below to HANDLER_CAN_RONLY */
my_handler = netsnmp_create_handler_registration(
"netSnmpIETFWGTable",
netsnmp_table_array_helper_handler,
netSnmpIETFWGTable_oid,
netSnmpIETFWGTable_oid_len,
HANDLER_CAN_RWRITE);

if (!my_handler || !table_info) {
snmp_log(LOG_ERR, "malloc failed in "
"initialize_table_netSnmpIETFWGTable_handler\n");
return; /** mallocs failed */
}

/***************************************************
* Setting up the table's definition
*/
/*
* TODO: add any external indexes here.
*/

/*
* internal indexes
*/
/** index: nsIETFWGName */
netsnmp_table_helper_add_index(table_info, ASN_OCTET_STR);

table_info->min_column = netSnmpIETFWGTable_COL_MIN;
table_info->max_column = netSnmpIETFWGTable_COL_MAX;

/***************************************************
* registering the table with the master agent
*/
cb.get_value = netSnmpIETFWGTable_get_value;
cb.container = netsnmp_container_find("netSnmpIETFWGTable_primary:"
"netSnmpIETFWGTable:"
"table_container");
#ifdef netSnmpIETFWGTable_IDX2
netsnmp_container_add_index(cb.container,
netsnmp_container_find("netSnmpIETFWGTable_secondary:"
"netSnmpIETFWGTable:"
"table_container"));
cb.container->next->compare = netSnmpIETFWGTable_cmp;
#endif
#ifdef netSnmpIETFWGTable_SET_HANDLING
cb.can_set = 1;
#ifdef netSnmpIETFWGTable_ROW_CREATION
cb.create_row = (UserRowMethod*)netSnmpIETFWGTable_create_row;
#endif
cb.duplicate_row = (UserRowMethod*)netSnmpIETFWGTable_duplicate_row;
cb.delete_row = (UserRowMethod*)netSnmpIETFWGTable_delete_row;
cb.row_copy = (Netsnmp_User_Row_Operation *)
netSnmpIETFWGTable_row_copy;
/*
cb.can_activate = (Netsnmp_User_Row_Action *)
netSnmpIETFWGTable_can_activate;
cb.can_deactivate = (Netsnmp_User_Row_Action *)
netSnmpIETFWGTable_can_deactivate;
*/
cb.can_delete = (Netsnmp_User_Row_Action *)
netSnmpIETFWGTable_can_delete;

cb.set_reserve1 = netSnmpIETFWGTable_set_reserve1;
cb.set_reserve2 = netSnmpIETFWGTable_set_reserve2;
cb.set_action = netSnmpIETFWGTable_set_action;
cb.set_commit = netSnmpIETFWGTable_set_commit;
cb.set_free = netSnmpIETFWGTable_set_free;
cb.set_undo = netSnmpIETFWGTable_set_undo;
#endif
DEBUGMSGTL(("initialize_table_netSnmpIETFWGTable",
"Registering table netSnmpIETFWGTable "
"as a table array\n"));
netsnmp_table_container_register(my_handler, table_info, & cb,
cb.container, 1);
}

The init routines performs a couple of basic steps.

5.1.1. Handler

    /* if your table is read only, it's easiest to change the
HANDLER_CAN_RWRITE definition below to HANDLER_CAN_RONLY */
my_handler = netsnmp_create_handler_registration("netSnmpIETFWGTable",
netsnmp_table_array_helper_handler,
netSnmpIETFWGTable_oid,
netSnmpIETFWGTable_oid_len,
HANDLER_CAN_RWRITE);

Create a handler which contains the OID of the table, make it writeable, and which (if any) if the NET-SNMP library helper functions to use.

Note

An OID - Object IDentifier is a an array of longs. The netSnmpIETFWGTable_oid is defined as:

       oid netSnmpIETFWGTable_oid[] = { 1,3,6,1,4,1,8072,2,2,1 };
Note

The netsnmp_table_array_helper_handler is one of many helper functions available. Consult the manpage - man netsnmp_table

5.1.2. Notify the helper function

        /** index: nsIETFWGName */
netsnmp_table_helper_add_index(table_info, ASN_OCTET_STR);

table_info->min_column = netSnmpIETFWGTable_COL_MIN;
table_info->max_column = netSnmpIETFWGTable_COL_MAX;

Notify the helper how many indexes tuples to have and which type they are.

Note

The netSnmpIETFWGTable_COL_MIN is defined in the automaticly generated header file.

Note

The ASN_OCTET_STR and other types are defined in the net-snmp/library/asn1.h file

5.1.3. Register the GET operation

    /***************************************************
* registering the table with the master agent
*/
cb.get_value = netSnmpIETFWGTable_get_value;

This registers the GET routine which will be responsible for providing the Net-SNMP library with the correct data for appropiate columnar nodes. Consult Section 5.3.

5.1.4. Register compare function

#ifdef netSnmpIETFWGTable_IDX2
netsnmp_container_add_index(cb.container,
netsnmp_container_find("netSnmpIETFWGTable_secondary:"
"netSnmpIETFWGTable:"
"table_container"));
cb.container->next->compare = netSnmpIETFWGTable_cmp;
#endif

Register with the main NetSNMP code (by injecting the address of a user-written routine in the callback mechanism) the OID compare routine. This routine is NOT needed if your table rows only have one index value. If you do have more than one index value, and you do need to sort the rows based on a custom sorting algorithm, then make sure that netSnmpIETFWGTable_cmp is properly implemented.

If you do not need to use a custom sorting algorithm, and have more than one index value comment out netSnmpIETFWGTable_IDX2 and let the NetSNMP library do the sorting.

Note

The compare routine should ONLY be used during adding and removal of rows that have more than one index value. For more reasons why, consult this array-user Frequently Asked Questions Page.

More on the compare function in Section 5.1.8.

5.1.5. Alternation and creation of row support

#ifdef netSnmpIETFWGTable_ROW_CREATION
cb.create_row = (UserRowMethod*)netSnmpIETFWGTable_create_row;
#endif

Without this routine being injected in the call-back mechanism, no row creation is possible. The row creation process is user-agnostic - the sub-agent code using internal routines or the SNMP-user using SET operation can create new rows.

However, the SNMP SET operation has to go throughout a set of routines to determine if it has the valid syntax, the right type and length, and other user-defined tests. Those tests are not exercised when the sub-agent code uses the internal routines.

The process by which the NetSNMP library uses to decide if the data is OK is a four state based machine. The data is writen to a new row or an existing one, if it has passed the RESERVE1, RESERVE2, and ACTION phase. The ACTION phase does the modification and if anything goes wrong the process moves to UNDO phase - which restores the original data. Otherwise a COMMIT is perfomed - which modifies the data.

Note

This two-phase commit system is unique. Many other SNMP sub-agent librarys do not implement such technique and have only one SET operation which has to take care of checking the type, size, limitation, perform the write, and undo if needed.

The following picture, from the Net-SNMP Tutorial webpage clearly explains the states.

They are also part of the call-back mechanism. It is the developer responsibility to make sure that those functions are properly implemented (more on this in Section 5.2.7.2).

    cb.set_reserve1 = netSnmpIETFWGTable_set_reserve1;
cb.set_reserve2 = netSnmpIETFWGTable_set_reserve2;
cb.set_action = netSnmpIETFWGTable_set_action;
cb.set_commit = netSnmpIETFWGTable_set_commit;
cb.set_free = netSnmpIETFWGTable_set_free;
cb.set_undo = netSnmpIETFWGTable_set_undo;

Consult Section 5.2.8, Section 5.2.9, Section 5.2.10, Section 5.2.11 and Section 5.2.12 for more details on what these functions do.

5.1.6. Miscellaneous

The above mentioned functions also require a couple of helper functions. Usually you don't need to modify them as the mib2c tool does a great job of implementing them. The routines in question are:

    cb.duplicate_row = (UserRowMethod*)netSnmpIETFWGTable_duplicate_row;
cb.delete_row = (UserRowMethod*)netSnmpIETFWGTable_delete_row;
cb.row_copy = (Netsnmp_User_Row_Operation *)
netSnmpIETFWGTable_row_copy;
cb.can_activate = (Netsnmp_User_Row_Action *)
netSnmpIETFWGTable_can_activate;
cb.can_deactivate = (Netsnmp_User_Row_Action *)
netSnmpIETFWGTable_can_deactivate;
cb.can_delete = (Netsnmp_User_Row_Action *)netSnmpIETFWGTable_can_delete;


5.1.7. Registering handler with container

The last thing that must be done is to register the handler, table information (how many rows, columns, etc), and the call-back mechansim with the container.

The container is the Net-SNMP library part of code that will keep our rows in memory. It will take care of sorting it (when rows are added or removed), providing a specific row for a GET/SET request (so you don't have to extract from the OID the index/column values and find the row by yourself), and more. This container mechanism provides a seperation of the sub-agent developer to have to deal with SNMP GET/SET details and instead concentrate on operating the data.

Note

Look in net-snmp/library/container.h for more details. The macro calls which are defined in there (CONTAINER_FIRST, CONTAINER_GET_SUBSET, etc) will be explained in more details in Section 5.2.6.2.

5.1.7.1. Helper functions to handle n-tuple indexes

Usually tables only require one index value (for example the enumeration of network driver). But in some cases there is a need for a second index, or third, or an index value coded as a string.

The index values are used in determining the order of rows. If the index value is a string in UNICODE of a foreign language - the ascending order the user expects might be completly different from ASCII sort order. Therefore the NetSNMP library provides a mechanism to register the developer's own sorting routine. More on this topic in the section below and in Section 5.2.6.2.2.

5.1.8. Generic compare function

The secondary compare function is only used if netSnmpIETFWGTable_IDX2 is defined. This compare function is used by NetSNMP library when rows are being added or deleted that have more than one index value. If this function is not enabled, the NetSNMP library will use its own generic compare function - which compares index tuples and returns an response based on ASCII ascending order.

The goal of this compare function is to return an integer less than, equal to, or greater than zero if lhs (first argument) is found, respectively, to be less than, to match, or be greater than rhs (second argument).

The implementation of this function is quite straightforward. You use your own method of figuring out which of passed context structures is less, matches or greater.

Consult array-user Frequently Asked Questions Page for more advanced information.

5.1.9. Tree searching function

The tree searching function purpose is to find a context structure based on the custom arguments (defined by developer). This function is not used by NetSNMP library - it is a helper function for the developer if needed.

It returns a matched context structure based on the arguments.





5.2. Creating and Writing (SNMP SET or user) to rows (and columns)

As mentioned in Section 5.1.5, there are three different write-states (RESERVE1, RESERVE2, ACTION) through which a write request to a column, or creation of a row, must pass before it is considered safe to change. If there is any failure during these write-states, it is automaticly free-ed and the request is discarded. If there are no problems, the new request will replace the original (if a column is being modified), or added (if the row did not exist beforehand).

There are also helper functions that are required for this to work, such as making a copy of the row (to keep the original in case the write is unsucessful), extracting the index tuple, creating the index tuple, free-ing the row and checking to see if a row can be safely deleted.

Most of these functions (and the helper ones) generated by the 'mib2c' tool have all of its basic functionality writen. However, some of them require special attention.

The functions are geared towards using SNMP SET and changing the columns of a row. As such their implementation is geared towards such goal, and at first it might be unclear how a sub-agent would create a row by itself. This will be explained in details in Section 5.2.6.2.

To have a better grasp of how the sub-agent would handle request, it is important to first explain some of the helper functions.

5.2.1. Row copy

netSnmpIETFWGTable_row_copy. As the name implies - this function purpose is to copy rows. The generated implementation takes care of copying all of the context structure records. Of interest might be the function snmp_clone_mem which copies the index tuple based on the length of the oid. The reason why a normal memcpy function is not used is due to the neccessity of error checking. If the function cannot determing the correct length of the index tuple (for example the index tuple length might be defined as zero) the copying of the row is stopped. Of course having rows with no index values should never have happened in the first place, but you never known.

5.2.2. Extracting index

The netSnmpIETFWGTable_extract_index is a very important function and needs tweaking to work. Its purpose is to extract the index tuple in an appropiate format for NetSNMP library to understand.

The function purpose is to extract from a linked list of indexes (passed in as netsnmp_index * hdr) its values and store them in the corresponding context structure.

The snippets of code generated by the 'mib2c' tool do most of the work. However the linking of the linked list entries must be done by the developer.

     netsnmp_variable_list var_nsIETFWGName;
...
/**
* Create variable to hold each component of the index
*/
memset( & var_nsIETFWGName, 0x00, sizeof(var_nsIETFWGName) );
var_nsIETFWGName.type = ASN_OCTET_STR;
/** TODO: link this index to the next, or NULL for the last one */
#ifdef TABLE_CONTAINER_TODO
snmp_log(LOG_ERR, "netSnmpIETFWGTable_extract_index index
list not implemented !\n" );
return 0;
#else
var_nsIETFWGName.next_variable = & var_XX;
#endif

The var_nsIETFWGName is the first index of the row. If there were more index values they would be defined as var_<name of columnar node>.

In this snippet of code the linked list of the index values is built. This linked list will be used by parse_oid_indexes to figure out where each index value is suppose to go.

Each of the var_<named values> (there would be more than just one in the example if the index tuple had more than one object defined) type is set to its type (ASN_OCTET_STR, ASN_INTEGER, etc - look in the net-snmp/library/asn1.h) and linked to the next index value. The last index value is set to NULL.

For example, if this table had two extra index values: an enumerated integer value (nsIETFWGProgress) and TruthValue (nsIETFWGIsWorking) with the following ASN.1 definition:

nsIETFWGProgress OBJECT-TYPE
SYNTAX INTEGER {
undefined(0),
proposed(1),
debated(2),
rewritting(3),
draft(4),
standard(50)
}
MAX-ACCESS read-only
STATUS current
DESCRIPTION
"Progress of a work-group"
::= { netSnmpIETFWGEntry 4 }

nsIETFWGIsWorking OBJECT-TYPE
SYNTAX TruthValue
MAX-ACCESS read-only
STATUS current
DESCRIPTION
"Is the work group still working?"
::= { netSnmpIETFWGEntry 5 }

netSnmpIETFWGEntry would have these two objects as extra index nodes:

       INDEX   { nsIETFWGName, nsIETFWGProgress, nsIETFWGIsWorking }

These two extra ASN.1 entries would add two extra variables in the context structure:

        /** INTEGER = ASN_INTEGER */
long nsIETFWGProgress;

/** TruthValue = ASN_INTEGER */
long nsIETFWGIsWorking;

As such, the _extract_index code snippet would look like:

    netsnmp_variable_list var_nsIETFWGName;
netsnmp_variable_list var_nsIETFWGProgress;
netsnmp_variable_list var_nsIETFWGIsWorking;

memset( & var_nsIETFWGName, 0x00, sizeof(var_nsIETFWGName) );
var_nsIETFWGName.type = ASN_OCTET_STR;
var_nsIETFWGName.next_variable = & var_nsIETFWGProgress;

memset( & var_nsIETFWGProgress, 0x00, sizeof(var_nsIETFWGProgress) );
var_nsIETFWGProgress.type = ASN_INTEGER;
var_nsIETFWGProgress.next_variable = & var_nsIETFWGIsWorking;

memset( & var_nsIETFWGIsWorking, 0x00, sizeof(var_nsIETFWGIsWorking));
var_nsIETFWGIsWorking.type = ASN_INTEGER;
var_nsIETFWGIsWorking.next_variable = NULL;

The parse_oid_indexes parses the linked list of index value against the hdr, checks to make sure it is right type, and correct length. If everything is correct, each item of the linked list is populated with the index value taken from the hdr.

However, that does not fill the values of context structure. That is the job of the next part of the code segment in which the return code of parse_oid_indexes is checked, and if found to be OK, the values from linked list are copied to the corresponding context structure entries.

    err = parse_oid_indexes( hdr->oids, hdr->len, & var_nsIETFWGName );
if (err == SNMP_ERR_NOERROR) {
/*
* copy components into the context structure
*/
/** skipping external index nsIETFWGName */
if(var_nsIETFWGName.val_len > sizeof(ctx->nsIETFWGName))
err = -1;
else
memcpy( ctx->nsIETFWGName,
var_nsIETFWGName.val.string,
var_nsIETFWGName.val_len );
ctx->nsIETFWGName_len = var_nsIETFWGName.val_len;
}

Lastly the linked list is cleaned up (during the parsing it might have allocated memory) using:

     snmp_reset_var_buffers( & var_nsIETFWGName );
Note

If you add more index values and change the order in which they appear, make sure that it is always the first index tuple, defined as one of var_<name of columnar node> is being free-ed using snmp_reset_var_buffers.

5.2.3. Row deletion checking

netSnmpIETFWGTable_can_delete does a small check on the passed context structure to see if it can be deleted.

If there are any specific conditions under which a row can not be deleted these should be implemented here.

By default the function returns a positive number, which implies that the row can be safely deleted.

Returning zero means that the row cannot be deleted.

5.2.4. Duplicating rows

netSnmpIETFWGTable_duplicate_row is pretty self-explanatory. Duplicate a row.

5.2.5. Deleting rows

netSnmpIETFWGTable_delete_row purpose is to delete a row. This is function that frees the index tuple, and any other memory that the user might have allocated using

     void * data;

defined in the context structure in the header file.

This function is only used when the NetSNMP library is trying to write to a row and finds out that something is wrong. Then it deletes the temporary row (a copy of the original).

To delete a row from the sub-agent, you need to use the CONTAINER_REMOVE macro. Look in Section 5.2.6.3 for more information.

5.2.6. Creating rows

The netSnmpIETFWGTable_create_row purpose is to create a newly allocated context structure.

netSnmpIETFWGTable_context *
netSnmpIETFWGTable_create_row( netsnmp_index* hdr)
{
netSnmpIETFWGTable_context * ctx =
SNMP_MALLOC_TYPEDEF(netSnmpIETFWGTable_context);
if(!ctx)
return NULL;

/*
* TODO: check indexes, if necessary.
*/
if(netSnmpIETFWGTable_extract_index( ctx, hdr )) {
free(ctx->index.oids);
free(ctx);
return NULL;
}

/* netsnmp_mutex_init(ctx->lock);
netsnmp_mutex_lock(ctx->lock); */

/*
* TODO: initialize any default values here. This is also
* first place you really should allocate any memory for
* yourself to use. If you allocated memory earlier,
* make sure you free it for earlier error cases!
*/
ctx->nsIETFWGChair1_len = 0;
ctx->nsIETFWGChair2_len = 0;

return ctx;
}

The passed in argument netsnmp_index* hdr defines the index of the row. This hdr value is checked using netSnmpIETFWGTable_extract_index routine which extracts the index values from hdr and populates the correct entries in the context structure. If this call fails, the newly allocated context structure is free-ed and the function returns a NULL.

Note

In case you do not know how netSnmpIETFWGTable_extract_index knows which entries to populate, refer to Section 5.2.2, for details.

Note

The netsnmp_mutex_init(ctx->lock); purpose is to create a locking mutex mechanism in case your application is multi-threaded.

Otherwise the context is filled with default values - specified by the developer in the last lines of this routine.

5.2.6.1. What calls netSnmpIETFWGTable_create_row>

Looking back at Section 5.1.5, which described part of the initialization process for a table, the netSnmpIETFWGTable_create_row is initialized in call-back mechansim:

     #ifdef netSnmpIETFWGTable_ROW_CREATION
cb.create_row = (UserRowMethod*)netSnmpIETFWGTable_create_row;
#endif

The NetSNMP library uses this information to call the create_row function whenever a SET request is issued against a non-existing row. The NetSNMP library first searches through the rows - if it cannot find a row with the matching index value, it calls netSnmpIETFWGTable_create_row. If this call returns a NULL value the SET request is dropped. Otherwise the next call the NetSNMP library makes is the netSnmpIETFWGTable_set_reserve1 routine, explained later in this chapter.

The create_row function is also the vehicle by which the sub-agent internally can create fully populated rows.

5.2.6.2. Developer row creation

The source code has a perfect spot for user creation - it is right after the call to initialize_table_netSnmpIETFWGTable.

    void
init_netSnmpIETFWGTable(void)
{
initialize_table_netSnmpIETFWGTable();

/*
* TODO: perform any startup stuff here
*/
}

After the initialize call, the table is ready to populated with rows. The rows are the context structures, explained in the previous sections.

Consult Section 4.1.1 for more in depth explanation of the context structure.

The big question is what magic will make NetSNMP library aware of the container structures? By the usage of CONTAINER macros. Look in net-snmp/library/container.h. In it, there are a couple of macro calls:

   /*
* useful macros
*/
#define CONTAINER_FIRST(x) (x)->find_next(x,NULL)
#define CONTAINER_FIND(x,k) (x)->find(x,k)
#define CONTAINER_NEXT(x,k) (x)->find_next(x,k)
#define CONTAINER_GET_SUBSET(x,k) (x)->get_subset(x,k)
#define CONTAINER_SIZE(x) (x)->get_size(x)
#define CONTAINER_ITERATOR(x) (x)->get_iterator(x)
#define CONTAINER_COMPARE(x,l,r) (x)->compare(l,r)
#define CONTAINER_FOR_EACH(x,f,c) (x)->for_each(x,f,c)

These macros provide a wrapper around the row-data (context structure) and make its manipulation possible.

5.2.6.2.1. Example of CONTAINER usage

A good comprehensive example of how to use CONTAINER_ macros is available in NetSNMP source tarball - in the snmplib/test_binary_array.c , or the following example in this tutorial (these lines of code were inserted after initialize_table_netSnmpIETFWGTable().

Note

It is assumed that create_row function is fully implemented for this example to work properly.

static void
print_string(netsnmp_index *i, void *v)
{
int a;
printf("item %p = [",i);
for (a = 1; a <= i->oids[0]; a++)
printf("%c", (char) i->oids[a]);
printf("]\n");
}

/************************************************************
* Initializes the netSnmpIETFWGTable module
*/
void
init_netSnmpIETFWGTable(void)
{
netsnmp_index index;
oid index_oid[MAX_OID_LEN];
char *index_char[] = {"hickory","joe","hickory",
"bob","new orleans","help"};
int i,j;
netSnmpIETFWGTable_context *ctx;

initialize_table_netSnmpIETFWGTable();

for (i = 0; i< 6; i++) {
/*
First value of an index that is ASN_OCTET_STR is
the length of the string.
*/
index_oid[0] = strlen(index_char[i]);
/* The rest is the data copied. */
for (j = 0; j < index_oid[0];j++) {
index_oid[j+1] = *(index_char[i]+j);

}
index.oids = (oid *) & index_oid;
index.len = index_oid[0]+1;
ctx = NULL;
/* Search for it first. */
ctx = CONTAINER_FIND (cb.container, & index);
if (!ctx) {
/* No dice. We add the new row */
ctx = netSnmpIETFWGTable_create_row( & index);
printf("inserting %s\n", ctx-> nsIETFWGName);
CONTAINER_INSERT (cb.container, ctx);
}

}
/*
Since we are done adding the rows, let us display them for the fun.
The easy way:
*/

CONTAINER_FOR_EACH(cb.container, print_string, NULL);

/*
We do not like 'joe', so we remove him.
*/
index_oid[0] = 3;
index_oid[1] = 'j'; index_oid[2] = 'o'; index_oid[3] = 'e';
index.oids = (oid *) & index_oid;
index.len = 4;

ctx = CONTAINER_FIND(cb.container, & index);
if (ctx) {
CONTAINER_REMOVE( cb.container, & index);
netSnmpIETFWGTable_delete_row ( ctx );
printf("Removed joe\n");
}
/*
Print the hard way:
*/

ctx = CONTAINER_FIRST(cb.container);
printf("Find first = %p %s\n",ctx, ctx->nsIETFWGName);
while( ctx ) {
ctx = CONTAINER_NEXT(cb.container,ctx);
if(ctx)
printf("Find next = %p %s\n",ctx, ctx->nsIETFWGName);
else
printf("Find next = %p\n",ctx);
}


}

The output of this daemon should look like this:

inserting hickory
inserting joe
inserting bob
inserting new orleans
inserting help
item 0x4036d008 = [bob]
item 0x4033c008 = [joe]
item 0x4030b008 = [hickory]
item 0x4039e008 = [new orleans]
item 0x403cf008 = [help]
Removed joe
Find first = 0x4036d008 bob
Find next = 0x403cf008 help
Find next = 0x4030b008 hickory
Find next = 0x4039e008 new orleans
Find next = (nil)

5.2.6.2.2. Sorted

You might wonder why the rows are alphabethicly sorted? The reason is that the NetSNMP library uses its own sorting method when adding/deleting rows. It is set by default to be the function netsnmp_compare_netsnmp_index which has the exact same function prototype as the static int netSnmpIETFWGTable_cmp( const void *lhs, const void *rhs );. If you would like to use your own sorting method, add in initialize_table_netSnmpIETFWGTable function the following line:

           cb.container->compare = netSnmpIETFWGTable_cmp;

And make sure that your compare function works properly. Consult Section 5.1.8 and Section 5.1.4 for more details.

5.2.6.3. Developer row deletion.

Removing rows requires four steps. You have know which row you want (by the index value), find it, remove from the container, and then finally free it.

   /*     
We do not like 'joe', so we remove him.
*/
index_oid[0] = 3;
index_oid[1] = 'j'; index_oid[2] = 'o'; index_oid[3] = 'e';
index.oids = (oid *) & index_oid;
index.len = 4;

ctx = CONTAINER_FIND(cb.container, & index);
if (ctx) {
CONTAINER_REMOVE( cb.container, & index);
netSnmpIETFWGTable_delete_row ( ctx );
printf("Removed joe\n");
}

5.2.7. Writing to a row

There are two ways to write to a row. From the perspective of a SNMP SET command and sub-agent (developer).

5.2.7.1. Developer writing to a row

The process of writing to a row from a sub-agent perspective (developer) is simplistic. It requires the task of retrieving the row and modifying it. No need to re-insert it using CONTAINER_INSERT, since it is already in there.

   char chair* = "John Block";
/*
* Modify 'bob'
*/
index_oid[0] = 3;
index_oid[1] = 'b'; index_oid[2] = 'o'; index_oid[3] = 'b';
index.oids = (oid *) & index_oid;
index.len = 4;
ctx = CONTAINER_FIND(cb.container, & index);
if (ctx) {
/* Modify the context to our content. */
ctx->nsIETFWGChair1_len = strlen(chair);
memcpy(ctx->nsIETFWGChair1, chair, ctx->nsIETFWGChair1_len);
}

5.2.7.2. SNMP SET writing to a row

This process is more complex due to the neccessity of checking that the SNMP SET request is the correct type, length, and other checks that the developer might deem neccesary.

The process by which the NetSNMP library uses to decide if the data is OK is by a state machine. If the SET request passes succesfully through the RESERVE1, RESERVE2, and ACTION phase it is committed to memory.

The following picture, borrowed from NetSNMP webpage, demonstrates these steps. A more detailed explanation of what happens during these steps is explained in this rstory's NET-SNMP Developers Frequently Asked Questions Page: Baby Steps Flow.

5.2.8. RESERVE1 function

The netSnmpIETFWGTable_set_reserve1 job is to make sure that the SET request is of right type.

 for( current = rg->list; current; current = current->next ) {

var = current->ri->requestvb;
rc = SNMP_ERR_NOERROR;

switch(current->tri->colnum) {

case COLUMN_NSIETFWGCHAIR1:
/** OCTETSTR = ASN_OCTET_STR */
rc = netsnmp_check_vb_type_and_size(var, ASN_OCTET_STR,
sizeof(row_ctx->nsIETFWGChair1));
break;

The for loop goes through all of the SNMP SET requests. The netsnmp_request_group *rg keeps a list of aggregated SNMP SET request for this particular table.

Note

This can mean that this function is called with more than one SNMP SET request for different columns.

Note

This list of SNMP SET request can also be for non-existent rows, because the index values do not match what the NetSNMP library has in memory. For rows that do not exist in the container (as in, they have not been inserted using CONTAINER_INSERT), the NetSNMP creates a context structure using the netSnmpIETFWGTable_create_row. For those that do exist, it grabs them from the container.

The netsnmp_check_vb_type_and_size checks the type of the SNMP SET request and also the size of the payload.

If checking process fails, the rc is set to an appropiate error code (consult net-snmp/library/snmp.h for the list) and netsnmp_set_mode_request_error is notified. This will result in removal of this SNMP SET request and the end-user will be notified of the appropiate error code.

Note

If the end-user is using SNMP v1, only a selective set of error codes is available. This might give the user a different error code than what the developer had set.

5.2.9. RESERVE2 function

The second stage is checking for appropiate values of the SNMP SET request. This is where the developer checks for the correct length and values of the columnar nodes.

This check is necessary for enumerated integers - it is important to check for the right enumeration values. If you refer back to Section 5.2.2, you will notice the extra defined ASN.1 columnar node called nsIETFWGProgress. This object defines six enumerations: undefined(0), proposed(1), debated(2), rewritting(3), draft(4) and standard(50). The check for the proper enumerations can be carried out in this function, such as:

        case COLUMN_NSIETFWGPROGRESS:
if (((*var->val.integer < 0) ||
(*var->val.integer > 4)) && (*var->val.integer != 50))
{
rc = SNMP_ERR_BADVALUE;
}
break;

For string variables, it important to check the length of the string - to make sure it is not more (or less) to what is defined in the MIB.

nsIETFWGChair1 OBJECT-TYPE
SYNTAX OCTET STRING
MAX-ACCESS read-create
STATUS current
DESCRIPTION
"One of the names of the chairs for the IETF working group."
::= { netSnmpIETFWGEntry 2 }
Note

And our check for strings of length more than 255. In truth, this check might be inappropriate because an OCTET STRING can have a length of 65536. However most SNMP implementations cannot carry such large payloads.

        case COLUMN_NSIETFWGCHAIR2:
/** OCTETSTR = ASN_OCTET_STR */
if (var->val_len > 255)
rc = SNMP_ERR_WRONGLENGTH;
break;

5.2.10. ACTION

The netSnmpIETFWGTable_set_action is the function where the new value is writen in the appropiate context structure and where the developer would perform the write to his/her datastore.

The changes are being writen to row_ctx. A copy of the original row is in undo_ctx. If this function calls netsnmp_set_mode_request_error the newly modified row is discarded and the user is notified of the error state.

It is in this function that the developer can decide if the row has to be removed or created. For deleting, the variable row_deleted in the netsnmp_request_group has to be set:

       rg->row_deleted = 1;

For creating:

       rg->row_created = 1;

5.2.11. COMMIT function

This routine is used to commit the changes to the row. The intent of the ACTION/COMMIT division is that all of the fallible code should be done in the ACTION phase, so that it can be backed out if necessary.

5.2.12. FREE and UNDO function

This explanation is taken from the generated C code.

5.2.12.1. FREE function

If either of the RESERVE calls fail, the write routines are called again with the FREE action, to release any resources that have been allocated. The agent will then return a failure response to the requesting application.

AFTER calling this routine, the agent will delete undo_info.

5.2.12.2. UNDO function

If the ACTION phase does fail (for example due to an apparently valid, but unacceptable value, or an unforeseen problem), then the list of write routines are called again, with the UNDO action. This requires the routine to reset the value that was changed to its previous value (assuming it was actually changed), and then to release any resources that had been allocated. As with the FREE phase, the agent will then return an indication of the error to the requesting application.

BEFORE calling this routine, the agent will update the container (remove any newly inserted row, re-insert any removed row).



5.3. Getting values

The mechanism by which the NetSNMP library retrieves the correct row is by calling the function defined in the callback mechanism. Please refer to Section 5.1.3 for more details.

   cb.get_value = netSnmpIETFWGTable_get_value;

The netSnmpIETFWGTable_get_value is similar to the ACTION, RESERVE1, and RESERVE2 routines by the mechanism of looping through the requests for columns and performing some operation on the correct column. In this case, instead of checking the value, or writing, it is setting a pointer to the appropiate data and the data's length (in bytes).

  switch(table_info->colnum) {

case COLUMN_NSIETFWGNAME:
/** OCTETSTR = ASN_OCTET_STR */
snmp_set_var_typed_value(var, ASN_OCTET_STR,
(char*)& context->nsIETFWGName,
context->nsIETFWGName_len );
break;
...
Note

If one of your columnar nodes is of type write-only. Then just return NULL for that specified column.

The snmp_set_var_typed_value function sets the correct type of the data, a pointer to the location of the data, and the length of the data in bytes. There is no need for host to network byte swapping - the NetSNMP library performs these functions internally if needed.


About Files:
  1.NET-SNMP-EXAMPLES-MIB.txt
  2.netSnmpIETFWGTable.h
  3.netSnmpIETFWGTable.c
posted @ 2008-01-10 12:14  shipfi  阅读(6458)  评论(0)    收藏  举报