Aus Linux-Magazin 08/2020

Eigene MIBs mit Python implementieren

© rido, 123RF

Per SNMP lassen sich aus der Ferne Messwerte und Statusinformationen sammeln und abrufen, Nachrichten empfangen und Konfigurationen ändern. Wer das auch für selbst erstellte Hard- oder Software nutzen will, braucht eine eigene MIB.

Wer bei einer frei erweiterbaren, hierarchischen Wissensdatenbank zuerst an Wikipedia denkt, liegt zwar nicht falsch, hat aber doch einen Klassiker übersehen: SNMP. Auch dieses altgediente Protokoll kann eine sehr breite Palette ganz verschiedener Informationen aus dem Hut zaubern. Es nutzt dafür ein formalisiertes Verfahren und die sogenannten MIBs.

Diese MIBs gibt es vorgefertigt von Geräte- oder Software-Herstellern, doch auch der Anwendungsentwickler, der interne Metriken seiner Software zugänglich machen will, kann sie nutzen. Genauso hilfreich sind sie für Sysadmins, die komplexe Infrastruktur-Setups wie DNSSEC überwachen wollen. Auf den ersten Blick mag es aufwendig wirken, eine eigene MIB zu implementieren – aber mit den richtigen Tools ist das gar nicht so schwer. Dieser Artikel zeigt den Weg dorthin am Beispiel eines Raspberry Pi, dessen SoC-Temperatur überwacht werden soll. Mit von der Partie ist ein leicht nutzbares Python-Modul.

Die Management Information Base

SNMP lässt sich in mancherlei Hinsicht mit LDAP vergleichen, einem anderen Internet-Klassiker, der ebenfalls Informationen bereitstellt. Bei beiden handelt es sich um Netzwerkprotokolle, die beschreiben, wie man auf eine Information zugreifen kann. Im Fall von LDAP konsultiert man ein LDAP-Verzeichnis, das ganz unterschiedliche Formen annehmen kann. Ähnlich ist es bei SNMP: Die befragte Management Information Base (MIB), kann vielfältige Informationen enthalten, von Routing-Tabellen über den Füllgrad von Filesystemen und Lüfterdrehzahlen bis hin zu Konfigurationszuständen.

Man stellt sich die MIB ab besten als Sammlung definierter Objekte und möglicher Benachrichtigungen vor, im Fachjargon Managed Objects und Notifications. Sie bilden eine baumförmige Struktur unterhalb eines Wurzelknotens. Beide werden entsprechend ihrer jeweiligen Position in diesem Baum über einen Object Identifier (kurz OID) identifiziert, der den Pfad von dem Wurzelknoten entlang der Äste bis zum Managed Object beziehungsweise der Notification beschreibt.

Jede OID beginnt mit einem Punkt, der für den Wurzelknoten steht, gefolgt von durch Punkte getrennten Zahlen, die für die Äste und – am Ende – für das Blatt des Baums stehen. Eine noch vergleichsweise kurze OID könnte etwa .1.3.6.1.2.1.31 lauten. Hier gibt es eine Analogie zu IP-Adressen: Die kann sich der Nutzer auch nicht als Zahlenkolonne merken. Deshalb wurde das Domain Name System (DNS) erfunden. Im Fall von OIDs ordnen sogenannte MIB Modules jeder Zahlenkomponente einer OID einen Namen zu. So liest sich etwa obige OID einfacher als ».iso.org.dod.internet.mgmt.mib-2.ifMIB«.

An dieser Beispiel-OID lässt sich auch sehr schön erkennen, dass der MIB-Baum (Abbildung 1) eine Ordnung aufweist: Das beschriebene Objekt findet sich, ausgehend vom Wurzelknoten, zunächst im Zweig der International Organization for Standardization ISO (.1), dort im Zweig für benannte Organisationen »org« (3), dort im Zweig des US-amerikanischen Department of Defense »dod« (6), dort im Zweig für TCP/IP-based Internets »internet« (1), dort im Zweig für Netzmanagement »mgmt« (2) und dort schließlich im Zweig »mib-2« (1) unter dem Namen »ifMIB« (31).

Abbildung 1: Ein Ausschnitt aus dem MIB-Baum.

Abbildung 1: Ein Ausschnitt aus dem MIB-Baum.

MIBs können aber noch mehr: Sie definieren ganze Teilbäume der Managed Information Base, docken dabei oben an vorhandene Bäume an und lassen sich oft wiederum unten durch andere MIBs erweitern. Ganz oben steht dabei die SNMPv2-SMI: Entsprechend den Spezifikationen in RFC 2578 [1] definiert sie den Teilbaum ».iso.org.dod.internet« (.1.3.6.1), unter dem sich wiederum die Teilbäume »directory« (1), »mgmt« (2), »experimental« (3) und »private« (4) befinden.

In der Praxis liegt dort alles Interessante: Während die ersten drei Teilbäume durch herstellerunabhängige Standards gekennzeichnet sind, finden sich unter »private« (4) und dem dortigen Teilbaum »enterprises« (1) von der Internet Authorised Names Authority (IANA) zugewiesene Nummern für herstellerspezifische Teilbäume [2].

MIBs definieren darüber hinaus aber auch Syntax und Semantik. So ordnen sie den Objekten ihres Teilbaums Datentypen zu und definieren gegebenenfalls eigene Datentypen. Dies können entweder skalare Typen sein (quasi alles, was keine Tabelle ist), oder Tabellen, die eine Sammlung skalarer Objekte in Tabellenzeilen darstellen. So beschreibt beispielsweise die in RFC 2863 [3] definierte IF-MIB die »ifTable«, eine Tabelle mit allen Interfaces eines überwachten Systems, und deren Statistiken wie die Anzahl empfangener und versandter Pakete.

Embrace and extend

Eine MIB stellt also letztlich eine Sammlung von Definitionen dar. Zudem bedarf es aber eines Programms, das diese Informationen ermittelt und bereitstellt: Es heißt SNMP-Agent. Bei Netzwerkgeräten wie Switches bleibt der Agent regelmäßig im Verborgenen, weil er dort Teil der Firmware ist und sich die Frage einer Erweiterung durch eigene MIBs ohnehin nicht stellt.

Im Falle eines Linux-Systems wie dem Raspberry Pi im vorliegenden Beispiel sieht das Ganze schon anders aus: Hier implementiert der SNMP-Agent Snmpd zwar ebenfalls eine feste Auswahl von MIBs (Tabelle 1), er lässt sich jedoch auf verschiedenen Wegen erweitern.

Tabelle 1
Von Snmpd verwaltete MIBs

»DISMAN-EVENT-MIB«

»EtherLike-MIB«

»HOST-RESOURCES-MIB«

»IF-MIB«

»IP-FORWARD-MIB«

»IP-MIB«

»IPV6-MIB«

»NOTIFICATION-LOG-MIB«

»RMON-MIB«

»SNMPv2-MIB«

»TCP-MIB«

»UDP-MIB«

Ganz zu Anfang gilt es, auf dem Raspberry Pi zunächst die für diesen Artikel benötigten Pakete zu installieren (Listing 1, erste Zeile). Der einfachste Weg zur Erweiterung führt nun über eine minimale Snmpd-Konfigurationsdatei (Listing 2). Für einen ersten Test empfiehlt es sich, dafür eine bereits vorhandene »/etc/snmp/snmpd.conf« zu verwenden, von der man am besten zuerst eine Sicherungskopie anlegt.

Listing 1

SNMP-Pakete installieren

$ apt-get install snmp snmpd snmp-mibs-downloader smitools
$ snmpget -v2c -c rpitesting -Ovq localhost NET-SNMP-EXTEND-MIB::nsExtendOutputFull.\"rpitemp\"

Listing 2

Minimale /etc/snmp/snmpd.conf

rocommunity rpitesting
extend rpitemp /bin/cat /sys/class/thermal/thermal_zone0/temp

Startet man nun Snmpd durch, lässt sich mit dem Befehl aus der zweiten Zeile von Listing 1 ein Temperaturwert des Broadcom-SoCs ausgeben. Als Resultat erhält man die aktuelle Temperatur, multipliziert mit 1000, die man als Ganzzahl ohne Nachkommastellen verarbeiten kann.

Das funktioniert deshalb, weil das Snmpd-Modul »extend« dynamisch für jeden konfigurierten Befehl mittels der »NET-SNMP-EXTEND-MIB« passende MIB-Einträge unterhalb des Teilbaums ».iso.org.dod.internet.private.enterprises.netSnmp.netSnmpObjects.nsExtensions« erzeugt, wie der Befehl in Listing 3 offenbart.

Listing 3

Snmpwalk

$ snmpwalk -Of -v2c -c rpitesting localhost NET-SNMP-AGENT-MIB::nsExtensions
.iso.org.dod.internet.private.enterprises.netSNmp.netSnmpObjects.nsExtensions.2.1.0 = INTEGER: 1
.iso.org.dod.internet.private.enterprises.netSNmp.netSnmpObjects.nsExtensions.2.2.1.2.7.114.112.105.116.101.109.112 = STRING: "/bin/cat"
.iso.org.dod.internet.private.enterprises.netSNmp.netSnmpObjects.nsExtensions.2.2.1.3.7.114.112.105.116.101.109.112 = STRING: "/sys/class/thermal/thermal_zone0/temp"
[...]

Die hier gezeigte Schreibweise mit den doppelten Doppelpunkten ist übrigens eine von den Net-SNMP-Werkzeugen gern gebrauchte Verkürzung: Vorn steht der MIB-Name, hinten der Name des gesuchten und in dieser MIB definierten Managed Objects beziehungsweise der Notification. Da sowohl MIB-Namen als auch Object- und Notification-Namen innerhalb einer MIB eindeutig sein müssen, ist es auch die Kombination von beiden.

Die Vorgehensweise mit »extend« lässt sich zwar einfach und schnell umsetzen, weist aber auch einige Nachteile auf. So eignet sie sich gut für einfache, nicht im Zusammenhang stehende Informationen: Man könnte leicht mit einer weiteren »extend«-Zeile in der »snmpd.conf« ein weiteres Skript aufrufen, das etwa den Wert eines via I2C angeschlossenen Sensors zurückgibt.

Allerdings gibt es zum einen kein Caching. Jede Abfrage eines via »extend« realisierten Objekts führt also zu einer eigenen Ausführung des jeweils konfigurierten Befehls, der unter Umständen auch einmal etwas länger brauchen könnte. Zum andern möchte man Informationen oft in strukturierterer Form präsentieren, als es die zwangsläufig generisch gehaltene »NET-SNMP-EXTEND-MIB« zulässt.

Will man diese Nachteile vermeiden, braucht es zweierlei: den Entwurf einer eigenen MIB für den gewünschten Einsatzzweck und deren Implementierung auf einem etwas leistungsfähigeren Weg als mit »extend«.

Grundzüge für eigene MIBs

MIBs werden als ASCII-Textdateien verfasst, die sich bei einer Net-SNMP-Standardinstallation in der Regel unter »/usr/share/snmp/mibs/« finden. Sie gehorchen einer Spezifikationssprache namens Structure of Management Information, kurz SMI. SMI – aktuell ist Version 2 – umfasst einen adaptierten Teil der Abstract Syntax Notation ASN.1, die als gemeinsamer Standard der ITU-T (International Telecommunication Union / Telecommunication Standardization Sector) und der ISO sonst vor allem im Telekommunikationsbereich zum Einsatz kommt, etwa bei GSM oder X.509-Zertifikaten, aber zum Beispiel auch bei LDAP.

Prinzipiell besteht eine MIB aus einer Aneinanderreihung von Definitionen im Format Name Keyword [Parameter] ::= Wert, wobei sich die verschiedenen Teile je nach Keyword über mehrere Zeilen erstrecken können. Keywords dürfen zusätzliche verpflichtende oder optionale Parameter mitbringen. Ein erstes Grundgerüst einer »RASPI-MIB.txt« sieht wie in Listing 4 gezeigt aus.

Listing 4

MIB-Grundgerüst

RASPI-MIB DEFINITIONS ::= BEGIN
-----------------------------------------------------------
-- RASPI-MIB zur Ueberwachung der Temperatur des RasPi-SoCs
-----------------------------------------------------------
END

Das Keyword »DEFINITIONS« im Zusammenhang mit dem Namen »RASPI-MIB« sorgt dafür, dass diesem ein konkreter Wert zugeordnet wird – alles, was zwischen den Keywords »BEGIN« und »END« folgt. Dies ist zunächst noch nicht viel, denn alles, was zwischen zwei alleinstehenden Paaren von Bindestrichen beziehungsweise zwischen einem Paar und dem Zeilenende steht, wird als Kommentar aufgefasst und ignoriert. »DEFINITIONS« tanzt zudem insofern aus der Reihe, als es einen Namen mit Großbuchstaben am Anfang und mit Bindestrich verwendet. Beides ist nämlich nach SMIv2 bei allen sonstigen Namen verboten.

Zunächst muss aber eine weitere Ausnahme zur Sprache kommen: das Keyword »IMPORTS«. Es folgt überhaupt nicht dem beschriebenen Format und widmet sich analog ähnlichen Keywords in C- und Python-Programmen der Aufgabe, vorhandene Definitionen aus anderen MIBs zu importieren. Ein typischer »IMPORTS«-Block, der einliest, was im Folgenden noch gebraucht wird, könnte so aussehen wie in Listing 5.

Listing 5

Importe

-- Imports
IMPORTS
  MODULE-IDENTITY, OBJECT-TYPE,
  Integer32
    FROM SNMPv2-SMI
  netsnmpPlaypen
    FROM NET-SNMP-MIB;

Der erste Teil einer MIB besteht immer aus einer Moduldefinition, die zahlreiche Informationen über die MIB umfasst. Um diese praktischerweise in einem Datentyp zusammenzufassen, würde man etwa in C ein Struct definieren, in ASN.1 hingegen verwendet man ein Makro. Makros dienen der Konstruktion von Datentypen und Werten über das ASN.1-Standardrepertoire hinaus. Das in SNMPv2-SMI definierte Makro »MODULE-IDENTITY« wird wie in Listing 6 gezeigt verwendet.

Listing 6

Makro MODULE-IDENTITY

raspiMIB MODULE-IDENTITY
  LAST-UPDATED "202005060200Z"
  ORGANIZATION "Keine"
  CONTACT-INFO
    "Editor:
    Aufmerksamer Leser
    Raspberry-Pi-Weg 42
    10487 Berlin"
  DESCRIPTION
    "Eine MIB zum Überwachen unseres Raspberry Pis"
  REVISION "202005060200Z"
  DESCRIPTION
    "Erste Version."
  ::= { netSnmpPlaypen 42 }

Der Name »raspiMIB« erfüllt die oben erwähnte Regel, dass Namen mit einem Kleinbuchstaben beginnen und keinen Bindestrich enthalten dürfen. Die zusätzlichen Parameter sind alle notwendig und haben folgende Bedeutungen:

  • »LAST-UPDATED« gibt an, wann die MIB zuletzt aktualisiert wurde.
  • »ORGANIZATION« gibt die Organisation oder das Unternehmen an, dem die Autoren angehören.
  • »CONTACT-INFO« liefert Kontaktinformationen zu den Autoren der MIB.
  • »DESCRIPTION« ist eine textliche Beschreibung der MIB, etwa ihres Zwecks.
  • »REVISION« und »DESCRIPTION« treten immer paarweise auf und beschreiben eine konkrete Revision der MIB. Änderungen an der MIB sollten immer in »DESCRIPTION« kurz erläutert werden und mit einer neuen »REVISION« einhergehen, die sich auch in »LAST-UPDATED« spiegelt.

Das Beispiel ordnet dem Namen den Wert »netSnmpPlaypen 42« zu. Wer hier vermutet, dass dies einer OID entspricht, liegt richtig: Der referenzierte Name »netSnmpPlaypen« ist in der »NET-SNMP-MIB« definiert, wiederum mit Bezug auf einen vorhandenen Namen. Löst man diese Kette rekursiv auf, bei installiertem Net-SNMP etwa mit dem Befehl aus Listing 7, bekommt man die OID .1.3.6.1.4.1.8072.9999.9999 zurück. Die obige Definition hängt hier die Zahl 42 an, die MIB definiert also letztlich »raspiMIB« als .1.3.6.1.4.1.8072.9999.9999.42.

Listing 7

OID ermitteln

$ snmptranslate -On NET-SNMP-MIB::netSnmpPlaypen

Man könnte nun vermuten, dass mit dieser Definition automatisch sämtliche weiteren Definitionen der MIB unterhalb dieser OID angelegt werden. Dies ist aber mitnichten so. Sie müssen einerseits explizit auf »raspiMIB« oder andere Namen hin definiert werden. Andererseits ist es zwar die Regel, aber keineswegs Gesetz, dass sie sich auf den mit dem Makro »MODULE-IDENTITY« verwendeten Namen und den dahinter stehenden Teilbaum beziehen müssen.

Bleibt die Frage, warum raspiMIB unterhalb von »netSnmpPlaypen« definiert wurde. Glücklich, wer eine MIB innerhalb einer Organisation entwickelt, für die die IANA unterhalb von »enterprises« einen eigenen Teilbaum zugewiesen hat und der einen entsprechenden Einhängepunkt intern beantragen kann. Selbst in diesem Fall kann es sich aber anbieten, für erste Schritte den vom Net-SNMP-Projekt explizit für eigene Experimente vorgesehenen Teilbaum »netSnmpPlaypen« zu verwenden.

Soll die eigene MIB nur innerhalb einer geschlossenen Organisation zum Einsatz kommen, könnte man dazu neigen, einfach eine x-beliebige OID zu benutzen – schließlich kann das ja niemanden da draußen stören. Das rächt sich aber bitter, wenn es dann doch zu OID-Kollisionen kommt, weil man auch andere MIBs im Einsatz hat. Solch eine Wahl will also wohlüberlegt sein.

Baum- und Blattpflege

Bislang wurde nur der Stamm des eigenen Teilbaums definiert. Erste Äste kommen nun mit dem Keyword »OBJECT IDENTIFIER« hinzu. Der Code aus Listing 8 definiert unterhalb von »raspiMIB« einen Ast »raspiMIBObjects« und darunter wiederum einen Zweig »raspiScalars«. Zweige können übrigens nicht Gegenstand von »IMPORTS«-Statements sein. Möchte man also sämtliche Objekte eines Zweigs importieren, muss man sie explizit und einzeln aufführen.

Listing 8

Erste Äste

-- MIB root nodes
  raspiMIBObjects  OBJECT IDENTIFIER ::= { raspiMIB 1 }
  raspiMIBScalars  OBJECT IDENTIFIER ::= { raspiMIBObjects 1 }

Unterhalb von »raspiScalars« definiert nun das »OBJECT-TYPE«-Makro aus Listing 9 ein Managed Object mit dem Namen »socTemp« und dem Datentyp »Integer32«, also eine 32-Bit-Ganzzahl. Das Objekt wird in der aktuellen Revision der MIB unterstützt (»STATUS current«) und kann außerdem via SNMP ausschließlich gelesen werden, nicht aber gesetzt (»MAX-ACCESS read-only«). Bei der späteren Abfrage mit Snmpget gilt es übrigens , bei skalaren Managed Objects immer ein .0 anzuhängen.

Listing 9

Managed Object socTemp

-- Scalars
  socTemp OBJECT-TYPE
    SYNTAX        Integer32
    MAX-ACCESS    read-only
    STATUS        current
    DESCRIPTION
      "Die aktuelle Temperatur des Broadcom SoCs multipliziert mit 1000."
    ::= { raspiMIBScalars 1 }

Wer Tabelle 2 aufmerksam studiert, der bemerkt, dass sie keinen Datentyp für Fließkommazahlen aufführt. Das liegt daran, dass ASN.1 selbst keinen nativen Datentyp »Float« kennt und sich kein Standard für ein Encoding von Fließkommazahlen durchsetzen konnte. Das Problem lässt sich aber zumeist – das gilt auch im vorliegenden Fall – umgehen, indem man einen konkreten Wert mit 10 multipliziert übermittelt, statt »35.9« also »359«. Solche Konventionen sollte der MIB-Autor aber stets in der »DESCRIPTION« beschreiben.

Tabelle 2
Gängige skalare Datentypen in SMIv2

Typ

Kurzbeschreibung

definiert in MIB

»Counter32«

positiver 32-Bit-Zählerwert

»SNMPv2-SMI«

»Counter64«

positiver 64-Bit-Zählerwert

»SNMPv2-SMI«

»DisplayString«

ASCII-String von 0 bis 255 Zeichen Länge

»SNMPv2-TC«

»Gauge32«

positiver 32-Bit-Messwert

»SNMPv2-SMI«

»Integer32«

positive oder negative 32-Bit-Ganzzahl

»SNMPv2-SMI«

»IpAddress«

IPv4-Adresse

»SNMPv2-SMI«

»Object Identifier«

definiert einen neuen Unterast

»SNMPv2-SMI«

»Unsigned32«

positive 32-Bit-Ganzzahl

»SNMPv2-SMI«

»TimeTicks«

positiver 32-Bit-Zeitwert

»SNMPv2-SMI«

Die Beispiel-MIB wäre jetzt soweit fertig. Es bietet sich aber an, sie noch mit dem Werkzeug Smilint aus dem Paket smitools zu checken. Es prüft die als Argument übergebene MIB-Datei auf Konformität mit den SMI-v2-Vorgaben aus den RFCs 2578 bis 2580, wobei der Parameter »-l« die Strenge der Prüfung beeinflusst. Auf der zu empfehlenden Stufe 4 findet es dann auch noch etwas (Listing 10).

Listing 10

Prüfen mit Smilint

$ smilint -l4 RASPI-MIB.txt
  RASPIMIB.TXT:36: warning: node `socTemp' must be contained in at least one conformance group

Die in RFC 2578 spezifizierten Conformance Statements tragen der Tatsache Rechnung, dass der Anwender in einer MIB vieles vorsehen kann, was nicht unbedingt jede Implementierung auch umsetzt. Aus diesem Grund lassen sich mit dem Makro »OBJECT-GROUP« Gruppen zusammenhängender Managed Objects und mit dem Makro »NOTIFICATION-GROUP« zusammenhängender Benachrichtigungen definieren, die entweder gemeinsam oder gar nicht zu implementieren sind. Mehrere dieser Gruppen kann man dann mit dem Makro »MODULE-CAPABILITIES« zusammenfassen und quasi Umsetzungsgrade definieren, die eine konkrete Implementierung für sich reklamieren kann.

Um Smilint zufriedenzustellen, genügt es aber, eine passende »OBJECT-GROUP« zu definieren, was der Ordnung halber in eigenen Unterästen geschieht (Listing 11). Listing 12 zeigt die so vervollständigte MIB, die es nun zu implementieren gilt, als Ganzes.

Listing 11

OBJECT-GROUP-Definition

raspiMIBConformance    OBJECT IDENTIFIER ::= { raspiMIB 2 }
  -- Conformance
  raspiMIBGroups OBJECT IDENTIFIER ::= { raspiMIBConformance 1 }
  raspiMIBScalarsGroup OBJECT-GROUP
    OBJECTS {
      socTemp
    }
    STATUS current
    DESCRIPTION
      "Skalare Managed Objects der RASPI-MIB."
    ::= { raspiMIBGroups 1 }

Listing 12

RASPI-MIB.txt

RASPI-MIB DEFINITIONS ::= BEGIN
-----------------------------------------------------------
-- RASPI-MIB zur Ueberwachung verschiedener Werte des RasPi
-----------------------------------------------------------
-- Imports
IMPORTS
  MODULE-IDENTITY, OBJECT-TYPE,
  Integer32
    FROM SNMPv2-SMI
  OBJECT-GROUP
    FROM SNMPv2-CONF
  netSnmpPlaypen
    FROM NET-SNMP-MIB;
raspiMIB MODULE-IDENTITY
  LAST-UPDATED "202005060200Z"
  ORGANIZATION "Keine"
  CONTACT-INFO
    "Editor:
    Aufmerksamer Leser
    Raspberry-Pi-Weg 42
    10487 Berlin"
  DESCRIPTION
    "Eine MIB zur Ueberwachung unseres Raspberry Pis"
  REVISION "202005060200Z"
  DESCRIPTION
    "Erste Version."
  ::= { netSnmpPlaypen 42 }
-- MIB root nodes
raspiMIBObjects      OBJECT IDENTIFIER ::= { raspiMIB 1 }
raspiMIBConformance  OBJECT IDENTIFIER ::= { raspiMIB 2 }
raspiMIBScalars      OBJECT IDENTIFIER ::= { raspiMIBObjects 1 }
-- Scalars
socTemp OBJECT-TYPE
  SYNTAX        Integer32
  MAX-ACCESS    read-only
  STATUS        current
  DESCRIPTION
    "Die aktuelle Temperatur des Broadcom SoCs multipliziert mit 1000."
  ::= { raspiMIBScalars 1 }
-- Conformance
raspiMIBGroups OBJECT IDENTIFIER ::= { raspiMIBConformance 1 }
raspiMIBScalarsGroup OBJECT-GROUP
  OBJECTS {
    socTemp
  }
  STATUS current
  DESCRIPTION
    "Skalare Managed Objects der RASPI-MIB."
  ::= { raspiMIBGroups 1 }
END

Von Agent zu Agent

Die Schnittstelle, über die Snmpd mit unserer noch zu schreibenden Implementierung in Kontakt tritt, ist das in RFC 2741 [4] standardisierte Agent Extensibility Protocol (»AgentX«). Es gibt noch andere Erweiterungsmechanismen, wie etwa dynamisch ladbare Module, »pass_persist« und »SMUX«. AgentX bietet aber mehr Möglichkeiten bei gleichzeitig lockerer Kopplung, was aus der Sicherheitsperspektive Vorteile bietet.

Im AgentX-Jargon ist Snmpd der Master Agent, wovon es immer genau einen gibt und an den mehrere, völlig unabhängig laufende Prozesse als sogenannte Subagents andocken können. Sie erklären sich nach dem Verbindungsaufbau für bestimmte Unterbäume des MIB-Baums zuständig und implementieren sie. Dabei spricht der Master Agent als einziger SNMP, versteht aber nichts von MIBs und deren Implementierung. Bei den Subagents verhält es sich genau umgekehrt.

Um AgentX-Unterstützung in Snmpd zu aktivieren, genügt die zusätzliche Zeile »master agentx« in der »/etc/snmp/snmpd.conf«. Standardmäßig läuft die Verbindung zwischen Snmpd und den Subagents über den Unix Domain Socket »/var/agentx/master«; AgentX stellt hier nichts anderes dar als einen Inter-Process-Communication-Mechanismus (IPC). Technisch wäre es zwar auch möglich, einen TCP-Socket zu konfigurieren. Da AgentX aber keinerlei Authentifizierungsmechanismen zwischen Master Agent und Subagents vorsieht und jeden Subagent akzeptiert, sollte eine solche Konfiguration von absichernden Maßnahmen wie Firewalls begleitet werden.

Net-SNMP bringt glücklicherweise nicht nur Snmpd mit, sondern bietet mit seinen Bibliotheken APIs an, auf die der Master Agent auch selbst zurückgreift. Für Entwickler, die der Programmiersprache C mächtig sind, gibt es auf der Net-SNMP-Webseite etliche Tutorials [5], die die Entwicklung eines MIB-Moduls etwa als Subagent beschreiben. Mit »mib2c« gibt es zudem ein Scaffolding-Werkzeug, also ein Tool, das eine MIB als Input nimmt und nach Beantwortung einiger Implementierungsfragen ein detailliert kommentiertes Grundgerüst an C-Sourcecode erzeugt, auf dessen Basis man seine MIB-Implementierung vorantreiben kann.

Nur beherrscht nicht jeder Entwickler C. Deshalb bietet es sich gerade für Szenarien, in denen man externe Informationsquellen in eine eigene MIB integrieren möchte, sowie für schnelle und dennoch ausreichend leistungsfähige Ergebnisse an, stattdessen auf eine der gängigen Skriptsprachen zu setzen. Net-SNMP kommt mit einem eigenen Perl-Modul. Die Lingua franca der Gegenwart ist aber sicherlich Python – und hier sah es bis 2013 eher mau aus. Mit dem bei Net-SNMP mitgelieferten, 2500 Zeilen C-Code umfassenden Python-Modul, konnte man zwar SNMP-Clients implementieren, aber keine Agents. Auf Sourceforge wiederum gab es zwar ein Python-AgentX-Modul [6], das aber seit 2010 kein Release mehr erfahren hatte und etliche Defizite aufwies.

Der Autor dieses Artikels entwickelte deswegen im Rahmen seiner damaligen Tätigkeit ein eigenes, als Open Source veröffentlichtes Python-Modul: Python-Netsnmpagent [7]. Es ist selbst in Python geschrieben und nutzt das bei Python mitgelieferte Ctypes-Modul, um auf die C-API der Net-SNMP-Bibliotheken »libnetsnmpagent.so« und »libnetsnmphelpers.so« zuzugreifen. Die werden dabei für den Python-Programmierer hinter einer »netsnmpAgent«-Klasse und weiteren Klassen für gängige Datentypen abstrahiert, die sich bereits mit wenigen Zeilen Code nutzen lassen.

Zwischenzeitlich fand sich auf Github zwar ein weiteres Python-Modul, Pyagent [8], doch das versuchte sich an einer Eigenimplementierung des kompletten AgentX-Protokolls und wird seit 2015 nicht weiter gepflegt. Python-Netsnmpagent hingegen hatte seine letzten Änderungen 2019, funktioniert aber weiterhin sowohl auf älteren Enterprise-Distributionen wie SLES 11 (Python 2.7, Net-SNMP 5.4.x) als auch auf aktuelleren Systemen mit Python 3.5 oder neuer und Net-SNMP 5.7.x/5.8. Für manche Distributionen wie Suse gibt es fertige Pakete, für Debian und Raspbian jedoch nicht, weswegen Python-Entwickler das Modul dort – gegebenenfalls innerhalb einer virtuellen Umgebung – mit dem Python-eigenen Paketmanager Pip installieren (Listing 13).

Listing 13

Netsnmpagent installieren

$ apt-get install --no-install-recommends python3-pip
$ pip3 install netsnmpagent

Listing 14 zeigt nun eine erste Version des Subagents aus dem vorliegenden Beispiel. Nach dem Import des Netsnmpagent-Moduls erzeugt er eine Instanz der Klasse »netsnmpAgent«. Sie dient als zentrales Drehkreuz für die Verbindung zu Snmpd und die Verwaltung von Managed Objects. Als Parameter erhält sie einen sprechenden Namen für den Agenten und in diesem Beispiel den Pfad zu unser MIB. Durch die explizite Angabe der MIB kann man mit der »RASPI-MIB.txt« experimentieren, ohne sie systemweit in »/usr/share/snmp/mibs/« installieren zu müssen.

Listing 14

raspiagent.py-Grundgerüst

#!/usr/bin/python3
import os
import netsnmpagent
import sys
try:
  agent = netsnmpagent.netsnmpAgent(
    AgentName = "RaspiAgent",
    MIBFiles = [
      os.path.abspath(os.path.dirname(sys.argv[0])) +
      "/RASPI-MIB.txt"
    ]
  )
  agent.start()
  while True:
    agent.check_and_process()
except netsnmpagent.netsnmpAgentException as e:
  print(e)
  sys.exit(1)

Der Aufruf der Klassenmethode »start()« stellt die Verbindung zu Snmpd her. Anschließend wird in einer Endlosschleife immer wieder die Funktion »check_and_process()« aufgerufen, die auf Anfragen des Master Agent wartet und sie bearbeitet. Bislang fehlt dem Code aber noch jeglicher Bezug zum Managed Object »socTemp«.

Hierfür gilt es, vor der Zeile »agent.start()« den Code aus Listing 15 einzufügen. Das Agent-Objekt bietet sogenannte Factory-Methoden an, die nach den gängigen SMIv2-Datentypen benannt sind und eine neue Instanz der gleichnamigen Klasse zurückliefern, hier »Integer32«. Als Parameter erhalten sie immer »oidstr«, das die OID angibt, unter der die Instanz registriert werden soll. Hinzu kommt optional der Parameter »writable«, der angibt, ob man die Instanz via »snmpset« ändern darf. Als wichtigste Methode stellen diese Klassen »update()« zum Aktualisieren des Werts des Managed Objects zur Verfügung.

Listing 15

Integer32-Objekt für socTemp

socTemp = agent.Integer32(
  oidstr   = "RASPI-MIB::socTemp",
  writable = False
)

Startet man nun in einer Konsole mit Root-Rechten »raspiagent.py« und ruft in einer zweiten im selben Verzeichnis, in dem sich »RASPI-MIB.txt« und »raspiagent.py« befinden, den Befehl aus Listing 16 auf, darf man einen ersten Erfolg verbuchen.

Listing 16

Erstes Erfolgserlebnis

$ snmpget -M+. -v2c -c rpitesting localhost RASPI-MIB::socTemp.0 RASPI-MIB::socTemp.0 = INTEGER: 0

Der Befehl liefert aber immer den Wert 0 zurück, da ja die Abfrage der Temperatur noch nicht implementiert wurde. Das holt der Code aus Listing 17 nach. Der Befehl ist derselbe wie in Listing 2, die weitere Verarbeitung der Ausgabe erfolgt jedoch mit Python-Bordwerkzeugen. Ein erneuter Aufruf von Snmpget zeigt jetzt die aktuelle Temperatur an, wie vorher bei Extend (Listing 18).

Listing 17

Aktualisieren des Werts für socTemp

while True:
  line = open("/sys/class/thermal/thermal_zone0/temp").readline()
  socTemp.update(int(line))
  agent.check_and_process()

Listing 18

socTemp auslesen

$ snmpget -M+. -v2c -c rpitesting localhost RASPI-MIB::socTemp.0
RASPI-MIB::socTemp.0 = INTEGER: 40622

Das gezeigte Beispiel implementiert nur ein einziges Managed Object, und dazu mit »Integer32« ein relativ einfaches. Weitere Beispiele für andere skalare Datentypen sowie Tabellen finden sich im via PyPI erhältlichen Python-Netsnmpagent-Archiv sowie im Github-Repository [9] in Form des »simple_agent.py« im Unterverzeichnis »examples/«.

Dort findet sich auch ein Lösungsansatz für ein anderes Problem: Bei »raspiagent.py« fällt schnell auf, dass es jedes Mal einen kurzen Moment dauert, bevor Snmpwalk einen Wert für »socTemp« zurückgibt. Das liegt daran, dass der Agent seine zwei Hauptaufgaben, nämlich die Beschaffung und Weitergabe der Information, nacheinander ausführt und für jede Anfrage (und nur dann) »vcgencmd« aufruft, was ein wenig dauert. Die beiden Aufgaben sollte man besser entkoppeln, und genau das demonstriert »threading_agent.py«, das dazu mit Threads arbeitet.

Fazit

Hat man erst einmal die formalen Regeln und die zur Verfügung stehenden Datentypen und Makros verinnerlicht, geht das Schreiben einer MIB relativ leicht von der Hand. Mit dem passenden Python-Modul zur Implementierung der MIB können sich Sysadmins wie Entwickler fortan auf das Integrieren und Aufbereiten zusätzlicher Informationsquellen in ein vorhandenes SNMP-Monitoring-Setup konzentrieren. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 8 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben