Contents

fsubagent - a MIB compiler and C library to implement simple MIBs

The learning curve for SNMP libraries like ucd-snmp is steep, and some managers hesitate to integrate even simple SNMP agents into their server code because of the perceived complexity.

The fsubagent library and MIB compiler address both those problems. It focuses on a simple subset of SNMP - read-only scalar variables and tables indexed by one or two small integers - and a simple data model; in exchange for these limitations, it relieves the programmer of knowing anything about the ucd-snmp library, it allows you to avoid linking libsnmp into your server program, it provides a .h file with OID values translated into C symbolic constants, and it provides convenient C++ wrapper classes for accessing SNMP variables.

Its MIB compiler is more fragile than the stock 'mib2c' MIB compiler that comes with ucd-snmp, but it automatically generates C++ wrapper classes for each table row (aka table entry) as well as for 'scalar rows' (groups of scalar variables that share the same OID prefix).

fsubagent is written in C for compatibility with ucd-snmp, in an object-oriented style which provides good encapsulation and data hiding. fsubagent.h is the 'top level' of the package; it represents a Windows Registry - like container for MIB variables, and provides load and save methods as well as row and table creation and accessor methods.

The C classes fsubagentRow, fsubagentArray, and fsubagentArray2 help represent rows, one-index tables, and two-index tables, respectively, and provide row and item accessor methods.

The C++ wrapper classes generated by mib2h.pl may also be used if using C code directly is frowned upon for whatever reason; they provide a modicum of convenience and typesafety, and help hide the C-ishness of the library from the programmer to some extent.

One reason it's hard to add SNMP capabilities to an existing server is that being an SNMP server is kind of an event driven thing; your server suddenly has to respond to asynchronous requests from the outside world on a new channel it wasn't originally designed for. That raises thread safety issues. Also, ucd-snmp by itself makes you write event-driven code to handle the SNMP requests, which isn't always the easiest thing to get used to. fsubagent addresses both of these issues by implementing a threadsafe datastore. This means that no big rewrite of an existing server is needed to add simple read-only monitoring; just add lines like

myRow->set_fooValue(5);
as desired in the server; fsubagent's mib module, running in the snmpd process, grabs the values out of the datastore when requests come in.

Demo application using fsubagent

Life is hard enough without good examples, so fsubagent comes with one. It consists of the following files: To try out the demo, download ucd-snmp-4.2.1.tar.gz into the same directory as these files, then run demo.sh. It does the following things:
  1. compiles the fsubagent library
  2. compiles the demo MIB into a .h file
  3. compiles and run the demo apps
  4. unpacks ucd-snmp and installs the demo mib and mib module into ucd-snmp
  5. builds and runs an snmpd
  6. retrieves the demo app's values from the snmpd via snmpdump
You might want to read through demo.sh and understand it before you run it.

Using fsubagent

Here are the steps needed to add monitoring to an application via fsubagent and ucd-snmp:
  1. Read and understand demo.sh. It goes through all the steps you'll have to follow.
  2. Adapt the example MIB ( PODUNK-RR-MIB.txt ) and compile it with mib2h.pl to produce a .h file (to be #include'd by both your server program and the module that implements your MIB)
  3. Adapt the example MIB implementation ( podunk_rr.c) to reflect the tables and scalar rows defined by your MIB
  4. Copy the demo MIB and demo MIB module into your ucd-snmp source tree with 'sh cp2ucd.sh DEST' or something like it
  5. Build ucd-snmp with that MIB implementation linked in, which means passing the following option to its configure script:
    --with-mib-modules="podunk_rr"     
    

    Once you go through this once, no further edits to the MIB implementation are needed; when you add more variables, just rerun mib2h.pl and recompile, and the MIB implementation will pick up the new variables from the .h file.
  6. add fsubagent objects to the server program you want to monitor (see below)
  7. Build libfsubagent.a with 'make', and link it in to your application.
  8. Set values of SNMP variables (see below)
Note: the ucd-snmp build system might not recompile everything properly after rerunning mib2h.pl (makedepend on red hat linux seems to ignore the -o flag?!), so you might want to do a 'make clean' in agent/mibgroup just in case.

Add fsubagent objects to the program you want to monitor

The server program which wishes to act like an SNMP agent creates a fsubagent; inside the fsubagent, it may create fsubagentRows to hold scalar variables describing the whole program, fsubagentArrays to hold one-index tables, and fsubagentArray2s to hold two-index tables. It then calls fsubagent_load() to restore the values of the monitoring variables as of the last save. It should then call fsubagent_save() periodically to update the SNMP daemon's snapshot of the state of the monitoring variables. The MIB module inside snmpd then reads that same data file on demand.

After the server program calls fsubagent_load(), it needs to link its business objects to the corresponding table entries inside the fsubagent, if any. It does this by looking for items inside the tables using fsubagentArray_getRowByStr(). If it finds one, it reuses it, else it creates one, and also creates a fsubagentArray at the same index inside any tables that extend the main one.

Set values of SNMP variables

Once your server program has created the fsubagentArray2, fsubagentArray, and fsubagentRow objects corresponding to all the two-index and one-index tables and scalar rows in the MIB, it needs to set variable values within those objects. (That's the point of this whole exercise.) It can do this by calling e.g. fsubagentRow_setInt(row, PREFIX_milesPerGallon, value), where PREFIX_milesPerGallon is a symbolic constant defined by the .h file produced by mib2h, corresponding to the SNMP variable milesPerGallon defined in your MIB. If it's a C++ program, you can also do this in a more C++ fashion, e.g. rowWrapper->set_milesPerGallon(value).

See podunk_demo.c for a trivial example. (FIXME: need podunk_demo2.cc, showing same thing using the C++ wrapper objects.)

Dumping a fsubagent data file

Occasionally, when debugging a program that uses fsubagent, you might need to look inside a file saved by fsubagent_save(). You'd think it'd be easy to write a little program that called fsubagent_load(), but that requires knowledge of your MIB, and a fair amount of work. For a quick-and-dirty look, you can run the program fsubagentDump; it doesn't know anything about your MIB, so it just dumps everything out blindly to stdout, guessing the type of the data as it goes. Often it's enough to tell you what you needed to know.

Issues

Appendix

SNMP MIBs for C++ Programmers

SNMP is not easy to understand at first, so here's a microscopic introduction to SNMP MIBs for C++ programmers; this is just enough to cover the data model used by fsubagent.
(For more info on SNMP in general, see e.g. snmp.com or snmpinfo.com; for more info on SNMP MIB design, see Understanding SNMP MIBs by Perkins and McGinnis (reviewed at amazon); for more info on ucd-snmp, see netsnmp.org.)

To export data via SNMP, you first need to define an SNMP MIB (similar to an XML DTD) which declares the data you're going to export. The exact syntax of MIB definition files is beyond the scope of this introduction, but is somewhat human-readable. The MIB file is also used by SNMP clients (and humans!) to interpret the data they receive when performing SNMP queries.

SNMP variable names are called OID's (Object Identifiers). Like DNS hostnames, they are tree-structured, globally unique, and assigned by designated naming authorities. Unlike DNS hostnames, they are read left-to-right (so .iso.org.dod is a subtree of .iso.org), start with a period, can be written numerically (e.g. .iso.org.dod = .1.3.6), and only have a concrete value when paired with a particular IP address (e.g. "The value of sysUptime at server1.foo.com is 15 hours; it must be running NT 4".)

An organization that wants to define SNMP variables must apply for an OID subtree from a naming authority such as IANA (the Internet Assigned Numbers Authority), which assigns subtrees of .iso.org.dod.internet.private.enterprises.

OIDs are very long, so a way of abbreviating them has been defined: simply drop any number of initial componants, and leave off the initial period. Thus private.enterprises and enterprises are both abbreviations for .iso.org.dod.internet.private.enterprises

SNMP was intended to be used with very primitive devices, so it had to be kept simple. Accordingly, SNMP does not really support any kind of data structuring; it only provides for a sea of individual integers or strings. It simulates arrays by appending one or more positive numbers to the end of variable names, and simulates user-defined data structures with a strict naming convention. Scalar variables always have the number 0 appended to their name. For example, truck.wheelCount.0 might be a scalar integer variable giving how many wheels a truck has, and truck.wheelTable.wheelEntry.wheelPressure.5 might hold the current inflation pressure of the fifth wheel on a truck.

The above example illustrates the naming convention for simulating arrays (called columns in SNMP-speak), which goes like this: each column has a prefix, e.g. 'wheel', and its elements are named
prefixTable.prefixEntry.prefixBlahBlah.index1[.index2[...]]
where "prefix" and "BlahBlah" may be any arbitrary names, but "Table" and "Entry" are literal strings.

The naming convention also helps simulate arrays of data structures (called tables in SNMP-speak). An array of simple data structures is simulated by a collection of similarly named columns. For instance, the C code

    struct {
        string fooName;
        int fooSize;
    } fooTable[2];
is expressed in SNMP as the one-index table fooTable, i.e. as the variables
    fooTable.fooEntry.fooName.1
    fooTable.fooEntry.fooName.2
    fooTable.fooEntry.fooSize.1
    fooTable.fooEntry.fooSize.2

A table row is defined as all the elements of a table that share the same index, e.g. row 1 of fooTable is the set of variables

    fooTable.fooEntry.fooName.1
    fooTable.fooEntry.fooSize.1
The row is represented in the MIB by the 'table entry' object (this is where the 'fooEntry' object in the variable name comes from), which looks quite similar to a C struct, but many MIB compilers can and do safely ignore all information about this object except its name, since the naming convention makes the same information available in the variable names.

Arrays of more complicated data structures are simulated using multiple tables. For instance, the C code used by a train company to represent its fleet of 3 trains (each made up of 2 cars) might look like this:

    struct {
        struct {
            string trainName;
			int trainCarCount;
            struct {
                string carName;
                ...
            } trainCars[2];
            ...
        } trains[3];
    } fleet;
but would be expressed in SNMP as the one-index table trainTable plus the two-index table carTable, i.e. as the variables
    fleet.trainTable.trainEntry.trainName.1
    fleet.trainTable.trainEntry.trainName.2
    fleet.trainTable.trainEntry.trainCarCount.1
    fleet.trainTable.trainEntry.trainCarCount.2
    fleet.carTable.carEntry.carName.1.1
    fleet.carTable.carEntry.carName.1.2
    fleet.carTable.carEntry.carName.1.3
    fleet.carTable.carEntry.carName.2.1
    fleet.carTable.carEntry.carName.2.2
    fleet.carTable.carEntry.carName.2.3
That is, instead of nesting an array of structures inside a structure, SNMP hoists the inner array of structures out to the top level, creating a two-index table whose first index is the same as the only index of the original one-index table.

Copyright 2001 Omniva Policy Systems and Dan Kegel
Released under same BSD-style license as ucd-snmp-4.2.1