Integration of Net-SNMP into an event loop
Vincent Bernat
Net-SNMP comes with its own event loop based on the select()
system call. While you can build your program around it, you may
prefer to use an existing event loop, either a custom one or something
like libevent, libev or Twisted.
Own custom event loop#
Let’s start with the easiest case: you have written your own event
loop. This means you make a call to select()
, poll()
, epoll()
,
kqueue()
or something similar. All these functions take a set of
file descriptors and wait for any of them to become available in a
specified time frame.
Here is a typical use of select()
(adapted from Quagga)
readfd = m->readfd; writefd = m->writefd; exceptfd = m->exceptfd; timer_wait = thread_timer_wait (&m->timer); num = select (FD_SETSIZE, &readfd, &writefd, &exceptfd, timer_wait); if (num < 0) { if (errno == EINTR) continue; /* signal received - process it */ zlog_warn ("select() error: %s", safe_strerror (errno)); return NULL; } if (num == 0) { /* Timeout handling */ thread_timer_process (&m->timer); } if (num > 0) { thread_process_fd (&readfd); thread_process_fd (&writefd); }
thread_process_fd (fds)
function iterates on each file descriptor
fd
and executes the appropriate action if FD_ISSET (fds, fd)
is
true.
Integrating Net-SNMP into such an event loop is easy: Net-SNMP
provides the snmp_select_info()
function which alters a set of file
descriptors to insert its own. Here is the new code:
#if defined HAVE_SNMP struct timeval snmp_timer_wait; int snmpblock = 0; int fdsetsize; #endif /* ... */ #if defined HAVE_SNMP fdsetsize = FD_SETSIZE; snmpblock = 1; if (timer_wait) { snmpblock = 0; memcpy(&snmp_timer_wait, timer_wait, sizeof(struct timeval)); } snmp_select_info(&fdsetsize, &readfd, &snmp_timer_wait, &snmpblock); if (snmpblock == 0) timer_wait = &snmp_timer_wait; #endif num = select (FD_SETSIZE, &readfd, &writefd, &exceptfd, timer_wait); if (num < 0) { /* ... */ } #if defined HAVE_SNMP if (num > 0) snmp_read(&readfd); else if (num == 0) { snmp_timeout(); run_alarms(); } netsnmp_check_outstanding_agent_requests(); #endif /* ... */
snmp_select_info()
may modify the provided set of file descriptors
(and therefore, its size). It may also modify the provided timer in
case it needs to schedule an action before the original timeout.
snmpblock
is a tricky variable. While select()
can be called with
a timeout set to NULL
, this is not the case for snmp_select_info()
:
you have to pass a valid pointer. snmpblock
is set to 0 if the
provided timeout must be considered or to 1
otherwise. snmp_select_info()
will set snmpblock
to 0 if and only
if it alters the timeout.
Here are a three examples of integration. They are for a subagent but the code for a manager is the same:
Third-party event loop#
With a third party event loop, you don’t have access to the select()
system call anymore. Therefore, you cannot alter it with
snmp_select_info()
. Instead, at each iteration of the loop, a list
of SNMP related file descriptors is kept up-to-date with the help of
snmp_select_info()
: new ones are added and old ones are removed.
libevent#
Let’s assume that snmp_fds
is the list of current SNMP related
events.1 A function levent_snmp_update()
calls
snmp_select_info()
to update this list. Here is a partial
implementation (error handling removed and some declarations omitted,
look at the
complete version
from lldpd):
static void levent_snmp_update() { int maxfd = 0; int block = 1; FD_ZERO(&fdset); snmp_select_info(&maxfd, &fdset, &timeout, &block); /* We need to untrack any event whose FD is not in `fdset` anymore */ for (snmpfd = TAILQ_FIRST(snmp_fds); snmpfd; snmpfd = snmpfd_next) { snmpfd_next = TAILQ_NEXT(snmpfd, next); if (event_get_fd(snmpfd->ev) >= maxfd || (!FD_ISSET(event_get_fd(snmpfd->ev), &fdset))) { event_free(snmpfd->ev); TAILQ_REMOVE(snmp_fds, snmpfd, next); free(snmpfd); } else FD_CLR(event_get_fd(snmpfd->ev), &fdset); } /* Invariant: FD in `fdset` are not in list of FD */ for (int fd = 0; fd < maxfd; fd++) if (FD_ISSET(fd, &fdset)) { snmpfd->ev = event_new(base, fd, EV_READ | EV_PERSIST, levent_snmp_read, NULL); event_add(snmpfd->ev, NULL); TAILQ_INSERT_TAIL(snmp_fds, snmpfd, next); } /* If needed, handle timeout */ evtimer_add(snmp_timeout, block?NULL:&timeout); }
Then, replace the main loop (usually, a call to
event_base_dispatch()
) with this:
do { if (event_base_got_break(base) || event_base_got_exit(base)) break; netsnmp_check_outstanding_agent_requests(); levent_snmp_update(); } while (event_base_loop(base, EVLOOP_ONCE) == 0);
Here is how are defined the two callbacks:
static void levent_snmp_read(evutil_socket_t fd, short what, void *arg) { FD_ZERO(&fdset); FD_SET(fd, &fdset); snmp_read(&fdset); levent_snmp_update(); } static void levent_snmp_timeout(evutil_socket_t fd, short what, void *arg) { snmp_timeout(); run_alarms(); levent_snmp_update(); }
Twisted reactor#
As a second example, here is how to integrate Net-SNMP into
Twisted reactor. Twisted is an event-driven network programming
framework written in Python. It does not come with support for
SNMP.2 Because of the language mismatch, the integration
of Net-SNMP in Twisted needs to be done using a C extension or the
ctypes
module.3
Twisted event loop is called a reactor. Like for libevent, events
need to be registered. It is possible to register file descriptor-like
objects using a class implementing a handful of methods. Here is the
implementation of such a class (adapted from PyNet-SNMP, a subproject
of Zenoss using the ctypes
module):
class SnmpReader: "Respond to input events" implements(IReadDescriptor) def __init__(self, fd): self.fd = fd def doRead(self): netsnmp.snmp_read(self.fd) def fileno(self): return self.fd
Like for libevent, we have a function to update the list of SNMP
related events (stored as a mapping between file descriptors and
SnmpReader
instances):
class Timer(object): callLater = None timer = Timer() fdMap = {} def updateReactor(): "Add/remove event handlers for SNMP file descriptors and timers" fds, t = netsnmp.snmp_select_info() for fd in fds: if fd not in fdMap: reader = SnmpReader(fd) fdMap[fd] = reader reactor.addReader(reader) current = Set(fdMap.keys()) need = Set(fds) doomed = current - need for d in doomed: reactor.removeReader(fdMap[d]) del fdMap[d] if timer.callLater: timer.callLater.cancel() timer.callLater = None if t is not None: timer.callLater = reactor.callLater(t, checkTimeouts)
Contrary to libevent, we cannot alter the main loop to call
updateReactor()
at each iteration. Therefore, it must be called
after each SNMP related function.
For another example, have a look at my equivalent implementation as a C extension.
Miscellaneous#
Lack of asynchronicity#
Integrating Net-SNMP into an event-based program is not risk-free. I have written a fairly comprehensive article on the lack of asynchronicity in Net-SNMP AgentX protocol implementation. I urge you to read it if you want to integrate a SNMP subagent into an existing program.
On the manager side, you will get similar drawbacks when using
SNMPv3. To retrieve or manipulate management information using SNMPv3,
it is necessary to know the identifier of the remote SNMP protocol
engine. This identifier can be configured directly on each manager but
it is usually discovered by querying
SNMP-FRAMEWORK-MIB::snmpEngineID
, as described in RFC 5343.
Unfortunately, this discovery is done synchronously by
Net-SNMP. There is a bug report for this issue but it
seems difficult to fix. I have not been able to come up with a proper
patch but I have described a workaround around
snmp_sess_async_send()
.
There is no such problem with SNMPv2.
Limitation of file descriptor sets#
snmp_select_info()
and snmp_read()
uses the fd_set
type to
handle a set of file descriptors. This type should be manipulated with
FD_CLR()
, FD_ISSET()
, FD_SET()
and FD_ZERO()
. They may be
defined as functions but they usually are macros. Moreover, no file
descriptor greater than FD_SETSIZE
(which is usually set to 1024)
can be handled with the fd_set
type. This means that if you have a
file descriptor greater than 1024, you won’t be able to use
snmp_select_info()
with it.
Starting from Net-SNMP 5.5, you can use snmp_select_info2()
and
snmp_read2()
instead of snmp_select_info()
and snmp_read()
. They
use the netsnmp_large_fd_set
.
If you want to keep compatibility with Net-SNMP 5.4, here is another
twist. The fd_set
type is usually a fixed-size array of long
integers. FD_CLR()
, FD_ISSET()
and FD_SET()
are
size-independant. The size only matters for FD_ZERO()
which is not
used by either snmp_select_info()
or snmp_read()
. You can
therefore allocate a larger fd_set
. A common way is to compile your
program with -D__FD_SETSIZE=4096
. You can still use FD_ZERO()
yourself. However, this is not portable (GNU C Library only). Another
way is to allocate your own array of long integers and cast it to
fd_set
. You’ll have to redefine FD_ZERO()
:
typedef struct { long int fds_bits[4096/sizeof(long int)]; } my_fdset; #undef FD_ZERO #define FD_ZERO(fdset) memset((fdset), 0, sizeof(my_fdset)) { /* ... */ my_fdset fdset; FD_ZERO(&fdset); /* ... */ rc = snmp_select_info(&n, (fd_set *)&fdset, &timeout, &block); /* ... */ }
Threads#
Net-SNMP provides two API:
- the traditional session API which is not thread-safe and
- the single session API.
In the above examples, I have used the traditional session API. If you
are using threads, you need to ensure that all SNMP operations are
done in a single thread. Otherwise, you need to adapt the examples to
use the single session API. snmp_select_info()
is part of the
traditional session API. You should replace it with
snmp_sess_select_info()
and keep a list of SNMP sessions.
This API is not available for the agent side of Net-SNMP.
-
The event list is global but some code could be added to bind a different list for each base in case you use different bases. This is not needed in the case of lldpd. ↩︎
-
TwistedSNMP is an SNMP protocol implementation for Twisted based on PySNMP but is not maintained anymore. PySNMP comes with examples on how to integrate with Twisted. ↩︎