Intégration de Net-⁠SNMP dans une boucle d’évènements

Vincent Bernat

Net-⁠SNMP utilise sa propre boucle d’évènements basée sur l’appel système select(). Bien qu’il soit possible de l’utiliser dans un programme tiers, on lui préfère généralement sa propre boucle ou des bibliothèques telles que libevent, libev ou Twisted.

Boucle d’évènements propre#

Commençons par le cas le plus simple : vous avez mis en place votre propre boucle d’évènements à l’aide de select(), poll(), epoll(), kqueue() ou quelque chose de similaire. Toutes ces fonctions prennent un ensemble de descripteurs de fichiers et attendent un certain temps que l’un d’eux devienne actif.

Voici un exemple typique de l’utilisation de select(), inspiré de 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);
  }

La fonction thread_process_fd (fds) va exécuter l’action associée à chaque descripteur fd si la condition FD_ISSET (fds, fd) est vérifiée.

Intégrer Net-⁠SNMP dans une telle boucle est relativement simple : on dispose de la fonction snmp_select_info() qui permet d’intégrer les descripteurs de fichiers liés à SNMP dans un ensemble existant. Voici le code adapté :

#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

/* ... */

L’appel à snmp_select_info() peut modifier l’ensemble des descripteurs de fichiers (ainsi que sa taille). Il peut également modifier le timer fourni dans le cas où une action doit être exécutée avant que le timer original ne soit écoulé.

La compréhension de la variable snmpblock est importante. Alors que select() peut être invoquée avec son dernier paramètre à NULL, ce n’est pas le cas de snmp_select_info() : il faut toujours passer un pointeur valide. snmpblock doit être mis à 1 si le timer fourni doit être ignoré (ce qui est le cas si on avait appelé select() avec NULL) ou 0 dans le cas contraire. snmp_select_info() placera snmpblock à 0 si et seulement si il modifie le timer fourni.

Voici trois autres exemples d’intégration. Il s’agit dans tous les cas de sous-agents, mais le code peut être réutilisé tel quel pour un manager :

Boucle d’évènements tierce#

Avec une boucle d’évènements tierce, il n’est plus possible d’accéder directement à l’appel système select() afin de l’altérer avec snmp_select_info(). Au lieu de cela, il convient, à chaque itération, de maintenir une liste des descripteurs de fichiers liés à SNMP en s’aidant de snmp_select_info(): les nouveaux descripteurs sont ajoutés à la boucle d’évènements et les anciens sont retirés.

libevent#

Supposons que snmp_fds est la liste des évènements liés à SNMP que nous souhaitons tenir à jour1. Voici un exemple de fonction pour remplir cette tâche. Il s’agit d’une version partielle omettant certaines déclarations et la gestion des erreurs. Reportez-vous ci besoin à la version complète issue de 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);
}

La boucle principale est généralement un appel à event_base_dispatch(). On la remplace par cette version :

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);

Voici comme sont définies les deux fonctions attachées aux évènements mis en place:

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#

Comme second exemple, nous allons voir comment intégrer Net-⁠SNMP dans le réacteur de Twisted. Twisted est un ensemble de composants pour la programmation réseau basé sur le paradigme évènementiel. Fourni avec le support de nombreux protocoles, il n’y a cependant rien pour SNMP2. Twisted étant écrit en Python, l’interface avec Net-⁠SNMP doit se faire à l’aide d’une extension en C ou en utilisant le module ctypes3.

La boucle d’évènements de Twisted est appelée le réacteur. Comme pour libevent, chaque évènement doit etre y être enregistré. Il est possible d’y inscrire des descripteurs de fichiers en implémentant une classe respectant une certaine interface. Voici un exemple d’implémentation tiré de PyNet-⁠SNMP, un sous-projet de Zenoss, utilisant le module ctypes :

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

Comme pour libevent, nous avons besoin d’une fonction pour mettre à jour la liste des évènements liés à SNMP (il s’agit ici d’un dictionnaire associant un descripteur de fichier avec l’instance SnmpReader correspondante) :

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)

Contrairement à libevent, nous ne pouvons pas modifier la boucle principale pour appeler updateReactor() à chaque itération. Il est donc nécessaire d’appeler cette fonction après chaque appel d’une fonction liée à SNMP.

Pour un autre exemple, le lecteur intéressé pourra consulter mon implémentation sous forme d’extension C.

Divers#

Asynchronicité défaillante#

Intégrer Net-⁠SNMP dans un programme reposant sur les évènements n’est pas sans risque. Le lecteur souhaitant intégrer la fonctionnalité d’agent SNMP dans un programme existant est invité à lire de toute urgence mon article sur le manque d’asynchronicité dans l’implémentation de Net-⁠SNMP du protocole AgentX.

Côté manager, il y a des limitations similaires en SNMPv3. Pour interroger un agent en SNMPv3, il est nécessaire de connaître l’identifiant de l’agent distant. Celui-ci est habituellement découvert en interrogeant SNMP-FRAMEWORK-MIB::‌snmpEngineID, comme décrit dans la RFC 5343.

Malheureusement, avec Net-⁠SNMP, cette découverte se fait de manière synchrone. Il existe un rapport de bug pour ce problème mais le correctif n’existe pas. Sur la liste de diffusion de Net-⁠SNMP, j’ai décrit une méthode de contournement autour de la fonction snmp_sess_async_send().

Un tel problème n’existe pas avec SNMPv2.

Limitation sur le nombre de descripteurs de fichiers#

snmp_select_info() et snmp_read() font usage du type fd_set pour gérer les ensembles de descripteurs de fichiers. Ce type doit être manipulé à l’aide de FD_CLR(), FD_ISSET(), FD_SET() et FD_ZERO(). Ces derniers peuvent être définis comme fonctions mais sont habituellement des macros. De plus, fd_set ne peut pas contenir de descripteurs de fichiers supérieurs à FD_SETSIZE (dont la valeur est habituellement de 1024). Il n’est donc pas possible d’utiliser snmp_select_info() avec des descripteurs de fichiers supérieurs à 1024.

À partir de Net-⁠SNMP 5.5, il est possible d’utiliser snmp_select_info2() et snmp_read2() au lieu de snmp_select_info() et snmp_read(). Ils utilisent le type netsnmp_large_fd_set qui doit être manipulé avec des macros comme NETSNMP_LARGE_FD_SET().

Afin de garder la compatibilité avec Net-⁠SNMP 5.4, il existe une autre possibilité. Le type fd_set est souvent un tableau d’entiers longs de taille fixe. Les macros FD_CLR(), FD_ISSET() et FD_SET() sont indépendantes de la taille de celui-ci à la différence de la macro FD_ZERO(). Ni snmp_select_info() ni snmp_read() n’utilisent FD_ZERO(). Il est donc possible d’utiliser un fd_set plus grand. Une façon commune de faire est de compiler votre programme avec -D__FD_SETSIZE=4096. Il est alors toujours possible d’utiliser FD_ZERO() sur les fd_set que vous créez. Cependant, cela ne fonctionne pas partout. Une autre méthode proche est d’allouer un tableau d’entiers et de le transtyper en un fd_set. Il faut alors redéfinir 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 dispose de deux API :

  • l’API traditionnelle et
  • l’API « single session ».

Les exemples ci-dessus ont fait usage de l’API traditionnelle. Celle-ci ne peut être utilisée que si toutes les opérations liées à SNMP sont faites dans un même fil d’exécution. Dans le cas contraire, il faut utiliser la seconde API. snmp_select_info() doit par exemple être renplacé par snmp_sess_select_info(), ce qui oblige à garder une liste des sessions SNMP utilisées.

La partie agent de Net-⁠SNMP n’est disponible que via la première API.


  1. La liste des évènements liés à SNMP est globale mais il est possible de maintenir une liste par base si nécessaire. Ce n’est pas nécessaire dans le cas de lldpd qui n’utilise qu’une seule base. ↩︎

  2. TwistedSNMP est un implémentation de SNMP pour Twisted basée sur PySNMP. Elle n’est cependant plus maintenue. PySNMP est livré avec quelques exemples d’intégration avec Twisted↩︎

  3. Actuellement, il serait préférable d’utiliser CFFI↩︎