AgentXcpp  Version:0.3
API Documentation
All Classes Functions Variables Enumerations Enumerator Friends Pages
How to implement a table
Note
This is a follow-up tutorial to How to send notifications and traps.

So far we learned how to implement single variables and how to send notifications. This tutorial teaches us how an SNMP table can be implemented. We start with the code from the previous tutorials and add a table to the SIMPLE-MIB:

SIMPLE-MIB DEFINITIONS ::= BEGIN

IMPORTS
    OBJECT-TYPE FROM SNMPv2-SMI
    enterprises FROM SNMPv2-SMI
    NOTIFICATION-TYPE FROM SNMPv2-SMI;

simpleCounter OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-write
    STATUS current
    DESCRIPTION "A simple counter which is incremented each time
                 it is queried. Starts with 0."
    ::={ enterprises 42 1 }

simpleCounterStatus NOTIFICATION-TYPE
    OBJECTS { simpleCounter }
    STATUS current
    DESCRIPTION "A notification sent each second."
    ::={ enterprises 42 0 1 }

counterChangeTable OBJECT-TYPE
    SYNTAX SEQUENCE OF counterChangeEntry
    ACCESS not-accessible
    STATUS current
    DESCRIPTION "This table lists all changes of the simpleCounter object."
    ::= { enterprises 42 2 }

counterChangeEntry OBJECT-TYPE
    SYNTAX counterChangeEntry
    ACCESS not-accessible
    STATUS current
    DESCRIPTION "An entry within the counterChangeEntry table."
    INDEX { counterChangeEpoch, counterChangeMilliseconds }
    ::= { counterChangeTable 1 }
    counterChangeEntry ::= SEQUENCE {
        counterChangeEpoch               INTEGER,
        counterChangeMilliseconds        INTEGER,
        counterChangeTimeString          DisplayString,
        counterChangeCounterValue        INTEGER
    }

counterChangeEpoch OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-only
    STATUS current
    DESCRIPTION "The time at which the change occured.This variable contains 
                 the epoch, i.e. seconds since 1970-01-01, 00:00."
    ::={ counterChangeEntry 1 }

counterChangeMilliseconds OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-only
    STATUS current
    DESCRIPTION "The time at which counter changed. This variable contains the 
                 milliseconds since counterChangeEpoch, to increase 
                 precision."
    ::={ counterChangeEntry 2 }

counterChangeTimeString OBJECT-TYPE
    SYNTAX OCTETSTRING
    ACCESS read-only
    STATUS current
    DESCRIPTION "The time (counterChangeEpoch + counterChangeMilliseconds) as 
                 a human-readable string."
    ::={ counterChangeEntry 3 }

counterChangeValue OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-only
    STATUS current
    DESCRIPTION "The counter value right after the change."
    ::={ counterChangeEntry 4 }

END

Our table has an entry for each counter change which occurred in the past. The table starts empty and each time the counter changes (i.e. by an SNMP GET or SET request), an entry is added.

Each entry within the table has a timestamp, which consists of the epoch time and a milliseconds component, which is used as index of the entry. This is to show how multiple variables can be used to form the table's index. In addition, the time is reported in an human-readable format (e.g. "2011-09-11, 12:00:52.77"). Finally, each entry reports the new counter value.

How tables work in agentXcpp

Technically, our SNMP table is a sequence of counterChangeEntry, which in turn is a structure containing four "real" SNMP variables. The OID of these variables are assembled like this:

<tableOid>.<entrySubOid>.<variableSubOid>.<indexOid>

The <tableOid> is the OID given to the table. The counterChangeTable has the OID enterprises.42.2, which is .1.3.6.1.4.1.42.2.

The <entrySubOid> is a single number assigned to the table entry. The CounterChangeEntry has the OID counterChangeTable.1, which means that <entrySubOid> is 1.

The <variableSubOid> is a single number which can be thought to be the column of a specific variable. The counterChangeEpoch has <variableSubOid> 1, while counterChangeTimeString has <variableSubOid> 3.

The <indexOid> is the concatenation of the index variables (counterChangeEpoch and counterChangeMilliseconds), converted to OID. For example, if counterChangeEpoch was 123456789 and counterChangeMilliseconds was 512, the index would be "123456789.512". The index can be thought of as the identifier of the row in which the entry resides. In terms of a relational database it would be called the primary key of the record.

Thus, the full OID of the counterChangeTimeString in the entry 123456789.512, for example, would be ".1.3.6.1.4.1.42.2.1.3.123456789.512".

A table is implemented by creating a Table object and tell it its <tableOid> (enterprises.42.2). An entry is implemented by subclassing TableEntry, such that the subclass initializes its TableEntry::subid to <entrySubOid> (i.e. 1 for counterChangeEntry). For our table, we will implement the CounterChangeEntry class which contains the four variables counterChangeEpoch, counterChangeMilliseconds, counterChangeTimeString and counterChangeCounterValue. The CounterChangeEntry class must implement the TableEntry::indexVariables() and TableEntry::variables() methods.

Each time an entry is added to the table, the table will query the new entry for all its variables and add these to the MasterProxy object, so that they are served like any other variable. If an entry is removed from the table, the table will remove its variables from the MasterProxy object again. Neither the table nor the entry can actually be queried by SNMP requests; they are only used to organize the variables which can be queried. Therefore, neither the table nor the entries are added to the MasterProxy object. Only the variables are added.

Implementing the CounterChangeEntry class

First of all, we implement the CounterChangeEntry class. Here is the full code of CounterChangeEntry.hpp:

#ifndef _COUNTERCHANGEENTRY_H_
#define _COUNTERCHANGEENTRY_H_
#include <agentxcpp/TableEntry.hpp>
#include <agentxcpp/IntegerVariable.hpp>
#include <agentxcpp/DisplayStringVariable.hpp>
using namespace agentxcpp;
class CounterChangeEntry : public TableEntry
{
public:
QSharedPointer<IntegerVariable> counterChangeEpoch;
QSharedPointer<IntegerVariable> counterChangeMilliseconds;
QSharedPointer<DisplayStringVariable> counterChangeTimeString;
QSharedPointer<IntegerVariable> counterChangeValue;
CounterChangeEntry()
: counterChangeEpoch(new IntegerVariable),
counterChangeMilliseconds(new IntegerVariable),
counterChangeTimeString(new DisplayStringVariable),
counterChangeValue(new IntegerVariable)
{
subid = 1;
}
virtual QVector< QSharedPointer<AbstractVariable> > indexVariables()
{
QVector< QSharedPointer<AbstractVariable> > result;
result << counterChangeEpoch
<< counterChangeMilliseconds;
return result;
}
virtual QMap< quint32, QSharedPointer<AbstractVariable> > variables()
{
QMap< quint32, QSharedPointer<AbstractVariable> > result;
result[1] = counterChangeEpoch;
result[2] = counterChangeMilliseconds;
result[3] = counterChangeTimeString;
result[4] = counterChangeValue;
return result;
}
};
#endif // _COUNTERCHANGEENTRY_H_

The class inherits TableEntry and has four members which represent the four SNMP variables. Since in agentXcpp those variables are handled as QSharedPointer's, the constructor needs to create all of them using the new operator. The constructor also sets TableEntry::subid to 1.

Our MIB states that counterChangeEpoch and counterChangeMilliseconds are used as index. The CounterChangeEntry::indexVariables() method implements this by returning these variables in a QVector, in the order in which they are used in the index. Note that the entry's OID changes if the index variables change their value. Our simpleagent will not change an entry after it was added to the table, though.

Note
The variable types Counter32 and Counter64 are not allowed as index variables according to RFC 2578, 7.7.

The CounterChangeEntry::variables() method return all variables as a QMap. The map key will be used by the table as <variableSubOid> part for the variable.

Extending the SimpleCounter class.

Next, we extent the SimpleCounter class so that it adds an entry to the table each time the counter is changed. First of all, we need additional includes:

#include <agentxcpp/Table.hpp>
#include "CounterChangeEntry.hpp"
#include <QDateTime>

One thing to do is to add a Table member, a member to store the entry just inserted into the table (needed for undoset(), a suitable constructor and the helper function addEntry():

private:
Table* table; // may be the null pointer
QSharedPointer<CounterChangeEntry> lastInsertedEntry; // for undoset()
public:
SimpleCounter(Table* t = 0)
{
table = t;
}
void addEntry()
{
// Add entry with new value to table
if(table)
{
lastInsertedEntry = QSharedPointer<CounterChangeEntry>(new CounterChangeEntry);
QDateTime timeStamp = QDateTime::currentDateTimeUtc();
lastInsertedEntry->counterChangeEpoch->setValue(timeStamp.toTime_t());
lastInsertedEntry->counterChangeMilliseconds->setValue(timeStamp.toMSecsSinceEpoch() - timeStamp.toTime_t()*1000);
lastInsertedEntry->counterChangeTimeString->setValue(timeStamp.toString("yyyy-MM-dd (dddd) hh:mm:ss.z"));
lastInsertedEntry->counterChangeValue->setValue(v);
table->addEntry(lastInsertedEntry);
}
}

Next we have to replace the perform_get(), perform_commitset() and perform_undoset() functions with variants adding and removing an entries to/from the table. Here is how they look like now:

// Replaces the first version:
virtual void perform_get()
{
++v;
addEntry();
}
// Replaces the first version:
virtual bool perform_commitset(qint32 new_value)
{
// Remember old value for rollback
(*old_value) = v;
// Set new value
v = new_value;
// Add table entry
addEntry();
// Operation succeeded
return true;
}
// Replaces the first version:
virtual bool perform_undoset(qint32 new_value)
{
// Restore old value
v = *old_value;
// Release old value
delete old_value;
// Remove table entry
if(table)
{
table->removeEntry(lastInsertedEntry);
}
// Rollback succeeded
return true;
}

And finally we update the main() function, where a table is instantiated and given to the SimpleCounter constructor. Here is the complete revised main() function:

int main(int argc, char** argv)
{
QCoreApplication app(argc, argv);
MasterProxy master;
Oid simpleagentOid = Oid(enterprises_oid, "42");
master.register_subtree(simpleagentOid);
// NEW: Instantiate a table:
Table table(simpleagentOid + 2, &master);
// CHANGED: provide table when creating the counter:
QSharedPointer<SimpleCounter> counter(new SimpleCounter(&table));
Oid simpleCounterOid = simpleagentOid + 1 + 0;
master.add_variable(simpleCounterOid, counter);
// Code for sending notifications ---------------
NotificationSender sender(&master, simpleCounterOid, counter);
QTimer timer;
QObject::connect(&timer, SIGNAL(timeout()),
&sender, SLOT(sendNotification()));
timer.setInterval(1000); // 1000 milliseconds
timer.start();
// --------------------------------------------------
app.exec();
}

And that's all we need for our table. Next, let's try it out.

Compiling the Subagent

We didn't add implementation files in this tutorial, so no additional steps are needed for compiling. However, we have to recompile the whole thing. We know the following commands already from the previous tutorials:

moc-qt4 `pkg-config --cflags QtCore QtNetwork` -o moc_NotificationSender.cc NotificationSender.hpp
g++ -c moc_NotificationSender.cc `pkg-config --cflags QtCore QtNetwork`
g++ simpleagent.cpp -o simpleagent moc_NotificationSender.o `pkg-config --cflags --libs QtNetwork QtCore` -lagentxcpp

Now, the executable is ready to run.

Running the subagent

The subagent is run like described in the previous tutorials, simply by executing it:

./simpleagent

Now, let's add some entries to the table by querying the simpleCounter variable:

snmpget -v1 -c rw localhost SIMPLE-MIB::simpleCounter.0
snmpset -v1 -c rw localhost SIMPLE-MIB::simpleCounter.0 i 23

And finally we print the table using the snmptable command from the NET-SNMP package, which requires that the MIB is installed properly:

snmptable -v2c -c rw localhost SIMPLE-MIB::counterChangeTable

If the MIB is not properly installed, or in case of programming errors, the snmptable command might return nothing. The snmpwalk command can help out in those cases, because it prints the complete subtree of a given OID, regardless whether it is a table or not:

snmpwalk -v2c -c rw -On localhost 1.3.6.1.4.1.42.2

Now you know all you need to implement your own SNMP subagents. Use the API documentation to get more help on the individual classes.

Happy Hacking!