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.
--with-mib-modules="podunk_rr"
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
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.1The 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.3That 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