Snimpy : SNMP & Python

Vincent Bernat

Souvent considéré comme désuet, SNMP est toujours omniprésent pour interagir avec des équipements réseau. Pour la supervision, il permet d’exposer diverses métriques telles que les compteurs liés aux interfaces réseau. Il permet également d’interagir sur la configuration des équipements.

Les variables exposées par les agents SNMP (serveurs) sont contenues dans une base hiérarchique appelée Management Information Base (ou MIB1). Chaque entrée est identifiée par un OID. En interrogeant un OID spécifique, un manager (client) obtient la valeur associée à la variable.

Par exemple, IF-MIB est une MIB présentée dans la RFC 2863. Elle contient les objets utilisés pour gérer les interfaces réseau. L’un d’eux est ifTable dont chaque ligne représente une interface réseau logique : son nom, ses caractéristiques et divers compteurs.

ifIndex ifDescr ifPhysAddress ifOperStatus ifOutOctets
1 lo up 545721741
2 eth0 0:18:f3:3:4e:4 up 78875421
3 eth1 0:18:f3:3:4e:5 down 0

ifTable est indexée par sa première colonne, ifIndex. Pour obtenir le statut opérationnel de la seconde interface, il suffit d’interroger IF-MIB::ifOperStatus.2 que l’on peut traduire vers l’OID .1.3.6.1.2.1.2.2.1.8.2 à l’aide des informations contenues dans la définition de la MIB.

Automatiser SNMP#

Un agent SNMP peut fournir de nombreuses informations intéressantes :

Des outils tels que snmpget et snmpwalk permettent de collecter manuellement ces informations :

$ snmpwalk -v 2c -c public localhost IF-MIB::ifDescr
IF-MIB::ifDescr.1 = STRING: lo
IF-MIB::ifDescr.2 = STRING: eth0
IF-MIB::ifDescr.3 = STRING: eth1

Toutefois, il est assez fastidieux de construire des scripts robustes à l’aide de ceux-ci. Par exemple, voici un script pour obtenir les descriptions de toutes les interfaces réseau actives ainsi que le nombre total d’octets transmis :

#!/bin/sh

set -e

host="${1:-localhost}"
community="${2:-public}"
args="-v2c -c $community $host"

for idx in $(snmpwalk -Ov -OQ $args IF-MIB::ifIndex); do
    descr=$(snmpget -Ov -OQ $args IF-MIB::ifDescr.$idx)
    oper=$(snmpget -Ov -OQ $args IF-MIB::ifOperStatus.$idx)
    in=$(snmpget -Ov -OQ $args IF-MIB::ifInOctets.$idx)
    out=$(snmpget -Ov -OQ $args IF-MIB::ifOutOctets.$idx)
    [ x"$descr" != x"lo" ] || continue
    [ x"$oper" = x"up" ] || continue
    echo $descr $in $out
done

Heureusement, il existe des extensions SNMP pour la plupart des langages. À titre d’exemple, voici comment pourrait être réécrit le script ci-dessus en exploitant l’extension SNMP pour Python livrée avec Net-⁠SNMP :

import argparse
import netsnmp

parser = argparse.ArgumentParser()
parser.add_argument(
    "host", default="localhost", nargs="?",
    help="Agent to retrieve variables from")
parser.add_argument(
    "community", default="public", nargs="?",
    help="Community to query the agent")
options = parser.parse_args()
args = {
    "Version": 2,
    "DestHost": options.host,
    "Community": options.community
}

for idx in netsnmp.snmpwalk(netsnmp.Varbind("IF-MIB::ifIndex"),
                            **args):
    descr, oper, cin, cout = netsnmp.snmpget(
        netsnmp.Varbind("IF-MIB::ifDescr", idx),
        netsnmp.Varbind("IF-MIB::ifOperStatus", idx),
        netsnmp.Varbind("IF-MIB::ifInOctets", idx),
        netsnmp.Varbind("IF-MIB::ifOutOctets", idx),
        **args)
    assert(descr is not None and
           cin is not None and
           cout is not None) # ❶
    if descr == "lo":
        continue
    if oper != "1": # ❷
        continue
    print("{} {} {}".format(descr, cin, cout))

Cette extension a plusieurs défauts importants :

  1. Tout est présenté sous forme de chaînes de caractères comme on peut le voir en ❷.
  2. La gestion des erreurs est inexistante. En cas d’erreur sur le nom d’une variable, le message snmp_build: unknown failure s’affiche mais aucune exception n’est levée. Si une variable n’existe pas, les fonctions retournent None. Voir en ❶.

L’impossibilité de gérer les erreurs rend cette extension dangereuse. On ne peut imaginer effectuer des modifications importantes sur la base des réponses retournées. Une vérification oubliée et un script peut enchaîner des actions inappropriées !

Snimpy#

N’ayant pas trouvé d’extension fiable pour Python, j’ai donc décidé d’écrire Snimpy avec deux principaux objectifs :

  1. S’appuyer sur les informations contenues dans les MIB pour fournir une interface pythonique.
  2. Toute erreur doit se traduire en une exception.

Voici comment le précédent script pourrait être réécrit :

#!/usr/bin/env snimpy

import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
    "host", default="localhost", nargs="?",
    help="Agent to retrieve variables from")
parser.add_argument(
    "community", default="public", nargs="?",
    help="Community to query the agent")
options = parser.parse_args()

m = M(options.host, options.community, 2)

load("IF-MIB")
for idx in m.ifDescr:
    if m.ifDescr[idx] == "lo":
        continue
    if m.ifOperStatus[idx] != "up":
        continue
    print("{} {} {}".format(m.ifDescr[idx],
                            m.ifInOctets[idx],
                            m.ifOutOctets[idx]))

Une alternative est d’utiliser les listes en compréhension :

load("IF-MIB")
print("\n".join([
    "{} {} {}".format(
        m.ifDescr[idx],
        m.ifInOctets[idx],
        m.ifOutOctets[idx])
    for idx in m.ifDescr
    if m.ifDescr[idx] != "lo"
    and m.ifOperStatus[idx] == "up" ]))

Voici un autre exemple pour obtenir la table de routage de l’agent :

load("IP-FORWARD-MIB")
m=M("localhost", "public", 2)
routes = m.ipCidrRouteNextHop
for x in routes:
    net, netmask, tos, src = x
    print("{:>15s}/{:<15s} via {:<15s} src {:<15s}".format(
        net, netmask, routes[x], src))

IP-FORWARD-MIB::ipCidrRouteNextHop est issue d’une table utilisant un index composé. Cependant, son utilisation semble toujours naturelle.

Techniquement, les requêtes SNMP sont gérées par PySNMP et les MIB sont interprétées à l’aide de libsmi2. Snimpy fonctionne à la fois avec Python 2 et Python 3. Pour plus d’informations, jetez un œil sur la documentation de Snimpy.


  1. Une MIB est définie à l’aide de SMI, un sous-ensemble de ASN.1. Toutefois, il n’est pas rare d’appeler également « MIB » cette définition. ↩︎

  2. À l’heure actuelle, il n’existe malheureusement pas d’analyseur robuste pour SMI écrit en Python. Par exemple, PySNMP s’appuie sur l’outil smidump livré avec libsmi. PySNMP s’appuie désormais sur PySMI. Snimpy exploite libsmi via CFFI↩︎