Initiation à SNMP avec Python :
PySNMP (Partie 1) - Le protocole et les
commandes
Par Gaël Pegliasco — publié 03/03/2016, édité le 22/04/2016
SNMP est un protocole de supervision réseau universellement répandu.
C'est le standard utilisé par la quasi totalité des équipements réseaux. Il
permet de superviser (interrogation et modification) tous les types de
matériels, allant du routeur à l'imprimante, et même certaines machines
à café connectées.
Introduction - Première partie
Ce tutoriel vous propose de découvrir la librairie Python PySNMP
permettant de dialoguer avec tout matériel compatible avec le
protocole du même nom.
Toutefois, avant de se lancer dans une présentation technique
décrivant comment exécuter une requête « get » ou « set » en SNMP
v2 ou v3, nous vous proposons une présentation des concepts de ce
protocole. Ceci vous permettra de mieux comprendre les tenants et
aboutissants de la librairie afin de vous mener vers le Graal absolu
de sa complète maîtrise.
Dans cette première partie nous présentons le protocole SNMP et
l'utilisation de commandes système qui vous permettront de toucher
du doigt les concepts que nous exposons.
A la fin de ce tutorial en 4 parties vous serez normalement capable :
D'exécuter des requêtes GET/SET
Envoyer des traps
Créer des sondes pour des outils de supervision
comme Nagios, Centreon et consorts.
Créer un agent SNMP
Avant de continuer la lecture de cet article, si vous débutez sur le
sujet, je vous invite à lire cette introduction à la supervision qui
vous donnera les bases techniques pour comprendre ce métier ainsi
que ses enjeux et les outils à votre disposition. Cette lecture vous
aidera à mieux situer la place de SNMP dans cet univers. Le
document est très orienté Nagios et dérivés, mais il vous fournira
tous les éléments de base pour bien appréhender cette discipline.
Plan
Présentation du protocole
SNMP
Les MIB
ASN1
Dialoguer avec des agents en utilisant des
commandes système
PySNMP
Les éléments de base
Les requêtes
Les tables
Utiliser ses MIBs avec un agent
Qu'est-ce que SNMP ?
SNMP signifie « Simple Network Management Protocol »
C'est un protocole de gestion de réseau.
Concrètement il permet de superviser l'état des équipements
réseaux ou d'un parc informatique, aussi bien le matériel que les
services :
Routeurs, switchs
Stations de travail
Imprimantes
Services (messagerie, ftp, ssh, http, processus, mémoire…)
Par supervision il faut comprendre « obtenir des informations » sur
l'état de ces « éléments ». Mais ce protocole permet aussi
d'effectuer des actions de maintenance comme redémarrer un
service ou une machine, nettoyer les têtes de lecture d'une
imprimante, ...
Fonctionnement de SNMP
SNMP se base sur l'idée qu'un système de supervision de réseau se
compose :
De nœuds à administrer (Managed Nodes), contenant
chacun un agent
L'agent est le service (logiciel) qui dialogue avec les
managers pour échanger de l'information sur l'état du nœud
D'au moins une station d'administration, le manager
(Network Management Station)
D'un protocole permettant d'échanger de l'information
entre les agents et le NMS, ce protocole est SNMP
Le site wikipédia offre une présentation succinte de ce dernier
dans sa version française. La version anglaise du même article
est beaucoup plus complète et cite notamment l'ensemble des RFC
qui le décrivent.
Manager et agents (source PySNMP)
Comment cela fonctionne ?
Le manager envoie des requêtes de type « GET » aux
agents pour récupérer de l'information sur un service/une
machine via le protocole SNMP
Il peut aussi envoyer des requêtes « SET » pour modifier
l'état des services et/ou de la machine gérés par l'agent
Les agents peuvent envoyer des requêtes de type « TRAP»
au manager pour signaler un dysfonctionnement
Les agents peuvent être regroupés au travers d'un « agent
maître »
Les informations accessibles et fournies par les agents
sont organisées dans une base « virtuelle » appelée MIB.
Cette base est normalisée.
Echange de requêtes entre le manager et les agents
(source wikipédia)
Et c'est tout. C'est simple ?
Pourquoi utiliser SNMP ?
C'est un protocole très ancien et supporté par quasiment
tous les constructeurs de matériels réseau.
Depuis les versions 2 et 3 il est sécurisé, offrant un accès
restreint à certaines informations et s'appuie sur des
protocoles de chiffrement comme SSH et TLS (mais avec
ses propres librairies et outils internes).
L'information contenue dans les MIBs est normalisée, c'est
à dire que quelque soit l'appareil et son constructeur une
même information sera accessible au même endroit et de
la même manière. Et cela c'est formidable :
« Vous êtes administrateur réseau, vous voulez connaître
la mémoire totale et disponible d'une machine, vous
l'obtiendrez de la même manière quelque soit l'OS
(mac/windows/unix/android) et le matériel utilisé (AMD,
ARM, Intel, …) ». N'est-ce pas fabuleux ?
Il est simple (les agents sont légers, la complexité est
déportée au niveau des managers)
Il est indépendant de l'architecture réseau et des éléments
(PC, Imprimantes, routeurs, …)
Il est extensible, on peut personnaliser les bases
d'informations en surcroît de ce qu'offre le standard
Il est Robuste. Il s'appuie sur UDP ou TCP :
Il fonctionnera encore lorsque le réseau aura de graves
problèmes d'engorgement : une trame d'information UDP
c'est léger.
Est-ce si bien ?
Globalement c'est plutôt top, et surtout il a peu de concurrents
matures et aussi répandus, mais :
Des fois quand même il n'est pas très simple : Les MIBs
sont « lisibles par l'homme » mais pour reprendre les
propos d'un de mes collègues, des fois, ceux qui y arrivent
ne se sont pas tous vraiment humains.
Le protocole est simple mais son implémentation peu
s'avérer bien plus complexe.
La version 3 a implémenté sa propre couche SSH plutôt que
de faire comme HTTPS (du HTTP over SSH). Ceci rend plus
pénible les communications : il n'y a pas d'auto-négociation
pour le chiffrement et vous devez vous-même fournir 5
informations pour créer une connexion sécurisée.
Certains concurrents commencent à voir le jour
comme WMI et offrent un langage d'interrogation plus
riche (WQL).
Historique
La première version du protocole date de 1989
Elle n'offre pas de sécurité et utilise la MIB version 1 qui
reste assez simple
La seconde version a été publiée en 1993
Elle apporte plus de sécurité tant au niveau de
l'authentification que du chiffrement
Elle permet l’administration distribuée
Elle apporte une version plus riche de la MIB( MIB-
2)
La version 3 a été publiée en 1998
Elle apporte des transactions chiffrées via SSH
Elle est plus modulaire
Dans les faits
La version 2 a eu du mal à démarrer et plusieurs « releases
» ont été faites. C'est la version « 2c » qui a su s'imposer.
Je vous conseille à ce sujet la lecture de
cette présentation de SNMP qui décrit un peu plus ces
difficultés de démarrage ainsi que la présentation de
PySNMP qui reprend certains de ces points.
La version 3, tout comme la v2 a eu du mal à s'imposer elle
aussi. Elle n'est pas supportée par tous les matériels
aujourd'hui même si cela s'est bien amélioré. Les
problèmes de sécurité étant devenus vraiment sensibles
aujourd'hui elle est la version recommandée.
SNMP domine le monde TCP/IP et il est le standard
recommandé depuis mai 1990
Les MIBs
Avant de pouvoir lancer nos premières requêtes SNMP il convient de
comprendre ce qu'est une MIB.
La Management Information Base est une base de données «
virtuelle » permettant d'organiser de manière hiérarchique les
informations fournies par les agents sur les équipements.
Ce n'est pas une base de données à proprement parler comme une
base relationnelle. Elle n'existe qu'au travers de l'agent qui est libre
de l'implémenter comme il le souhaite. Quand l'agent ne fonctionne
pas, il n'y a pas de données dans la MIB. Seule la description de la
MIB (un fichier texte généralement) existe.
Une MIB décrit en fait comment numéroter/accéder à l'information.
C'est l'agent (le logiciel) qui stocke (mais on devrait dire gère) cette
information, laquelle est le plus souvent « volatile », comme il le
veut.
Il est très important de bien comprendre le point ci-dessus, c'est ce
qui gêne souvent la compréhension des MIBs par des nouveaux
arrivants sur ce protocole.
Prenons l'exemple du nombre de processus actifs sur une machine.
Celui-ci est identifié de manière unique par la MIB-II. Mais cette
information est demandée par l'agent au Système d'Exploitation au
moment ou il reçoit une requête pour cette dernière, car cette
donnée est changeante à chaque instant. Elle n'est donc pas «
stockée » par l'OS. Elle n'est pas accessible au travers d'une
requête SQL. Elle est fournie quand on la demande, et tant que
l'agent ne l'a pas demandée lui-même à l'OS elle n'est pas vraiment
disponible ou n'a pas encore d'existence :
Le manager demande l'information à l'agent
L'agent reçoit la demande et appelle une primitive système
pour l'obtenir
L'agent retourne l'information au manager
C'est une information « volatile » qui n'est pas vraiment stockable.
Beaucoup d'informations issues des MIBs sont de cette nature,
d'autres sont parfois stockées quelque part, comme le responsable
d'une machine, le port d'un service ou encore l'état d'une ligne
électrique ou d'une diode (éteinte, allumée). Selon les
constructeurs/OS ces informations sont stockées physiquement
différemment, mais la MIB les normalise pour la grande majorité afin
qu'une même information soit toujours demandée de la même
manière par le manager. C'est à l'agent de savoir ou aller la
chercher selon le matériel/service géré.
Et cela apporte beaucoup de facilités dans l'écriture de
sondes/outils de supervisions. C'est une des grandes forces de ce
standard.
MIB-II
Une des MIB les plus connues est la MIB-II, décrite par la RFC 1213.
Elle est mise en œuvre dans quasiment tous les équipements TCP/IP.
Elle compte dix groupes :
system
interfaces
Address Translation
IP
ICMP
TCP
UDP
EGP"
transmission
et SNMP
C'est à dire que toute information que vous pourriez demander
concernant la MIB-II sera classée dans l'un de ces groupes.
Des exemples d'informations que vous pourriez retrouver dans ces
ensembles, sont:
la liste des processus (avec informations mémoire et CPU) ;
les débits réseaux ;
le load average ;
l'utilisation mémoire ;
le contact du système, l'uptime ;
l'utilisation de l'espace disque ;
les tables de routages.
Exemple de description d'une MIB
Enfin, les mibs sont décrites dans un « langage » appelé ASN.1
L'exemple ci-dessous présente l'information «description d'interface
(réseau) » telle que décrite en ASN.1 :
ifDescr OBJECT-TYPE
SYNTAX DisplayString (SIZE (0..255))
ACCESS read-only
STATUS mandatory
DESCRIPTION
"A textual string containing information about the
interface. This string should include the name of
the manufacturer, the product name and the version
of the hardware interface."
::= { ifEntry 2 }
La lecture de la définition de cette donnée est assez
compréhensible par elle-même.
Nous indiquons ici :
son nom, « ifDescr »
son type, « DisplayString », une chaîne de 255 caractères
maximum
son mode d'accès, « lecture seule »
son état, « obligatoire »
enfin son emplacement dans le nœud parent « ifEntry » :
2ème sous-élément du nœud « ifEntry »
Organisation de l'information dans les MIBs
Pour bien comprendre cette description « hiérarchique » de la
donnée « ifDescr », voici un graphe représentant la hiérarchie
principale de toutes les MIBs :
Hiérarchie principale d'une MIB (Source wikipédia)
Le graphe ci-dessus présente la structure hiérarchique des MIBs.
Sous le nœud racine, noté « . » se trouvent 3 informations
numérotées :
CCITT avec le numéro 0
ISO avec le numéro 1
ISO – CCITT avec le numéro 2
Sous le nœud ISO se trouvent 4 informations, elles aussi numérotées
:
standard, avec le numéro 0
registration authority, avec le numéro 1
member body, avec le numéro 2
organization, avec le numéro 3
Le chemin d'accès à l'information « organization » située sous « ISO
», elle-même située sous la racine sera « .1.3 » ou encore
« .ISO.organization »
Et ainsi de suite.
Par exemple l'accès à l'information « MIB-2 » se fera via le chemin
« .1.3.6.1.2.1 » ou encore
« .ISO.organization.DoD.Internet.management.MIB-2 »
Et voilà, vous savez tout sur les MIBs : elles définissent des
informations en ASN.1, lesquelles sont accessibles via un chemin
exprimé avec des numéros ou les noms des informations parentes.
Les OID
Les informations sont donc numérotées selon leur ordre dans l'arbre
de la MIB, ces numéros sont appelés OID (Object IDentifier).
Voici quelques exemples d'OIDs :
.1.3.6.1.2.1.1 system : uptime, contact, nom
.1.3.6.1.2.1.2 interfaces: interfaces réseau
.1.3.6.1.2.1.4 ip: ip routing, etc
.1.3.6.1.2.1.5 icmp: icmp errors, discards... (icmp c'est par
exemple le ping)
.1.3.6.1.2.1.6/7 tcp ou udp: états des connexions tcp ou udp
La question devient maintenant « comment trouver toutes les
informations disponibles dans une MIB ? »
Les réponses seront :
En lisant les standards et normes
associés: https://tools.ietf.org/html/rfc1213
En exécutant des requêtes avec des outils SNMP qui
permettent de parcourir toute l'arborescence d'une MIB.
En parcourant ces MIBs via des sites Internet qui ont
l'amabilité de les représenter facilement, comme le site IP
MONITOR.
On observera sur ce dernier que le chemin complet à l'information «
ifDescr » est fournit par l'OID « 1.3.6.1.2.1.2.2.1.2 ».
C'est ce que va nous permettre de faire PySNMP : envoyer des
requêtes demandant la valeur d'un nœud (une information) située à
un emplacement précis dans une MIB ou modifiant cette valeur.
Et c'est tout. Ou presque. Y voyez-vous un peu plus clair ?
La pratique approche.
Un univers de MIBs
Il existe tout plein de MIBs :
Selon le matériel (routeur, imprimante) les informations
sont différentes.
Pour cette raison, certains équipements ou constructeurs
fournissent donc leurs propres MIB définissant une branche
dans l'arbre des OIDs, avec un numéro officiel défini auprès
de l'IANA (Internet Assigned Numbers Authority) .
On retrouvera par exemple CISCO à cette branche de
l'arbre: iso.org.dod.internet.private.entreprise.cisco =
1.3.6.1.4.1.9
A cet emplacement, CISCO est libre de proposer les MIBs
décrivant les informations de ses propres matériels comme
il l'entend. Beaucoup d'autres sociétés en font autant,
comme Alcatel/Nokia pour ne citer qu'eux.
Le site IP Monitor présente de nombreuses bases (mib) pour
différents types d'informations/matériels/services que vous pouvez
consulter.
Les communautés
L'accès aux informations d'une MIB est filtré par ce que l'on appelle
la « communauté », sauf version 3 du protocole.
La « communauté » est une sorte de mot partagé, « public » par
défaut.
« public » peut-être rapproché du login « anonymous » disponible sur
la plupart des serveurs ftp.
Une demande d'information est toujours exécutée via une
communauté.
Selon la communauté utilisée, les informations reçues seront
différentes :
La communauté « public » donne accès au niveau « read-
only ».
Il existe un niveau privé en lecture/écriture ( communauté «
private ») qui est le plus souvent désactivé (Il donne accès
à toute la configuration système en écriture)
Donc, quand vous souhaitez accéder à/ou modifier une information
d'un agent, vous :
envoyez une requête GET/SET à l'agent
en fournissant le chemin de l'information désirée, son OID
en fournissant la communauté utilisée (« private » pour les
modifications)
en fournissant la nouvelle valeur de la donnée en cas de
modification
en fournissant l'adresse IP/nom de domaine ou s'exécute
l'agent
le port utilisé est par défaut le port 161
Utilisation du protocole en ligne de commandes
Pour bien comprendre ce que permet ce protocole, je vous propose
d'exécuter les commandes de bases avec des outils systèmes.
Ceci permettra de toucher du doigt la mécanique sous-jacente et de
vérifier que les agents comprennent bien vos requêtes avant de les
écrire en Python.
Les commandes
Avant de lancer nos premières commandes, regardons les types de
requêtes que le protocole propose pour permettre aux manager et
agents de s'échanger de l'information :
Get: Demande de la valeur d'un OID par le manager à
l'agent
L'agent retourne l'information via un message de type
Response
GetNext : Demande de l'information suivante dans la MIB
Permet de parcourir une MIB sans connaître ce qu'elle
contient
Set : Modifie la valeur associée à un OID
Permet de changer la configuration ou modifier l'état d'un
hôte
GetBulk : Demande tout un ensemble d'infos en une seule
requête.
Evite de multiplier les Get et GetNext
Response : message envoyé par l'agent au manager
contenant l'information demandée
Trap : Message envoyé par l'agent au manager pour
signaler un problème
Inform : Message envoyé par le manager pour confirmer la
réception du trap
Pré-requis
Pour lancer nos premières commandes, il convient de disposer d'un
agent et d'un manager.
Je vous conseille cet excellent tutorial pour une telle installation
sur une machine Linux (type debian).
Pour le manager, vous pouvez installer les outils snmp et mibs de
base via ces commandes
sudo apt-get install snmp snmp-mibs-downloader
sudo apt-get install smitools libsmi
Ensuite il vous faut un agent.
Pour le trouver vous pouvez :
Essayer avec une imprimante réseau ou une machine
possédant une adresse IP
Généralement elles sont compatibles avec le protocole
SNMP
Installer votre propre agent en suivant le tutoriel cité
précédemment
Lancer vos commandes vers l'agent « demo.snmplabs.com
» librement accessible via Internet, ce que nous ferons.
Enfin, si vous ne disposez pas de la possibilité d'installer les
commandes SNMP sous votre Linux, sachez que la librairie PySNMP
propose des scripts équivalents en pur Python que vous pouvez
installer avec la commande « pip install pysnmp-apps ».
La commande « GET »
Elle accepte plusieurs paramètres dont :
« -v » : version du protocole
« -c » : communauté
« -O » : format de sortie (Essayez a, f, n, s, …)
Exemple :
$ snmpget -v2c -c public -Oa demo.snmplabs.com sysDescr.0
SNMPv2-MIB::sysDescr.0 = STRING: SunOS zeus.snmplabs.com 4.1.3_U1 1 sun4m
La version du protocole à utiliser est obligatoire, dans la commande
ci-dessus nous utilisons le protocole version 2c et la communauté «
public ».
Nous demandons l'information « sysDescr ». Pour ce faire nous
ajoutons le chemin « .0 » qui désigne « la valeur du noeud
sysDescr».
Les autres valeurs sous un nœuds (.1, .2, etc.) désignent les sous-
éléments de ce dernier.
Les informations peuvent donc être demandées via le chemin
complet comme sous la forme d'un OID numérique
« 1.3.6.1.2.1.1.1.0 » (ou textuel) ou encore via le nom court. Car il
est normalement unique.
Vous pouvez donc relancer la commande ci-dessus avec l'OID
numérique…
La commande « Get Next »
Elle accepte les mêmes paramètres que « GET » mais retourne non
pas l'OID demandé mais celui qui suit dans l'arborescence. En
chainant un GET puis plusieurs GET NEXT vous pouvez donc
consulter tout l'arbre d'une MIB.
$ snmpgetnext -v2c -Oa -c public -Os demo.snmplabs.com sysDescr.0
sysObjectID.0 = OID: enterprises.20408
$ snmpgetnext -v2c -On -c public -Os demo.snmplabs.com sysObjectID.0
sysUpTimeInstance = Timeticks: (179062573) 20 days, 17:23:45.73
Vous pouvez exécuter GET NEXT sur n'importe quel nœud, donc la
racine d'une MIB.
Mais pour parcourir tout un arbre, c'est laborieux.
Les commandes « Get Bulk et Walk »
Pour éviter d'envoyer plusieurs commandes « GET » les unes après
les autres ce qui surcharge les échanges réseau, il est possible de
demander à consulter plusieurs OID en une seule fois via la
commande « GET BULK ». Mais tous les agents ne la supportent
pas.
Vous pouvez aussi utiliser la commande « Walk » pour parcourir
toute une arborescence, comme vous le feriez avec « GET » et «
GET NEXT ». Mais c'est la commande « WALK » qui les exécutera
pour vous.
$ snmpwalk -v2c -On -c public -Os demo.snmplabs.com system
sysDescr.0 = STRING: SunOS zeus.snmplabs.com 4.1.3_U1 1 sun4m
sysObjectID.0 = OID: enterprises.20408
sysUpTimeInstance = Timeticks: (179082328) 20 days, 17:27:03.28
sysContact.0 = STRING: SNMP Laboratories, info@snmplabs.com
sysName.0 = STRING: zeus.snmplabs.com
sysLocation.0 = STRING: Moscow, Russia
sysServices.0 = INTEGER: 72
sysORLastChange.0 = Timeticks: (179082382) 20 days, 17:27:03.82
sysORID.1 = OID: enterprises.20408.1.1
sysORDescr.1 = STRING: new comment
sysORUpTime.1 = Timeticks: (123) 0:00:01.23
Traduire des OIDs
La commande « snmptranslate » permet de traduire des OID du
mode numérique au mode texte et inversement.
$ snmptranslate -On SNMPv2-MIB::sysDescr.0
.1.3.6.1.2.1.1.1.0
$ snmptranslate 1.3.6.1.2.1.1.1.0
SNMPv2-MIB::sysDescr.0
Modifier un OID, la commande « SET »
La commande « snmpset » permet de modifier la valeur d'un OID.
Elle n'est disponible qu'avec le protocole v2 et la communauté «
private » ou le protocole v3.
L'exemple ci-dessous est fourni pour le protocole v3.
Les paramètres utilisés sont :
« -u » : utilisateur
« -l » : niveau de sécurité pour s'identifier
« -a » : protocole d'authentification
(chiffrement mot de passe)
« -A » : mot de passe d'authentification
« -x » : algorithme de chiffrement pour la connexion
« -X » : mot de passe/salt pour le chiffrement de la
connexion
C'est là que l'on découvre le côté pénible du protocole v3 qui
implémente lui-même les connexions SSH sans faire de SNMP over
SSH : il n'y a pas d'auto-négociation des algorithmes de chiffrement,
il faut tout préciser par soi-même ! Misérable !
$ snmpget localhost sysLocation.0
SNMPv2-MIB::sysLocation.0 = STRING: Rouans
$ snmpset -u demo -l authPriv -a MD5 -x DES -A pass1 -X pass2 localhost
sysLocation.0 s "Earth"
SNMPv2-MIB::sysLocation.0 = STRING: Earth
$ snmpget localhost sysLocation.0
SNMPv2-MIB::sysLocation.0 = STRING: Earth
Recommandations concernant l'utilisation du protocole SNMP
et des outils associés
Les versions 1 et 2 du protocole sont insuffisamment sécurisées et
pas du tout pour la première.
La protection dans la version 2 du protocole se fait uniquement via
les « communautés » qui n'utilisent pas de mot de passe. C'est très
faible comme type de protection, car il suffit de connaître le nom de
la communauté pour accéder aux informations d'un matériels qui
pourraient s'avérer être sensibles.
Les agents WindowsXP étaient particulièrement réputés pour
proposer dans leur configuration par défaut beaucoup trop
d'informations sensibles, comme les logins des utilisateurs qui
étaient accessibles directement via la communauté « public ».
Dans la pratique l'agent fournit par les outils NetSNMP (sous Linux)
est relativement lent et n'est pas forcément très bien sécurisé,
beaucoup de failles sont découvertes régulièrement.
Aussi, quelque soit la solution retenue, désactivez
systématiquement le mode anonyme, ou alors contrôlez chaque
information proposée.
Puis, créez des ACLS d'accès à chaque MIB par utilisateur afin
d'avoir une maîtrise complète de qui peut faire quoi.
Enfin, nous ne saurions que trop vous recommander de privilégier
exclusivement la version 3 du protocole.
Conclusion
Les commandes de base du protocole SNMP sont assez simples une
fois que l'on a compris comment manipuler les paramètres qu'elles
utilisent.
Cette première partie de l'initiation au protocole SNMP doit
normalement vous avoir donné :
Une compréhension globale du protocole
Une bonne idée de comment accéder et manipuler les
informations des MIBs, avec des commandes système
La seconde partie de ce tutoriel sur SNMP et PySNMP est
consacrée à la découverte de PySNMP:
Installer PySNMP
Savoir manipuler les composants de base (OIDs,
Communautés, Utilisateurs, Contextes, ...)
Exécuter des requêtes GET/GETNEXT/SET
La troisième partie de ce tutoriel vous plongera plus en avant dans
l'utilisation de PySNMP pour vous apprendre à lire des tableaux de
données.
Enfin, la quatrième partie vous apprendra à créer un agent utilisant
une MIB personnalisée.
SNMP est un protocole de supervision réseau universellement répandu.
C'est le standard utilisé par la quasi totalité des équipements réseaux. Il
permet de superviser (interrogation et modification) tous les types de
matériels, allant du routeur à l'imprimante, et même certaines machines
à café connectées. Cette seconde partie du tutoriel vous présente la
librairie PySNMP
Introduction
Ce tutoriel vous propose de découvrir la librairie Python PySNMP
permettant de dialoguer avec tout matériel compatible avec le
protocole du même nom.
Dans cette seconde partie nous allons vous présenter l'utilisation du
protocole SNMP au travers de la librairie PySNMP.
Vous pouvez démarrer son apprentissage directement par cette
seconde partie si vous connaissez déjà le protocole SNMP.
Toutefois, si vous débutez sur ce protocole nous vous conseillons
très vivement de lire auparavant la première partie de ce tutoriel
qui vous présentera le protocole SNMP, ses qualités et défauts et
vous fournira les outils de base pour commencer à l'utiliser et vous
l'approprier. Sans ces connaissances préliminaires, ce second
chapitre risque fort de vous sembler bien difficile à appréhender.
A la fin de cette seconde partie vous serez normalement capable :
De manipuler les éléments de base OID, communautés,
comptes utilisateurs, contextes, ...
D'utiliser vos propres MIBs
D'exécuter des requêtes GET/GETNEXT/SET et simuler des
WALK
De créer des sondes pour des outils de supervision
comme Nagios, Centreon et consorts.
Plan
Présentation du protocole
SNMP
Les MIB
ASN1
Les requêtes
PySNMP
Les requêtes
Les tables
Utiliser ses MIBs avec un agent
PySNMP
Maintenant que nous avons présenté le protocole SNMP et quelques
commandes pour l'utiliser, nous pouvons enfin passer au véritable
sujet de ce tutorial : la librairie PySNMP !
PySNMP est une librairie Python qui implémente toutes les versions
du protocole SNMP et ce entièrement en Python.
Ses principales caractéristiques sont :
Complète : elle implémente l'intégralité du protocole SNMP
(v1 à v3) entièrement en Python
Assez légère, environ 15000 lignes de code
Assez simple pour les choses simples
Très utilisée dans la communauté : 30 000
téléchargements par mois sur Pypi
Très pratique pour écrire des sondes nagios/centreon/autre
ou récupérer des informations SNMP dans vos programmes
Python
Elle permet d'écrire vos propres agents
Elle est parfois complexe et manque d'une documentation
claire sur certains éléments comme les agents
Installation de PySNMP
L'installation de la librairie est simple, c'est du Python :
$ pip install pysnmp
Toutefois, sur les machines Windows la commande « pip » échouera
généralement sur l'installation de « pycrypto » que « pip » veut
compiler. Or ces dernières sont souvent dépourvues de tels outils.
Dans ce cas installez un binaire « pycrypto » au préalable depuis ce
site ou depuis ce site pour Python 3.5.
Si vraiment vous avez des difficultés, suivez ce tutoriel
d'installation de PySNMP sous Windows rédigé spécialement à la
demande de nos lecteurs.
Manipuler les données ASN.1
Avec PySNMP il vous arrivera de manipuler des données au format
ASN.1 puisque c'est celui qui est utilisé par le protocole SNMP.
La librairie PySNMP s'appuie sur une autre librairie : pyasn1 pour
réaliser cela.
La conversion des types ASN.1 vers python est presque naturelle :
from pyasn1.type.univ import *
a = Integer(21) * 2
print(a)
a = Integer(-1) + Integer(1)
print(a)
a = int(Integer(42))
print(a)
a = OctetString('Hello') + ', ' + OctetString(hexValue='5079534e4d5021')
print(a, type(a))
Les commandes de base
PySNMP propose différentes fonctions de « bas niveau » pour
utiliser le protocole SNMP, mais il est conseillé d'utiliser le module «
hlapi » signifiant « High Level API ».
Ce dernier propose des fonctions de haut niveau, assez faciles à
paramétrer pour exécuter les commandes de base que l'on peut
lister de cette manière
from pysnmp.hlapi import *
l = [ x for x in dir() if 'Cmd' in x]
print(l)
help('pysnmp.hlapi.getCmd')
ce qui affiche
['bulkCmd', 'getCmd', 'nextCmd', 'setCmd']
Help on function getCmd in pysnmp.hlapi:
pysnmp.hlapi.getCmd = getCmd(snmpEngine, authData, transportTarget,
contextData, *varBinds, **options)
Creates a generator to perform one or more SNMP GET queries.
Avant d'utiliser ces commandes nous allons apprendre à manipuler
les paramètres dont elles ont besoin :
Les OIDs que l'on souhaite consulter/modifier
Les éléments de connexion « communauté » pour le
protocole v2 ou l'utilisateur pour le protocole v3
…
Les OIDS
Les OIDs sont les identifiants des informations décrites dans les
MIBs.
Un OID indique le chemin à suivre dans l'arbre de la MIB pour trouver
l'information ; « .1.2.3.0 » pourrait se comprendre par :
«.»
En partant de la place centrale, plusieurs rues sont faces à
vous.
«1»
Prenez la première à partir de la gauche
Vous arrivez sur une nouvelle place.
«2»
Plusieurs rues sont de nouveau face à vous.
Prenez la seconde sur votre gauche.
Vous arrivez sur une nouvelle place.
«3»
Plusieurs rues sont de nouveau face à vous.
Prenez la troisième sur votre gauche.
Vous arrivez sur une nouvelle place.
«0»
Il y a une plaque sur la place, elle contient votre
information.
Si vous avez envoyé un « GET » vous la lisez, si vous avez
envoyé un « SET » vous la modifiez.
Cheminement dans la MIB
Un OID est représenté soit par une suite de valeurs numériques, soit
par une suite de noms désignant chaque branche parcourue dans la
MIB.
Si vous souhaitez plus de précisions sur ces OIDs je vous invite
à lire la documentation de PySNMP à ce sujet.
Pour créer un OID avec PySNMP il convient d'instancier la classe «
ObjectIdentity »
Un OIDs peut être défini :
Par une chaîne de caractères
Par un tuple
Par son libellé
Ces 4 instances ci-dessous définissent le même objet
o = ObjectIdentity('1.3.6.1.2.1.1.3.0')
o = ObjectIdentity((1, 3, 6, 1, 2, 1, 1, 3, 0))
o = ObjectIdentity('SNMPv2-MIB', 'sysUpTime', 0)
o = ObjectIdentity('iso.org.dod.internet.mgmt.mib-2.system.sysUpTime.0')
Cela semble simple, mais à l'utilisation les malheurs commencent :
from pysnmp.hlapi import *
o = ObjectIdentity('1.3.6.1.2.1.1.3.0')
print(o.getLabel())
print(o.getMibNode())
print(o.getMibSymbol())
print(o.getOid())
print(o.prettyPrint())
Traceback (most recent call last):
File "<...>/oid_example.py", line 4, in <module>
print(o.getLabel())
File "<...>/site-packages/pysnmp/smi/rfc1902.py", line 185, in getLabel
raise SmiError('%s object not fully initialized' %
self.__class__.__name__)
pysnmp.smi.error.SmiError: ObjectIdentity object not fully initialized
Mais pourquoi cette erreur? Les exemples de la documentation de
PySNMP sont plutôt maladroits à cet égard :
>>> from pysnmp.hlapi import *
>>> x = ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0), 'Linux
i386 box'))
>>> # ... calling MIB lookup ...
>>> x[0].prettyPrint()
Il faut effectivement rechercher cet OID dans la MIB, ce n'est pas
automatique car il y a de nombreuses MIBs. PySNMP est fourni avec
tout un arsenal de Mibs, néanmoins il faut lui dire ou les trouver.
Et pour cela la documentation est bien malheureuse car elle
n'affiche jamais d'exemple réel !
Vous avez le droit de pester ; une fois vos nerfs calmés, rappelez-
vous que c'est un produit OpenSource et que vous disposez fort
heureusement de l'accès au code !
La commande « help » de Python montre qu'il existe une méthode «
resolveWithMib ».
Cette méthode prend en paramètre un objet « MibViewController ».
Ce contrôleur a pour rôle de résoudre la correspondance « OID
»/MIB.
La documentation de la classe « MibViewController » est quasiment
inexistante. Cette classe prend un objet « MibBuilder » en argument
lors de sa création.
Il faut donc aller voir la classe « MibBuilder »
Mais une petite recherche dans l'arborescence de PySNMP montre
qu'apparemment un « MibViewController » s'obtient à partir du
moteur SNMP engine.
L'engine est en quelque sorte le coeur de la librairie PySNMP qui
vous permet de dialoguer avec les différents éléments du protocole
(messages, hôtes, communautés, OIDs, MIBS, etc.)
Vous pouvez avoir plusieurs moteurs pour, par exemple, travailler
avec plusieurs MIBs.
Notre code devient alors :
from pysnmp.hlapi import *
se = SnmpEngine()
mvc = se.getUserContext('mibViewController')
o.resolveWithMib(mvc)
o = ObjectIdentity('1.3.6.1.2.1.1.3.0')
print(o.getLabel())
print(o.getMibNode())
print(o.getMibSymbol())
print(o.getOid())
print(o.prettyPrint())
Hélas, nous n'y sommes toujours pas...
Traceback (most recent call last):
File "<...>/PySNMP/oid_example.py", line 7, in <module>
o.resolveWithMib(mvc)
File "<...>/site-packages/pysnmp/smi/rfc1902.py", line 355, in
resolveWithMib
addMibCompiler(mibViewController.mibBuilder,
AttributeError: 'NoneType' object has no attribute 'mibBuilder'
Le mib view controller n'est pas défini car nous n'avons pas créé de
« UserContext ».
La solution la plus simple revient, finalement, à créer nous-même le
« MibViewController » :
from pysnmp.hlapi import *
from pysnmp.smi.view import MibViewController
se = SnmpEngine()
mvc = se.getUserContext('mibViewController')
if not mvc:
mvc = MibViewController(se.getMibBuilder())
o = ObjectIdentity('1.3.6.1.2.1.1.3.0')
o.resolveWithMib(mvc)
print(o.getLabel())
print(o.getMibNode())
print(o.getMibSymbol())
print(o.getOid())
print(o.prettyPrint())
Et voilà !
('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'system', 'sysUpTime')
MibScalar((1, 3, 6, 1, 2, 1, 1, 3), TimeTicks())
('SNMPv2-MIB', 'sysUpTime', (ObjectName('0'),))
1.3.6.1.2.1.1.3.0
SNMPv2-MIB::sysUpTime.
Le contrôleur de MIB utilise le MIB « builder » par défaut de PySNMP
qui stocke les versions « pythonisées » des MIBS dans le sous-
dossier d'installation de pysnmp : « pysnmp/smi/mibs/ ». N'hésitez
pas à y jeter un œil.
Travailler avec sa propre MIB
Pour les MIBs standard, vous savez comment faire. Mais vous allez
dire :
« Oui, mais je travaille avec des MIBs propriétaires car je suis un
constructeur de matériels ou j'utilise le matériel spécifique d'un
constructeur et j'aurais besoin de ma propre MIB ».
Pas de problème, les OIDs peuvent le faire pour vous.
Supposons que vous disposiez de votre propre MIB, comme celle ci-
dessous dont l'essentiel à été emprunté à ce tutoriel :
MAKINA_MIB DEFINITIONS ::= BEGIN
IMPORTS
OBJECT-TYPE, Integer32, NOTIFICATION-TYPE, enterprises
FROM SNMPv2-SMI
;
MakinaCorpus OBJECT IDENTIFIER ::= {enterprises 3}
dataValue OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A sample count of something."
::= {MakinaCorpus 1}
dataDescription OBJECT-TYPE
SYNTAX OCTET STRING
MAX-ACCESS read-only
STATUS current
DESCRIPTION "A description of something"
::= {MakinaCorpus 2}
testTrap NOTIFICATION-TYPE
STATUS current
DESCRIPTION "Test notification"
::= {MakinaCorpus 3}
END
En général les MIB des constructeurs sont stockées sous l'OID «
enterprises » : iso . org . dod . internet . private . Enterprises
(1.3.6.1.4.1)
Le site IP monitor semble montrer que le sous nœud « .3 » est libre,
je l'ai donc choisi pour installer la MIB de Makina ci-dessus, mais
vous pourriez prendre un sous noeud existant.
MakinaCorpus OBJECT IDENTIFIER ::= {enterprises 3}
Il convient maintenant de compiler cette MIB avec PySMI.
Normalement cette librairie est installée avec PySNMP, sinon
exécutez « pip install pysmi ».
PySNMP est livré avec plusieurs scripts, notamment « build-pysnmp-
mib » qui va réaliser cette conversion pour nous :
$ build-pysnmp-mib -o MAKINA_MIB.py MAKINA_MIB
Ceci générera un fichier « MAKINA_MIB.py » contenant le code
Python implémentant cette MIB.
Toutefois, depuis la version 4.3 de PySNMP le script "build-pysnmp-
mib" est devenu obsolète et n'est plus livré.
Pour compiler votre MIB vous devez utiliser le script "mibdump.py"
généralement installé dans le dossier "/usr/local/bin"
$ /path/to/mibdump.py /path/to/MAKINA_MIB # use ./MAKINA_MIB if you are in
the good repository to avoid mib not found error
Source MIB repositories: file:///path/to/MAKINA_MIB,
file:///usr/share/snmp/mibs, http://mibs.snmplabs.com/asn1/@mib@
Borrow missing/failed MIBs from:
http://mibs.snmplabs.com/pysnmp/notexts/@mib@
Existing/compiled MIB locations: pysnmp.smi.mibs, pysnmp_mibs
Compiled MIBs destination directory: /home/<user>/.pysnmp/mibs
...
Created/updated MIBs: MAKINA_MIB
Pre-compiled MIBs borrowed:
Up to date MIBs: SNMPv2-CONF, SNMPv2-SMI, SNMPv2-TC
Missing source MIBs:
Ignored MIBs:
Failed MIBs:
Enfin, pour construire un OID utilisant cette dernière vous devez
maintenant écrire :
o = ObjectIdentity((1, 3, 6, 1, 4, 1, 3, 1))
o.addMibSource('/path/to/makina_mib/dot/py/folder')
o.loadMibs("MAKINA_MIB")
o.resolveWithMib(mvc)
print(o.getLabel())
print(o.getMibNode())
print(o.getMibSymbol())
print(o.getOid())
print(o.prettyPrint())
('iso', 'org', 'dod', 'internet', 'private', 'enterprises',
'MakinaCorpus', 'dataValue')
MibScalar((1, 3, 6, 1, 4, 1, 3, 1), Integer32())
('MAKINA_MIB', 'dataValue', ())
1.3.6.1.4.1.3.1
MAKINA_MIB::dataValue
Nous verrons en exécutant les commandes de base que l'on peut
aussi spécifier directement la source ASN.1 au lieu de compiler la
MIB. Depuis la version 4.3, PySNMP peut le faire automatiquement
dans certains cas.
Autres éléments avant de commencer
Les commandes « GET », « SET » et « BULK » de la librairie PySNMP
requièrent plusieurs paramètres :
Le moteur SNMP à utiliser
Le paramètre d'authentification, « communauté » ou «
utilisateur »
Le type de transport utilisé (UDP), IPv4 ou IPv6 et le port
(161 par défaut)
Le contexte d'exécution
Et les variables à manipuler (les OIDs)
Le moteur SNMP
PySNMP permet de dialoguer avec tous les composants du protocole
SNMP via un objet « SNMP Engine ». Sa description complète est
consultable dans la documentation officielle.
Bien souvent vous avez uniquement besoin de l'instancier.
from pysnmp.hlapi import *
se = SnmpEngine()
Paramètre d'authentification
Ce paramètre est soit :
Une communauté pour les protocoles v1 et v2
Un utilisateur (avec les 5 paramètres login, mot de passe,
algorithme chiffrement mot de passe, salt chiffrement
communication, algorithme chiffrement communication)
dans le cas du protocole v3
La communauté est fournie par la classe « CommunityData »
from pysnmp.hlapi import *
cv1 = CommunityData('public', mpModel=0) # SNMPv1
cv2 = CommunityData('public', mpModel=1) # SNMPv2c
Les utilisateurs utilisés pour les connexions via le protocole v3
doivent être instanciés via la classe « UsmUserData » qui accepter
5 paramètres :
userName : Login de l'utilisateur
authKey : mot de passe
authProtocol : protocole pour chiffrer le mot de passe
Un tuple indiquant le chemin dans la MIB définisant le
protocole.
Sous « iso . org . dod . internet . snmpV2 . snmpModules .
snmpFrameworkMIB . snmprameworkAdmin .
SnmpAuthProtocols »
Par défaut « usmHMACMD5AuthProtocol »
privKey : La clef/le salt pour l'algorithme de chiffrement
privProtocol : L'algorithme de chiffrement.
Un tuple indiquant le chemin dans la MIB définissant cet
algorithme soit sous « iso . org . dod . internet . snmpV2 .
snmpModules . snmpFrameworkMIB .
snmpFrameworkAdmin . snmpPrivProtocols », soit
ailleurs.
PySNMP propose 6 algorithmes :
usmNoPrivProtocol, par défaut si pas de clef
associée
usmDESPrivProtocol, par défaut si non précisé et
clef associée
usm3DESEDEPrivProtocol
usmAesCfb128Protocol
usmAesCfb192Protocol
usmAesCfb256Protocol
Exemple :
u = UsmUserData('userlogin'
, authKey='authenticationkey'
, privKey='encryptionkey'
, privProtocol=usmAesCfb256Protocol)
print(u)
UsmUserData(userName='userlogin', authKey=<AUTHKEY>,
privKey=<PRIVKEY>, authProtocol=(1, 3, 6, 1, 6, 3, 10, 1, 1, 2),
privProtocol=(1, 3, 6, 1, 4, 1, 9, 12, 6, 1, 2),
securityEngineId='<DEFAULT>', securityName='userlogin')
Les types de transport
UDP en IPv4 ou IPv6 :
Ce sera donc une instance d'une de ces 2 types de transports:
UdpTransportTarget
Udp6TransportTarget
Ces 2 classes acceptent plusieurs arguments :
Le premier étant un couple (adresse ip, port).
L'adresse est une chaîne de caractères, hostname ou
adresse IP. Le port un entier.
Les adresses IPv6 doivent respecter la RFC 1924#section-
3
un timeout, exprimé en secondes, par défaut
un nombre maximum pour le « retry », par défaut
une liste de tags, selon la RFC 3413#section-4.1.4
Exemple :
t1 = UdpTransportTarget(('demo.snmplabs.com', 161))
t2 = Udp6TransportTarget(('google.com', 161))
print(t1)
print(t2)
UdpTransportTarget(('195.218.195.228', 161), timeout=1, retries=5,
tagList=b'')
Udp6TransportTarget(('2a00:1450:4007:80e::200e', 161), timeout=1,
retries=5, tagList=b'')
Les contextes
Il s'agit des contextes SNMP tels que définis par la RFC 3411.#3.3.1
Avec SNMPv1 un agent ne pouvait gérer qu'une seule MIB.
Or aujourd'hui certains matériels peuvent gérer plusieurs MIB, par
exemple un commutateur ATM ayant plusieurs cartes, chacune avec
sa propre base.
Le contexte permet donc d'indiquer à quelle sous-ensemble des
MIBs de l'agent on s'adresse.
Pour créer un contexte il convient d'instancier la classe
« ContextData ».
Elle accepte 2 paramètres :
Le contextEngineId
Ou le contextName
Les OIDs et ObjectType
Nous avons couvert les OIDs dans un précédent chapitre.
Cependant, lorsque vous envoyez une commande via la librairie
SNMP vous le faîtes via la classe « ObjectType ». L'object type
permet d'encapsuler l'OID et sa valeur réelle (dans le cas d'une
commande « SET ») ou en retour d'un « GET ».
Dans le cadre d'un GET vous ne spécifiez que l'OID lorsque vous
construisez l'instance « ObjectType » mais en retour vous aurez un
ObjectType avec le même OID et sa valeur.
ot = ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr'), 'Linux i386
box')
ot.resolveWithMib(mvc)
print(ot)
SNMPv2-MIB::sysDescr = Linux i386 box
La commande « GET »
Maintenant que nous avons passé en revue l'ensemble des éléments
nécessaires à l'appel de commandes, nous allons ENFIN pouvoir
écrire notre première commande GET
Pour envoyer un message « GET » il convient d'utiliser la fonction «
getCmd » listée précédemment.
Enfin, lorsque vous exécutez cette commande vous pouvez
demander plusieurs OID à la fois.
La syntaxe est la suivante :
pysnmp.hlapi.getCmd(snmpEngine, authData, transportTarget, contextData,
*varBinds, **options)
Cette fonction retourne un tuple de 4 éléments :
errorIndication : Une valeur considérée comme « True » si
une erreur s'est produite dans l'engine
errorStatus : Une valeur considérée comme « True » pour
une erreur PDU
errorIndex : L'index de la variable ayant provoqué l'erreur
(commence à 0)
varBinds : Une tuple contenant les valeurs retournées par
la commande dans des instances de la classe « ObjectType
»
Soit la commande suivante :
$ snmpget -v1 -c public demo.snmplabs.com sysLocation.0 sysDescr.0
Elle peut être exécutée via le code Python suivant :
from pysnmp.hlapi import *
data = (
ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysLocation', 0)),
ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0))
)
g = getCmd(SnmpEngine()
, CommunityData('public', mpModel=0)
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, *data)
errorIndication, errorStatus, errorIndex, varBinds = next(g)
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0]
or '?'
)
)
else:
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
SNMPv2-MIB::sysLocation.0 = Moscow, Russia
SNMPv2-MIB::sysDescr.0 = SunOS zeus.snmplabs.com 4.1.3_U1 1 sun4m
Pour l'exécuter avec le protocole « v2c » changez simplement la
valeur du paramètre « mpModel=1 » dans l'objet community.
Pour l'exécuter avec le protocole « v3 » remplacez l'instance «
CommunityData » par une instance « UsmUserData ».
La liste des communautés et utilisateurs disponibles sur le site «
demo.snmplabs.com » est accessible à cette URL.
Exemple d'utilisateur (usr-sha-des/SHA/authkey1/DES/privkey1 ) :
UsmUserData("usr-sha-des"
, authProtocol=usmHMACSHAAuthProtocol
, authKey="authkey1"
, privProtocol=usmDESPrivProtocol
, privKey="privkey1" )
La commande « GetNEXT »
La commande « GetNEXT » s'exécute exactement comme une
commande « get », avec les mêmes paramètres.
Elle permet de récupérer l'élément suivant l'OID passé en argument.
from pysnmp.hlapi import *
g = nextCmd(SnmpEngine()
, CommunityData('public', mpModel=1)
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysObjectID', 0)))
errorIndication, errorStatus, errorIndex, varBinds = next(g)
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint()
, errorIndex and varBinds[int(errorIndex) - 1][0]
or '?'
)
)
else:
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
SNMPv2-MIB::sysUpTime.0 = 280102869
Comme vous l'avez peut-être remarqué, la commande retourne un «
générateur ».
Vous pouvez donc la rappeler avec la fonction « next » pour avoir
l'élément suivant.
Ce qui vous permet de descendre toute une MIB en modifiant
quelque peu le code ci-dessus, une fois la variable « g » instanciée.
from pysnmp.hlapi import *
g = nextCmd(SnmpEngine()
, CommunityData('public', mpModel=1)
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysObjectID', 0)))
for errorIndication, errorStatus, errorIndex, varBinds in g:
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'
)
)
else:
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
SNMPv2-MIB::sysUpTime.0 = 280140241
SNMPv2-MIB::sysContact.0 = SNMP Laboratories, info@snmplabs.com
SNMPv2-MIB::sysName.0 = zeus.snmplabs.com
SNMPv2-MIB::sysLocation.0 = Moscow, Russia
SNMPv2-MIB::sysServices.0 = 72
SNMPv2-MIB::sysORLastChange.0 = 280140343
SNMPv2-MIB::sysORID.1 = PYSNMP-MIB::pysnmpObjects.1
SNMPv2-MIB::sysORDescr.1 = new comment
SNMPv2-MIB::sysORUpTime.1 = 123
La commande « SET »
La commande «SET» s'exécute exactement comme une commande
« get », avec les mêmes paramètres.
Excepté que l'ObjectType contient un second argument : la nouvelle
valeur.
from pysnmp.hlapi import *
def show_item( ):
g = getCmd(SnmpEngine()
, CommunityData('public', mpModel=1)
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysORDescr',
1)))
errorIndication, errorStatus, errorIndex, varBinds = next(g)
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
# Show initial value
show_item()
# Setting new value
g = setCmd(SnmpEngine()
, UsmUserData("usr-sha-des"
, authProtocol=usmHMACSHAAuthProtocol
, authKey="authkey1"
, privProtocol=usmDESPrivProtocol
, privKey="privkey1" )
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysORDescr', 1),
'Hello from Lannion using Kalray processor on Linux'))
errorIndication, errorStatus, errorIndex, varBinds = next(g)
print(errorIndication, varBinds)
show_item()
SNMPv2-MIB::sysORDescr.1 = Here is my new note
SNMPv2-MIB::sysORDescr.1 = Hello from Lannion using Kalray processor on
Linux
Conclusion
Les commandes de base du protocole SNMP s'utilisent assez
facilement une fois que l'on a compris comment configurer les
différents paramètres qu'elles requièrent.
Cette seconde partie de l'initiation au protocole SNMP et à PySNMP
doit normalement vous avoir donné :
Une compréhension globale des éléments de base de la
librairie PySNMP
Les moyens de convertir vos MIBs au format ASN.1 en un
code Python pour PySNMP
Les moyens de construire des sondes pour vos logiciels de
monitoring
Il nous reste encore à voir quelques éléments plus avancés comme :
Requêter des tableaux de données
Créer son propre agent
Nous réaliserons ces 2 dernières étapes dans la troisième et la
quatrième partie de ce tutoriel.
Initiation à SNMP avec Python :
PySNMP (Partie 3) - Les tables
Par Gaël Pegliasco — publié 07/04/2016, édité le 22/04/2016
SNMP est un protocole de supervision réseau universellement répandu.
C'est le standard utilisé par la quasi totalité des équipements réseaux. Il
permet de superviser (interrogation et modification) tous les types de
matériels, allant du routeur à l'imprimante, et même certaines machines
à café connectées. Cette troisième partie du tutoriel vous présente
comment gérer les données de type tables.
Introduction
Ce tutoriel vous propose de découvrir la librairie Python PySNMP
permettant de dialoguer avec tout matériel compatible avec le
protocole du même nom.
Dans cette troisième partie nous vous présentons comment
consulter des données de type "tableau" depuis la ligne de
commandes et avec la librairie PySNMP.
Si vous ne connaissez pas le protocole SNMP nous vous invitons à
lire auparavant la première partie de ce tutoriel qui vous présentera
le protocole SNMP, ses qualités et défauts et vous fournira les outils
de base pour commencer à l'utiliser et vous l'approprier. Sans ces
connaissances préliminaires, ce troisième chapitre risque fort de
vous sembler bien difficile à appréhender.
Si vous n'avez jamais utilisé la librairie PySNMP nous vous invitons
aussi à lire la seconde partie de ce tutoriel qui vous donnera les
bases pour utiliser le protocole SNMP en Python.
A la fin de cette troisième partie vous serez normalement capable :
D'interroger des tables de données en ligne de commande
De lire des tables de données SNMP avec Python
Les tables
Certaines informations des MIBs sont fournies sous forme de
tableaux.
Par exemple les interfaces réseau d'un serveur (eth0, wlan0,
loopback).
Ces tableaux sont organisés sous forme d'enregistrements. Chaque
enregistrement ou ligne du tableau contient plusieurs informations.
Ces enregistrements sont accessibles individuellement via leur
index.
Prenons l'exemple de l'entrée « ifTable », dont l'OID est :
iso . org . dod . internet . mgmt . mib-2 . interfaces . IfTable
1.3.6.1.2.1.2.2
Sa définition ASN.1 est la suivante :
-- the Interfaces table
-- The Interfaces table contains information on the entity's
-- interfaces. Each sub-layer below the internetwork-layer
-- of a network interface is considered to be an interface.
ifTable OBJECT-TYPE
SYNTAX SEQUENCE OF IfEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION
"A list of interface entries. The number of entries
is given by the value of ifNumber."
::= { interfaces 2 }
ifEntry OBJECT-TYPE
SYNTAX IfEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION
"An entry containing management information applicable
to a particular interface."
INDEX { ifIndex }
::= { ifTable 1 }
IfEntry ::=
SEQUENCE {
ifIndex InterfaceIndex,
ifDescr DisplayString,
ifType IANAifType,
ifMtu Integer32,
ifSpeed Gauge32,
ifPhysAddress PhysAddress,
ifAdminStatus INTEGER,
ifOperStatus INTEGER,
ifLastChange TimeTicks,
ifInOctets Counter32,
ifInUcastPkts Counter32,
ifInNUcastPkts Counter32, -- deprecated
ifInDiscards Counter32,
ifInErrors Counter32,
ifInUnknownProtos Counter32,
ifOutOctets Counter32,
ifOutUcastPkts Counter32,
ifOutNUcastPkts Counter32, -- deprecated
ifOutDiscards Counter32,
ifOutErrors Counter32,
ifOutQLen Gauge32, -- deprecated
ifSpecific OBJECT IDENTIFIER -- deprecated
}
On y lit que l'élément « ifTable » est une séquence d'éléments
« ifEntry » et que chaque élément « ifEntry » est indexé par un
élément « ifIndex »
Enfin, « ifEntry » est lui même une séquence de nombreux
éléments comme « ifDescr », « ifType », « ifSpeed » etc.
En d'autres termes, « ifTable » est un tableau de lignes de type
« ifEntry » possédant plusieurs colonnes « ifDescr », « ifType »
et caetera.
Représentation de la table « ifTable », source « etutorials.org »
Nous pouvons regarder cela de plus près avec la commande
SNMPWALK :
$ snmpwalk -v2c -c public demo.snmplabs.com ifTable
IF-MIB::ifIndex.1 = INTEGER: 1
IF-MIB::ifIndex.2 = INTEGER: 2
IF-MIB::ifDescr.1 = STRING: eth0
IF-MIB::ifDescr.2 = STRING: eth1
IF-MIB::ifType.1 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifType.2 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifMtu.1 = INTEGER: 1500
IF-MIB::ifMtu.2 = INTEGER: 1500
IF-MIB::ifSpeed.1 = Gauge32: 100000000
IF-MIB::ifSpeed.2 = Gauge32: 100000000
IF-MIB::ifPhysAddress.1 = STRING: 0:12:79:62:f9:40
IF-MIB::ifPhysAddress.2 = STRING: 0:12:79:62:f9:41
IF-MIB::ifAdminStatus.1 = INTEGER: up(1)
IF-MIB::ifAdminStatus.2 = INTEGER: up(1)
IF-MIB::ifOperStatus.1 = INTEGER: up(1)
Il est possible de retrouver les différents enregistrements du tableau
grâce à l'index « ifIndex » des entrées « ifEntry ». Ici nous avons 2
valeurs pour l'index : 1 et 2, donc 2 enregistrements.
Chaque colonne de l'enregistrement « ifEntry » sera donc associée
à son Index :
« ifDescr.1 » et « ifType.1 » pour le premier
enregistrement.
« ifDescr.2 » et « ifType.2 » pour le second, et ainsi de
suite.
Il est ainsi possible de reconstituer tous les enregistrements à l'aide
de la commande « snmpwalk » et des valeurs des index. Mais c'est
laborieux.
La commande « snmptable » réalise cela automatiquement pour
vous :
$ snmptable -v2c -c public demo.snmplabs.com ifTable
SNMP table: IF-MIB::ifTable
ifIndex ifDescr ifType ifMtu ifSpeed ifPhysAddress
1 eth0 ethernetCsmacd 1500 100000000 0:12:79:62:f9:40
2 eth1 ethernetCsmacd 1500 100000000 0:12:79:62:f9:41
Cela peut se compliquer lorsque qu'une entrée de table est indexée
par plusieurs index.
Vous aurez remarqué, que la valeur de l'index se retrouve dans l'OID
de chaque « colonne ». Il en est de même quand il y a plusieurs
index, ils sont chaînés dans l'OID, je vous laisse lire la
documentation de PySNMP sur les tables qui explique cela plutôt
bien.
Les tables avec PySNMP
PySNMP ne propose pas d'équivalent à la commande « snmptable »
pour lire une table.
Mais elle peut le faire très simplement avec la commande
« nextCmd » si vous la configurez convenablement.
Avant de regarder comment réaliser cette lecture avec les bonnes
pratiques nous allons essayer de parcourir nous-mêmes les
différents éléments de la MIB « Interface » OID (IF-MIB, ifTable)
manuellement et de retourner une liste de dictionnaires contenant
chacun les valeurs de chacune des colonnes du nœud "ifEntry".
Ceci nous permettra de mieux comprendre la mécanique utilisée par
PySNMP et nous apprendra à manipuler les itérations successives
de « nextCmd ».
Une fois que nous aurons mis en place une solution fonctionnelle,
nous pourrons appliquer quelques paramétrages supplémentaires
pour simplifier la création de notre liste d'enregistrements.
A l'aide de la commande suivante nous allons parcourir tous les
éléments de l'entrée (IF-MIB, ifTable) :
g = nextCmd(SnmpEngine()
, CommunityData('public', mpModel=1)
, UdpTransportTarget(('demo.snmplabs.com', 161)
, ContextData()
, ObjectType(ObjectIdentity('IF-MIB', 'ifTable'))
, lexicographicMode=False)
Ici nous avons tout de même ajouté l'option
« lexicographiqueMode=False » qui indique à la
commande getNext de ne pas continuer à parcourir les éléments en
dehors de ceux sous cet OID.
Pour construire les éléments « ifEntry » nous utilisons la
commande « getNext » comme à l'habitude :
print("\nWalking table")
data = []
dIndex = {}
for errorIndication, errorStatus, errorIndex, varBinds in g:
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'
)
)
else:
for varBind in varBinds:
print(varBind)
oid = varBind[0]
value = varBind[1]
label = oid.getLabel()
print("Label:", label)
print("Mib Symbol:", oid.getMibSymbol())
Ceci nous donnera une sortie semblable à celle-ci :
Walking table
IF-MIB::ifIndex.1 = 1
Label: ('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'interfaces',
'ifTable', 'ifEntry', 'ifIndex')
Mib Symbol: ('IF-MIB', 'ifIndex', (InterfaceIndex(1),))
IF-MIB::ifIndex.2 = 2
Label: ('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'interfaces',
'ifTable', 'ifEntry', 'ifIndex')
Mib Symbol: ('IF-MIB', 'ifIndex', (InterfaceIndex(2),))
IF-MIB::ifDescr.1 = eth0
Label: ('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'interfaces',
'ifTable', 'ifEntry', 'ifDescr')
Mib Symbol: ('IF-MIB', 'ifDescr', (InterfaceIndex(1),))
IF-MIB::ifDescr.2 = eth1
Ce listing nous montre 2 choses :
Le numéro de l'ifEntry à laquelle appartient chaque
propriété est fourni par le dernier élément du tuple « Mib
Symbol » InterfaceIndex(1) ou InterfaceIndex(2)
Le nom de la propriété est fourni par le dernier élément du
Label
('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'interfaces',
'ifTable', 'ifEntry', 'ifDescr')
Nous pouvons donc construire une liste stockant chaque élément
"ifEntry" dans un dictionnaire, comme suit :
key = str(oid.getMibSymbol()[-1][0])
if not key in dIndex:
dIndex[key] = {}
data.append(dIndex[key])
dIndex[key][label[-1]] = str(value.prettyPrint())
Le code final devient le suivant :
from pysnmp.hlapi import *
from pysnmp.smi.view import MibViewController
se = SnmpEngine()
mvc = se.getUserContext('mibViewController')
if not mvc:
mvc = MibViewController(se.getMibBuilder())
#
# # 1.3.6.1.2.1.2.2 ('iso', 'org', 'dod', 'internet', 'mgmt', 'mib-2', 'interfaces', 'ifTable')
oid_iftable = ObjectIdentity('IF-MIB', 'ifTable')
oid_iftable.resolveWithMib(mvc)
print("Root oid")
print(oid_iftable.getOid())
print(oid_iftable.getLabel())
print(oid_iftable.getMibNode())
print(oid_iftable.getMibSymbol())
g = nextCmd(SnmpEngine()
, CommunityData('public', mpModel=1)
, UdpTransportTarget(('demo.snmplabs.com', 161))
, ContextData()
, ObjectType(ObjectIdentity('IF-MIB', 'ifTable'))
, lexicographicMode=False)
print("\nWalking table")
data = []
dIndex = {}
for errorIndication, errorStatus, errorIndex, varBinds in g:
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'
)
)
else:
for varBind in varBinds:
print(varBind)
oid = varBind[0]
value = varBind[1]
label = oid.getLabel()
print("Label:", label)
print("Mib Symbol:", oid.getMibSymbol())
key = str(oid.getMibSymbol()[-1][0])
if not key in dIndex:
dIndex[key] = {}
data.append(dIndex[key])
dIndex[key][label[-1]] = str(value.prettyPrint())
print("\nPrinting items")
for ind, item in enumerate(data):
print("Item %s" % (str(ind + 1), ))
for key, value in item.items():
print(" %s: %s"% (key, value))
Avec la sortie finale suivante pour les éléments "ifEntry":
Printing items
Item 1
ifOutUcastPkts: 11277302
ifInErrors: 0
ifInUcastPkts: 22554581
ifOutErrors: 0
ifOutDiscards: 0
ifInNUcastPkts: 2506064
ifType: 'ethernetCsmacd'
ifSpecific: SNMPv2-SMI::zeroDotZero
ifSpeed: 100000000
ifPhysAddress: 00:12:79:62:f9:40
ifOutNUcastPkts: 1253033
ifOperStatus: 'up'
ifIndex: 1
ifInOctets: 275667056
ifDescr: eth0
ifLastChange: 150603818
ifInDiscards: 0
ifOutOctets: 125303333
ifAdminStatus: 'up'
ifInUnknownProtos: 125301
ifMtu: 1500
ifOutQLen: 0
Item 2
ifOutUcastPkts: 11277305
ifInErrors: 0
ifInUcastPkts: 22554583
ifOutErrors: 0
ifOutDiscards: 0
ifInNUcastPkts: 2506065
ifType: 'ethernetCsmacd'
ifSpecific: SNMPv2-SMI::zeroDotZero
ifSpeed: 100000000
ifPhysAddress: 00:12:79:62:f9:41
ifOutNUcastPkts: 1253034
ifOperStatus: 'up'
ifIndex: 2
ifInOctets: 250606431
ifDescr: eth1
ifLastChange: 249303192
ifInDiscards: 0
ifOutOctets: 125303346
ifAdminStatus: 'up'
ifInUnknownProtos: 125304
ifMtu: 1500
ifOutQLen: 0
Cette technique est intéressante pour comprendre comment sont
organisés les enregistrements dans une table SNMP et pour les
reconstituer manuellement.
Mais la commande « getNext » permet de les récupérer beaucoup
plus simplement en lui indiquant quelles informations nous
souhaitons lire dans les entrées "ifEntry" à chacun de ses appels au
lieu de les lire une par une, comme précédemment.
Pour cela il suffit de lui passer les OID qui nous intéressent en 1
seule fois :
nextCmd(SnmpEngine(),
CommunityData('public', mpModel=0),
UdpTransportTarget(('demo.snmplabs.com', 161)),
ContextData(),
ObjectType(ObjectIdentity('IF-MIB', 'ifDescr')),
ObjectType(ObjectIdentity('IF-MIB', 'ifType')),
ObjectType(ObjectIdentity('IF-MIB', 'ifMtu')),
ObjectType(ObjectIdentity('IF-MIB', 'ifSpeed')),
ObjectType(ObjectIdentity('IF-MIB', 'ifType')),
ObjectType(ObjectIdentity('IF-MIB', 'ifPhysAddress')),
lexicographicMode=False)
Notre programme devient alors :
from pysnmp.hlapi import *
item = 0
for errorIndication, \
errorStatus, \
errorIndex, \
varBinds in nextCmd(SnmpEngine(),
CommunityData('public', mpModel=0),
UdpTransportTarget(('demo.snmplabs.com', 161)),
ContextData(),
ObjectType(ObjectIdentity('IF-MIB', 'ifDescr')),
ObjectType(ObjectIdentity('IF-MIB', 'ifType')),
ObjectType(ObjectIdentity('IF-MIB', 'ifMtu')),
ObjectType(ObjectIdentity('IF-MIB', 'ifSpeed')),
ObjectType(ObjectIdentity('IF-MIB', 'ifType')),
ObjectType(ObjectIdentity('IF-MIB',
'ifPhysAddress')),
lexicographicMode=False):
print("Item ", item)
item += 1
if errorIndication:
print(errorIndication)
break
elif errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex)-1][0] or '?'
)
)
break
else:
for varBind in varBinds:
print(' = '.join([ x.prettyPrint() for x in varBind ]))
Avec la sortie suivante :
Item 0
IF-MIB::ifDescr.1 = eth0
IF-MIB::ifType.1 = 'ethernetCsmacd'
IF-MIB::ifMtu.1 = 1500
IF-MIB::ifSpeed.1 = 100000000
IF-MIB::ifType.1 = 'ethernetCsmacd'
IF-MIB::ifPhysAddress.1 = 00:12:79:62:f9:40
Item 1
IF-MIB::ifDescr.2 = eth1
IF-MIB::ifType.2 = 'ethernetCsmacd'
IF-MIB::ifMtu.2 = 1500
IF-MIB::ifSpeed.2 = 100000000
IF-MIB::ifType.2 = 'ethernetCsmacd'
IF-MIB::ifPhysAddress.2 = 00:12:79:62:f9:41
Conclusion
Nous avons vu dans cette troisième partie comment manipuler des
tableaux SNMP avec la ligne de commande ou la librairie Python.
PySNMP ne possèdant pas de commande spéciale pour parcourir
une table, nous oblige à lister tous ses éléments via des "getNext".
Heureusement il est possible de récupérer toutes les propriétés
d'une entrée au travers d'un seul appel à "getNext" ce qui permet de
grandement simplifier la reconstitution des enregistrements du
tableau.
Dans la quatrième partie de ce tutoriel nous apprendrons à créer des
agents.