Checkmk
to checkmk.com

1. Einleitung

Checkmk umfasst fast 2.000 fertige Checkplugins für alle nur denkbare Hardware und Software. Diese werden vom Checkmk-Team gepflegt, und jede Woche kommen neue dazu. Daneben gibt es auf der Checkmk Exchange weitere Plugins, die von unseren Anwendern beigesteuert werden.

Und trotzdem gibt es immer wieder Situationen, in denen ein Gerät, eine Anwendung oder einfach nur eine bestimmte Metrik, die für Sie wichtig ist, noch von keinem dieser Plugins erfasst ist — vielleicht auch einfach deshalb, weil es sich dabei um etwas handelt, dass in Ihrer Firma entwickelt wurde und es daher niemand anders haben kann.

1.1. Muss es immer ein echtes Plugin sein?

Welche Möglichkeiten haben Sie also, hier dennoch eine sinnvolle Überwachung zu implementieren? Nun — natürlich können Sie sich an unseren Support wenden und ein geeignetes Plugin entwickeln lassen. Aber Sie können sich auch selbst helfen. Dabei haben Sie erst einmal vier Möglichkeiten:

MethodeSo geht’sVorteileNachteile

Lokaler Check

Checkmk-Agent um einfaches Skript erweitern

Geht sehr einfach, ist in allen Programmiersprachen möglich, welche das Betriebssystem des überwachten Hosts anbietet, unterstützt sogar Serviceerkennung

Konfiguration der Schwellwerte nur beim Agenten selbst, für komplexere Dinge unkomfortabel, keine Unterstützung für SNMP

Nagios-kompatibles Checkplugin

Plugin per MRPE vom Windows- oder Linux-Agenten aufrufen lassen

Zugriff auf alle vorhandenen Nagios-Plugins, auch hier freie Wahl der Programmiersprache

Konfiguration der Schwellwerte nur beim Agenten selbst, Keine SNMP-Unterstützung durch Checkmk, keine Serviceerkennung möglich

Logmeldungen auswerten

Meldungen überwachen per Event Console

Keine Entwicklung notwendig sondern nur aufstellen von Regeln in der Event Console

Geht nur, wenn passende Logmeldungen vorhanden sind, kein gesicherter aktueller Status, kein Erfassen von Metriken, keine konfigurierbaren Schwellwerte

Echtes Checkmk-Plugin

Wird in diesem Artikel erklärt

Fügt sich zu 100% in Checkmk ein, automatische Serviceerkennung, zentrale Konfiguration der Schwellwerte über die grafische Oberfläche, sehr performant, unterstützt SNMP, automatische Host- und Servicelabels möglich, unterstützt HW/SW-Inventur, Unterstützung durch Standardbibliotheken von Checkmk

Erfordert mehr Einarbeitungszeit sowie Kenntnisse in der Programmsprache Python

Dieser Artikel zeigt Ihnen, wie Sie echte Checkmk-Checkplugins entwickeln können — mit allem was dazugehört. Dabei zeigen wir Ihnen, wie Sie die in Version 2.0.0 von Checkmk neu entwickelte API für die Pluginprogrammierung nutzen.

1.2. Was hat sich seit der alten API geändert?

Haben Sie schon Erfahrung mit dem Entwickeln von Checkplugins für die Checkmk-Version 1.6.0 oder früher? Dann finden Sie hier eine knappe Übersicht über alle Änderungen, welche die ab 2.0.0 verfügbare neue Check-API mit sich bringt:

  • Plugins sind jetzt Python-3-Module und die Dateien müssen mit .py enden.

  • Die eigenen Plugins liegen jetzt im Verzeichnis local/lib/check_mk/base/plugins/agent_based.

  • Am Anfang der Datei brauchen Sie nun mindestens eine spezielle import-Anweisung.

  • Die Sektionen und die eigentlichen Checks werden getrennt registriert. Dazu gibt es die neuen Funktionen register.agent_section und register.check_plugin.

  • Etliche Funktions- und Argumentnamen wurden umbenannt. Unter anderem wird jetzt immer konsequent von Discovery gesprochen (früher: Inventory).

  • Die Discovery-Funktion (vormals Inventory-Funktion) und auch die Check-Funktion müssen nun immer als Generatoren arbeiten (also yield verwenden).

  • Die Namen der Argumente der deklarierten Funktionen sind jetzt fest vorgegeben.

  • Anstelle der SNMP-Scanfunktion schreiben Sie eine Deklaration, welche OIDs mit welchen Werten erwartet werden.

  • Die Funktionen zum Darstellen von Zahlen wurden neu strukturiert (z.B. wird get_bytes_human_readable zu render.bytes).

  • Es gibt nun eine eigene Methode, mit der Checks andere ausschließen können (superseeds). Das wird nicht mehr in der SNMP-Scanfunktion gemacht.

  • Die Hilfsfunktionen für die Arbeit mit Countern, Raten und Durchschnitten haben sich geändert.

  • Anstelle von magischen Rückgabewerten wie z.B. 2 für CRIT gibt es jetzt Konstanten (z.B. State.CRIT).

  • Viele mögliche Programmierfehler in Ihrem Plugin erkennt Checkmk jetzt sehr früh und kann Sie gleich darauf hinweisen.

1.3. Wird die alte API noch unterstützt

Ja, die bis zu Version 1.6.0 von Checkmk gültige API für die Entwicklung von Checkplugins wird mit einigen kleinen Einschränkungen noch etliche Jahre unterstützt werden, da mit ihr sehr sehr viele Plugins entwickelt wurden. Während dieser Zeit wird Checkmk beide APIs parallel anbieten. Einzelheiten erfahren Sie in Werk #10601.

Trotzdem empfehlen wir für die Entwicklung von neuen Plugins die neue API, da diese konsistenter und logischer ist, besser dokumentiert und langfristig zukunftssicher.

1.4. Verschiedene Arten von Agenten

Checkplugins werten die Daten der Checkmk-Agenten aus. Bevor wir uns ins Geschehen stürzen, sollten wir uns deshalb zunächst einen Überblick darüber verschaffen, welche Arten von Agenten Checkmk eigentlich kennt:

Checkmk-Agent

Hier werten die Plugins Daten aus, welcher der Checkmk-Agent für Linux, Windows oder andere Betriebssysteme sendet. Damit werden Betriebssystemparameter und Anwendungen überwacht und teilweise auch Serverhardware. Jedes neue Checkplugin erfordert eine Erweiterung des Agenten in Form eines Agentenplugins, damit dieser die nötigen Daten bereitstellt.

Spezialagent / API-Integration

Einen Spezialagenten benötigen Sie, wenn Sie weder mit dem normalen Checkmk-Agenten noch per SNMP an die Daten kommen, welche für das Monitoring relevant sind. Der häufigste Fall ist das Abfragen von HTTP-basierten APIs. Beispiele sind die Überwachung von AWS, Azure oder VMware. Hier schreiben Sie ein Skript, welches direkt auf dem Checkmk-Server läuft, sich mit der API verbindet, und Daten im gleichen Format ausgibt, wie dies ein Agentenplugin tun würde.

SNMP

Bei der Überwachung via SNMP benötigen Sie keine Erweiterung eines Agenten sondern werten Daten aus, welche Checkmk von dem zu überwachenden Gerät per SNMP abruft, welche dieses standardmäßig bereitstellt. Checkmk unterstützt Sie dabei und übernimmt sämtliche Details und Sonderheiten des SNMP-Protokolls. Eigentlich gibt es auch hier einen Agenten: nämlich den auf dem überwachten System vorinstallierten SNMP-Agenten.

Aktiver Check

Dieser Checktyp bildet eine Sonderrolle. Hier schreiben Sie zunächst ein klassisches Nagios-kompatibles Plugin, welches für die Ausführung auf dem Checkmk-Server bestimmt ist und von dort aus mit einem Netzwerkprotokoll direkt einen Dienst auf dem Zielgerät abfragt. Das prominenteste Beispiel ist das Plugin check_http, mit welchem Sie Webserver und Webseiten überwachen können. Dieses Plugin können Sie dann so in Checkmk integrieren, dass man es wie gewohnt per Regeln einrichten kann.

1.5. Voraussetzungen

Wenn Sie Lust haben, sich mit dem Programmieren von Checkplugins zu befassen, benötigen Sie folgendes:

  • Kenntnisse in der Programmiersprache Python

  • Erfahrung mit Checkmk, vor allem was das Thema Agenten und Checks betrifft

  • etwas Übung mit Linux auf der Kommandozeile

Als Vorbereitung sind außerdem folgende Artikel gut:

2. Ein erstes einfaches Checkplugin

Nach dieser langen Einleitung wird es Zeit, dass wir unser erstes einfaches Checkplugin programmieren. Als Beispiel nehmen wir eine einfache Überwachung für Linux. Denn da Checkmk selbst auf Linux läuft, ist es sehr wahrscheinlich, dass Sie auch auf ein Linux-System Zugriff haben.

Das Checkplugin soll einen neuen Service anlegen, welcher erkennt, ob auf einem Linux-Server jemand einen USB-Stick eingesteckt hat. In diesem Fall soll der Service kritisch werden. Vielleicht werden Sie s etwas sogar nützlich finden, aber es ist wirklich nur ein vereinfachtes Beispiel und möglicherweise auch nicht ganz wasserdicht programmiert. Denn darum geht es hier erst einmal nicht.

Das Ganze läuft in zwei Schritten:

  1. Wir finden heraus, mit welchem Linux-Befehl man sehen kann, ob ein USB-Stick eingesteckt ist, und erweitern den Linux-Agenten um ein kleines Skript, welches diesen Befehl aufruft.

  2. Wir schreiben in der Checkmk-Instanz ein Checkplugin, welches diese Daten auswertet.

Und los geht’s…​

2.1. Den richtigen Befehl finden

Am Anfang jeder Checkprogrammierung steht: die Recherche! Das bedeutet, dass wir herausfinden, wie wir überhaupt an die Informationen kommen, die wir für die Überwachung brauchen. Bei Linux sind das oft Kommandozeilenbefehle, bei Windows hilft die PowerShell, VBScript oder WMI und bei SNMP müssen wir die richtigen OIDs finden (dazu gibt es ein eigenes Kapitel).

Für das Herausfinden des richtigen Befehls gibt es leider kein allgemeines Vorgehen und so wollen wir uns auch nicht all zulange mit dem Thema aufhalten, erklären aber kurz, wie das mit dem USB-Stick funktioniert.

Zunächst loggen wir uns also auf dem zu überwachenden Host ein. Unter Linux läuft der Agent per Default als root-Benutzer. Deswegen machen wir auch alle unsere Tests als root. Für unsere Aufgabe mit dem USB-Stick gibt es praktischerweise symbolische Links im Verzeichnis /dev/disk/by-id. Diese zeigen auf alle Linux-Block-Devices. Und ein solches ist auch ein eingesteckter USB-Stick. Außerdem kann man an der ID am Präfix usb- erkennen, wenn ein Block-Device ein USB-Gerät ist.

Folgender Befehl listet alle Einträge in diesem Verzeichnis auf:

root@linux# ls -l /dev/disk/by-id/
total 0
lrwxrwxrwx 1 root root  9 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191 -> ../../sda
lrwxrwxrwx 1 root root 10 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part2 -> ../../sda2
lrwxrwxrwx 1 root root 10 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part3 -> ../../sda3
lrwxrwxrwx 1 root root 10 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part4 -> ../../sda4
lrwxrwxrwx 1 root root 10 May 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part5 -> ../../sda5
lrwxrwxrwx 1 root root  9 May 14 11:21 wwn-0x5002538655584d30 -> ../../sda
lrwxrwxrwx 1 root root 10 May 14 11:21 wwn-0x5002538655584d30-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 May 14 11:21 wwn-0x5002538655584d30-part2 -> ../../sda2
lrwxrwxrwx 1 root root 10 May 14 11:21 wwn-0x5002538655584d30-part3 -> ../../sda3
lrwxrwxrwx 1 root root 10 May 14 11:21 wwn-0x5002538655584d30-part4 -> ../../sda4
lrwxrwxrwx 1 root root 10 May 14 11:21 wwn-0x5002538655584d30-part5 -> ../../sda5

So. Und das Ganze jetzt mit eingestecktem USB-Stick:

root@linux# ls -l /dev/disk/by-id/
total 0
lrwxrwxrwx 1 root root  9 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191 -> ../../sda
lrwxrwxrwx 1 root root 10 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part2 -> ../../sda2
lrwxrwxrwx 1 root root 10 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part3 -> ../../sda3
lrwxrwxrwx 1 root root 10 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part4 -> ../../sda4
lrwxrwxrwx 1 root root 10 Mai 14 11:21 ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part5 -> ../../sda5
lrwxrwxrwx 1 root root  9 Mai 14 12:15 usb-SCSI_DISK-0:0 -> ../../sdc
lrwxrwxrwx 1 root root 10 Mai 14 12:15 usb-SCSI_DISK-0:0-part1 -> ../../sdc1
lrwxrwxrwx 1 root root 10 Mai 14 12:15 usb-SCSI_DISK-0:0-part2 -> ../../sdc2
lrwxrwxrwx 1 root root  9 Mai 14 11:21 wwn-0x5002538655584d30 -> ../../sda
lrwxrwxrwx 1 root root 10 Mai 14 11:21 wwn-0x5002538655584d30-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 Mai 14 11:21 wwn-0x5002538655584d30-part2 -> ../../sda2
lrwxrwxrwx 1 root root 10 Mai 14 11:21 wwn-0x5002538655584d30-part3 -> ../../sda3
lrwxrwxrwx 1 root root 10 Mai 14 11:21 wwn-0x5002538655584d30-part4 -> ../../sda4
lrwxrwxrwx 1 root root 10 Mai 14 11:21 wwn-0x5002538655584d30-part5 -> ../../sda5

2.2. Die Daten entschlacken

Eigentlich wären wir damit fertig und könnten diese ganze Ausgabe per Checkmk-Agent zum Checkmk-Server transportieren und dort analysieren lassen. Denn im Checkmk gilt immer folgende Empfehlung: lassen Sie die komplexe Arbeit immer den Server erledigen. Halten Sie das Agentenplugin so einfach wie möglich.

Aber: Hier ist trotzdem noch zu viel heiße Luft drin. Es ist immer gut, unnötige Daten nicht zu übertragen. Das spart Netzwerkverkehr, Speicher, Rechenzeit und macht alles auch übersichtlicher. Das geht besser!

Als erstes können wir das -l weglassen. Damit ist die Ausgabe von ls schon deutlich schlanker:

root@linux# ls /dev/disk/by-id/
ata-APPLE_SSD_SM0512F_S1K5NYBF810191        ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part5  wwn-0x5002538655584d30-part3
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part1  wwn-0x5002538655584d30-part4                ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part2
wwn-0x5002538655584d30                      wwn-0x5002538655584d30-part5                ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part3
wwn-0x5002538655584d30-part1                ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part4  wwn-0x5002538655584d30-part2

Jetzt wiederum stört der mehrspaltige Aufbau, der aber nur deshalb erfolgt, weil der ls-Befehl erkennt, dass er in einem interaktiven Terminal läuft. Später als Teil vom Agenten wird er die Daten einspaltig ausgeben. Das können wir aber auch ganz einfach hier mit der Option -1 (für *ein*spaltige Ausgabe) erzwingen:

root@linux# ls -1 /dev/disk/by-id/
ata-APPLE_SSD_SM0512F_S1K5NYBF810191
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part1
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part2
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part3
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part4
ata-APPLE_SSD_SM0512F_S1K5NYBF810191-part5
wwn-0x5002538655584d30
wwn-0x5002538655584d30-part1
wwn-0x5002538655584d30-part2
wwn-0x5002538655584d30-part3
wwn-0x5002538655584d30-part4
wwn-0x5002538655584d30-part5

Wenn Sie genau hinsehen, werden Sie nicht nur die Blockgeräte selbst sehen, sondern auch dort vorhandene Partitionen. Dies sind die Einträge, die auf -part1, -part2 usw. enden. Diese brauchen wir für unseren Check nicht und bekommen sie ganz einfach weg mit einem grep. Dort nehmen wir die Option -v für eine negative Logik:

root@linux# ls /dev/disk/by-id/ | grep -v -- -part
ata-APPLE_SSD_SM0512F_S1K5NYBF810191
usb-SCSI_DISK-0:0
wwn-0x5002538655584d30

Hier sieht man jetzt auch viel deutlicher, dass es in unserem Beispiel genau drei Geräte sind, falls der USB-Stick eingesteckt ist:

Perfekt! Jetzt haben wir eine übersichtliche Liste aller Blockgeräte, die mit einem einfachen Befehl ermittelt wird. Mehr brauchen wir nicht.

Das -1 haben wir im letzten Kommando wieder weggelassen, weil ls jetzt in eine Pipe schreibt und von sich aus einspaltig ausgibt. Und grep braucht das --, da es sonst das Wort -part als die vier Optionen -p, -a, -r und -t interpretieren würde.

Übrigens: Warum „greppen“” wir nicht gleich noch nach usb? So dass nur noch USB-Geräte ausgegeben werden? Nun, natürlich könnten wir das tun. Aber zum einen wird dann unser Beispiel zunehmend langweilig und außerdem ist es irgendwie beruhigender, im Normalfall irgendeinen Inhalt in der Sektion zu bekommen und nicht einfach nur nichts. So kann man auf dem Checkmk-Server sofort erkennen, dass das Agentenplugin korrekt funktioniert.

2.3. Den Befehl in den Agenten einbauen

Damit wir vom Checkmk-Server aus diese Daten abrufen können, müssen wir den neuen Befehl Teil vom Checkmk-Agenten auf dem überwachten System machen. Wir könnten dazu natürlich einfach dort die Datei /usr/bin/check_mk_agent editieren und das einbauen. Das hätte dann aber den Nachteil, dass bei einem Softwareupdate des Agenten unser Befehl wieder verschwindet, weil die Datei ersetzt wird.

Besser ist daher, wenn wir ein Agentenplugin machen. Das ist sogar noch einfacher. Alles was wir brauchen, ist eine ausführbare Datei mit unserem Befehl im Verzeichnis /usr/lib/check_mk_agent/plugins.

Und noch eins ist wichtig: Wir können unsere Daten nicht einfach so ausgeben. Was wir noch brauchen, ist ein Sektionskopf (section header). Das ist eine speziell formatierte Zeile, in der der Name unseres neuen Checks steht. An diesen Sektionsköpfen kann Checkmk später erkennen, wo die Daten des Plugins beginnen und die des vorherigen aufhören.

Also brauchen wir jetzt erst einmal einen sinnvollen Namen für unseren neuen Check. Dieser Name darf nur Kleinbuchstaben (nur a-z, keine Umlaute, keine Akzente), Unterstriche und Ziffern enthalten und muss eindeutig sein. Vermeiden Sie Namenskollisionen mit vorhandenen Check-Plugins. Wenn Sie neugierig sind, welche Namen es schon gibt, können Sie diese in einer Checkmk-Instanz auf der Kommandozeile mit cmk -L auflisten lassen:

OMD[mysite]:~$ cmk -L | head -n 20
3par_capacity                agent      HPE 3PAR: Capacity
3par_cpgs                    agent      HPE 3PAR: CPGs
3par_cpgs_usage              agent      HPE 3PAR: CPGs Usage
3par_hosts                   agent      HPE 3PAR: Hosts
3par_ports                   agent      HPE 3PAR: Ports
3par_remotecopy              agent      HPE 3PAR: Remote Copy
3par_system                  agent      HPE 3PAR: System
3par_volumes                 agent      HPE 3PAR: Volumes
3ware_disks                  agent      3ware ATA RAID Controller: State of Disks
3ware_info                   agent      3ware ATA RAID Controller: General Information
3ware_units                  agent      3ware ATA RAID Controller: State of Units
acme_agent_sessions          snmp       ACME Devices: Agent Sessions
acme_certificates            snmp       ACME Devices: Certificates
acme_fan                     snmp       ACME Devices: Fans
acme_powersupply             snmp       ACME Devices: Power Supplies
acme_realm                   snmp       ACME Devices: Realm
acme_sbc                     agent      ACME SBC: Health
acme_sbc_settings            agent      ACME SBC: Health Settings
acme_sbc_snmp                snmp       ACME SBC: Health (via SNMP)
acme_temp                    snmp       ACME Devices: Temperature

Die zweite Spalte zeigt an, wie das jeweilige Checkplugin seine Daten bezieht.

Wählen wir für unser Beispiel den Namen linux_usbstick. In diesem Fall muss der Sektionskopf so aussehen:

<<<linux_usbstick>>>

Den können wir einfach mit echo ausgeben. Wenn wir dann noch den „Shebang“ nicht vergessen (das ist kein giftiger Stachel aus dem Wüstenplaneten sondern eine Abkürzung für sharp und bang, wobei letzteres eine Abkürzung für das Ausrufezeichen ist!), an dem Linux erkennt, dass es das Skript mit der Shell ausführen soll, dann sieht unser Plugin so aus:

/usr/lib/check_mk_agent/plugins/linux_usbstick
#!/bin/sh
echo '<<<linux_usbstick>>>'
ls /dev/disk/by-id/ | grep -v -- -part

Als Dateiname haben wir jetzt einfach auch linux_usbstick verwendet, auch wenn der eigentlich egal ist. Aber eines ist noch sehr wichtig: Machen Sie die Datei ausführbar!

root@linux# chmod +x /usr/lib/check_mk_agent/plugins/linux_usbstick

Natürlich können Sie das Plugin ganz einfach von Hand ausprobieren, indem Sie den kompletten Pfad als Befehl eingeben:

root@linux# /usr/lib/check_mk_agent/plugins/linux_usbstick
<<<linux_usbstick>>>
ata-APPLE_SSD_SM0512F_S1K5NYBF810191
wwn-0x5002538655584d30

2.4. Agent ausprobieren

Wie immer sind Test und Fehlersuche am wichtigsten. Am besten gehen Sie in drei Schritten vor:

  1. Plugin solo ausprobieren. Das haben wir gerade gemacht.

  2. Agent als ganzes lokal testen.

  3. Agent vom Checkmk-Server aus abrufen.

Das lokale Testen des Agenten ist sehr einfach. Rufen Sie als root den Befehl check_mk_agent auf. Irgendwo in der Ausgabe muss die neue Sektion erscheinen:

root@linux# check_mk_agent

Hier ist ein Ausschnitt der Ausgabe, welcher die neue Sektion enthält:

<<<lnx_thermal:sep(124)>>>
thermal_zone0|-|BAT0|35600
thermal_zone1|-|x86_pkg_temp|81000|0|passive|0|passive
<<<local>>>
<<<linux_usbstick>>>
ata-APPLE_SSD_SM0512F_S1K5NYBF810191
wwn-0x5002538655584d30
<<<lnx_packages:sep(124):persist(1589463274)>>>
accountsservice|0.6.45-1ubuntu1|amd64|deb|-||install ok installed
acl|2.2.52-3build1|amd64|deb|-||install ok installed
acpi|1.7-1.1|amd64|deb|-||install ok installed

Durch Anhängen von less können Sie in der Ausgabe blättern (drücken Sie die Leertaste zum Blättern, / zum Suchen und Q zum Beenden):

root@linux# check_mk_agent | less

Der dritte Test ist dann direkt von der Checkmk-Instanz aus. Nehmen Sie den Host ins Monitoring auf (z.B. als myserver01) und rufen Sie die Agentendaten dann mit cmk -d ab. Hier sollte die gleiche Ausgabe kommen:

OMD[mysite]:~$ cmk -d myserver01 | less

Übrigens: grep hat mit -A eine Option, nach jedem Treffer noch einige Zeilen mehr auszugeben. Damit können Sie bequem die Sektion suchen und ausgeben:

root@linux# cmk -d myserver01 | grep -A5 '^<<< linux_usbstick'
<<<linux_usbstick>>>
ata-APPLE_SSD_SM0512F_S1K5NYBF810191
wwn-0x5002538655584d30
<<<lnx_packages:sep(124):persist(1589463559)>>>
accountsservice|0.6.45-1ubuntu1|amd64|deb|-||install ok installed

Wenn das funktioniert, ist Ihr Agent vorbereitet! Und was haben wir dafür gemacht? Wir haben lediglich ein dreizeiliges Skript mit dem Pfad /usr/lib/check_mk_agent/plugins/linux_usbstick erzeugt und ausführbar gemacht!

Alles was nun folgt, geschieht nur noch auf dem Checkmk-Server: Dort schreiben wir das eigentliche Checkplugin.

2.5. Die Sektion deklarieren

Das Vorbereiten des Agenten ist zwar der komplizierteste Teil, aber nur die halbe Miete. Jetzt müssen wir Checkmk noch beibringen, wie es mit den Informationen und der neuen Agentensektion umgehen soll, welche Services es erzeugen soll, wann diese auf OK oder CRIT gehen sollen usw. All dies machen wir durch die Programmierung eines Checkplugins in Python.

Für Ihre eigenen Checkplugins finden Sie ein Verzeichnis vorbereitet in der local-Hierarchie des Instanzverzeichnisses. Dieses lautet local/lib/check_mk/base/plugins/agent_based/. Hier im Pfad bedeutet base den Teil von Checkmk, der für das eigentlich Monitoring und die Alarmierung zuständig ist. Das agent_based ist für alle Plugins, die sich auf den Checkmk-Agenten beziehen (also z.B. nicht Alarmierungsplugins). Am einfachsten, Sie wechseln zum Arbeiten dort hinein:

OMD[mysite]:~$ cd local/lib/check_mk/base/plugins/agent_based

Das Verzeichnis gehört dem Instanzbenutzer und ist daher für Sie schreibbar. Sie können Ihr Plugin mit jedem auf dem Linux-System installierten Texteditor bearbeiten.

Legen wir also unser Plugin hier an. Konvention ist, dass der Dateiname den Namen der Agentensektion wiedergibt. Pflicht ist, dass die Datei mit .py endet, denn ab Version 2.0.0 von Checkmk handelt es sich bei den Plugins immer um echte Pythonmodule.

Als erstes müssen wir die für die Plugins nötigen Funktionen aus anderen Pythonmodulen importieren. Die einfachste Methode dafür ist die mit einem *. Wie Sie vielleicht ahnen können, steckt hier auch eine Versionsnummer der API für die Pluginprogrammierung. Diese ist bis auf weiteres Version 1, was hier durch v1 abgekürzt ist:

local/lib/check_mk/base/plugins/agent_based/linux_usbstick.py
from .agent_based_api.v1 import *

Diese Versionierung ermöglicht es uns in Zukunft eventuell neue Versionen der API parallel zu den bisherigen bereitzustellen, so dass bestehende Checkplugins weiterhin problemlos funktionieren.

Im einfachsten Fall überspringen Sie das explizite deklarieren der Sektion. Wenn Sie eine Parsefunktion implementieren möchten (wozu Ihnen professionelle Entwickler immer raten würden), finden Sie im Abschnitt über Parsefunktionen mehr Informationen.

2.6. Den Check deklarieren

Damit Checkmk weiß, dass es den neuen Check gibt, muss dieser registriert werden. Dies geschieht durch den Aufruf der Funktion register.check_plugin. Dabei müssen Sie immer mindestens vier Dinge angeben:

  1. name: Der Name des Checkplugins. Wenn Sie keinen Ärger bekommen möchten, nehmen Sie hier den gleichen Namen wie bei Ihrer neuen Agentensektion. Damit weiß der Check automatisch, welche Sektion er auswerten soll.

  2. service_name: Der Name des Services wie er dann im Monitoring erscheinen soll.

  3. discovery_function: Die Funktion zum Erkennen von Services dieses Typs (dazu gleich mehr).

  4. check_funktion: Die Funktion zum Durchführen des eigentlichen Checks (auch dazu gleich mehr).

Für unseren Check sieht das dann also so aus:

register.check_plugin(
    name="linux_usbstick",
    service_name="USB stick",
    discovery_function=discover_linux_usbstick,
    check_function=check_linux_usbstick,
)

Versuchen Sie am besten noch nicht, das gleich auszuprobieren, denn natürlich müssen wir die Funktionen discover_linux_usbstick und check_linux_usbstick vorher noch schreiben. Und diese müssen im Quellcode vor obiger Deklaration erscheinen.

2.7. Die Discovery-Funktion schreiben

Eine Besonderheit von Checkmk ist die automatische Erkennung von zu überwachenden Services. Damit dies klappt, muss jedes Checkplugin eine Funktion definieren, welche anhand der Agentenausgaben erkennt, ob ein Service dieses Typs bzw. welche Services des Typs für den betreffenden Host angelegt werden sollen.

Die Discovery-Funktion wird immer dann aufgerufen, wenn für einen Host die Serviceerkennung durchgeführt wird. Sie entscheidet dann ob, bzw. welche Services angelegt werden sollen. In Standardfall bekommt sie genau ein Argument mit dem Namen section. Dieses enthält die Daten der Agentensektion in einem geparsten Format (dazu später mehr).

Wir implementieren folgende simple Logik: Wenn die Agentensektion linux_usbstick vorhanden ist, dann legen wir auch einen passenden Service an. Dann erscheint dieser automatisch auf allen Hosts, wo unser Agentenplugin ausgerollt ist. Das Vorhandensein der Sektion erkennen wir ganz einfach daran, dass unsere Discovery überhaupt aufgerufen wird!

Die Discovery-Funktion muss für jeden anzulegenden Service mittels yield ein Objekt vom Typ Service zurückgeben (nicht mit return). Bei Checks, die pro Host nur einmal auftreten können, benötigt man keine weitere Angaben:

def discover_linux_usbstick(section):
    yield Service()

2.8. Die Check-Funktion schreiben

Somit können wir nun zur eigentlichen Check-Funktion kommen, welche anhand aktueller Agentenausgaben endlich entscheidet, welchen Zustand ein Service annehmen soll. Da unser Check keine Parameter hat und es auch immer nur einen pro Host gibt, wird unsere Funktion ebenfalls mit dem einzigen Argument section aufgerufen.

Da wir diesmal den Inhalt auch wirklich brauchen, müssen wir uns mit dem Format dieses Arguments befassen. Solange Sie keine explizite Parsefunktion definiert haben, zerlegt Checkmk jede Zeile der Sektion anhand von Leerzeichen in eine Liste von Worten. Das Ganze wird dann wiederum eine Liste dieser Wortlisten. Als Endergebnis haben wir also immer eine Liste von Listen.

Im einfachen Fall, in dem unser Agentenplugin nur zwei Devices findet, sieht das dann z.B. so aus (hier gibt es pro Zeile nur ein Wort):

[['ata-APPLE_SSD_SM0512F_S1K5NYBF810191'], ['wwn-0x5002538655584d30']]

Die Check-Funktion geht nun Zeile für Zeile durch und sucht nach einer Zeile, deren erstes (und einziges) Wort mit usb-SCSI_DISK beginnt. Wenn das der Fall ist, wird der Zustand CRIT. Hier ist die Implementierung:

def check_linux_usbstick(section):
    for line in section:
        if line[0].startswith("usb-SCSI_DISK"):
            yield Result(state=State.CRIT, summary="Found USB stick")
            return
    yield Result(state=State.OK, summary="No USB stick found")

Und hier die Erklärung:

  1. Mit for line in section gehen wir in einer Schleife alle Zeilen der Agentenausgabe durch.

  2. Dann prüfen wir, ob das erste Wort der Zeile — das jeweilige Gerät — mit usb-SCSI_DISK beginnt.

  3. Falls ja, erzeugen wir ein Check-Resultat mit dem Status CRIT und dem Text Found USB stick. Und wir beenden dann die Funktion mit einem return.

  4. Falls die Schleife durchlaufen wird, ohne etwas zu finden, erzeugen wir den Status OK und den Text No USB stick found.

2.9. Das ganze Plugin auf einen Blick

Und hier ist das ganze Plugin nochmal komplett:

local/lib/check_mk/base/plugins/agent_based/linux_usbstick.py
from .agent_based_api.v1 import *

def discover_linux_usbstick(section):
    yield Service()

def check_linux_usbstick(section):
    for line in section:
        if line[0].startswith("usb-SCSI_DISK"):
            yield Result(state=State.CRIT, summary="Found USB stick")
            return
    yield Result(state=State.OK, summary="No USB stick found")

register.check_plugin(
    name = "linux_usbstick",
    service_name = "USB stick",
    discovery_function = discover_linux_usbstick,
    check_function = check_linux_usbstick,
)

Und das hier war das Plugin für den Linux-Agenten:

/usr/lib/check_mk_agent/plugins/linux_usbstick
#!/bin/sh
echo '<<<linux_usbstick>>>'
ls /dev/disk/by-id/ | grep -v -- -part

3. Checks mit mehr als einem Service pro Host (Items)

3.1. Grundprinzip

In unserem Beispiel haben wir einen sehr einfachen Check gebaut, der auf einem Host einen Service erzeugt — oder eben nicht. Ein sehr üblicher Fall ist aber natürlich auch, dass es von einem Check mehrere Services auf einem Host geben kann.

Das häufigste Beispiel dafür sind die Dateisysteme eines Hosts. Das Plugin mit dem Namen df legt pro Dateisystem auf dem Host einen Service an. Um diese Services zu unterscheiden, wird der Mountpunkt des Dateisystems (z.B. /var) bzw. der Laufwerksbuchstabe (z.B. C:) in den Namen des Services eingebaut. Das ergibt dann als Servicename z.B. Filesystem /var oder Filesystem C:. Das Wort /var bzw. C: wird hier als Item bezeichnet. Wir sprechen also auch von einem Check mit Items.

Wenn Sie einen Check mit Items bauen möchten, müssen Sie folgende Dinge umsetzen:

  • Die Discovery-Funktion muss jedes der Items, die auf dem Host sinnvollerweise überwacht werden sollen, einen Service generieren.

  • Im Servicenamen müssen Sie das Item mithilfe des Platzhalters %s einbauen (also z.B. "Filesystem %s").

  • Die Check-Funktion wird pro Item einmal separat aufgerufen und bekommt dieses als Argument. Sie muss dann aus den Agentendaten die für dieses Item relevanten Daten herausfischen.

3.2. Ein einfaches Beispiel

Um das ganze praktisch ausprobieren zu können, bauen wir uns einfach eine weitere Agentensektion, die nur Spieldaten ausgibt. Dazu genügt ein kleines Shell-Skript. Die Sektion soll hier im Beispiel foobar heißen:

/usr/lib/check_mk_agent/plugins/foobar
#!/bin/sh
echo "<<<foobar>>>"
echo "West 100 100"
echo "East 197 200"
echo "North 0 50"

Von foobar gibt es hier drei Sektoren: West, East und North (was immer auch das bedeuten mag). In jedem Sektor gibt es eine Anzahl von Plätzen von denen einige belegt sind (z.B. sind in West 100 von 100 Plätzen belegt).

Nun legen wir dazu ein passendes Checkplugin an. Die Registrierung ist wie gehabt, allerdings mit dem wichtigen Unterschied, dass der Servicename jetzt genau einmal ein %s enthält. An dieser Stelle wird später dann von Checkmk der Name des Items eingesetzt:

register.check_plugin(
    name = "foobar",
    service_name = "Foobar Sector %s",
    discovery_function = discover_foobar,
    check_function = check_foobar,
)

Die Discovery-Funktion hat jetzt die Aufgabe, die zu überwachenden Items zu ermitteln. Wie gehabt bekommt sie das Argument section. Und auch hier handelt es sich um eine Liste von Zeilen, welche ihrerseits wiederum Listen von Worten sind. Diese sieht in unserem Beispiel aus aus:

[['West', '100', '100'], ['East', '197', '200'], ['North', '0', '50']]

So eine Liste kann man mit Python prima in einer Schleife durchlaufen und den drei Worten pro Zeile gleich sinnvolle Namen geben:

for sector, _used, _slots in section:
    ...

In jeder Zeile ist das erste Wort — hier der Sektor — unser Item. Immer wenn wir ein Item gefunden haben, geben wir das mit yield zurück, wobei wir ein Objekt vom Typ Service erzeugen, welches den Sektornamen als Item bekommt. Der Unterstrich zeigt an, dass uns die beiden andere Spalten in der Ausgabe erst einmal egal sind, denn bei der Discovery ist es schließlich unerheblich, wie viele Slots belegt sind. Insgesamt sieht das dann so aus:

def discover_foobar(section):
    for sector, _used, _slots in section:
        yield Service(item=sector)

Es wäre natürlich ein Leichtes, hier anhand von beliebigen Kriterien manche Zeilen auszulassen. Vielleicht gibt es ja Sektoren, welche die Größe 0 haben und die man grundsätzlich nie überwachen möchte? Lassen Sie solche Zeilen einfach aus und „yielden“” Sie dafür kein Item.

Wenn dann später der Host überwacht wird, dann wird die Check-Funktion für jeden Service — und damit für jedes Item — separat aufgerufen. Sie bekommt deswegen zusätzlich zur Sektion das Argument item mit dem jeweils gesuchten Item. Jetzt gehen wir wieder alle Zeilen der Reihe nach durch. Dabei suchen wir diejenige Zeile heraus, die zum gewünschten Item gehört:

def check_foobar(item, section):
    for sector, used, slots in section:
        if sector == item:
            ...

Jetzt fehlt nur noch die eigentliche Logik, welche festlegt, wann das Ding denn überhaupt OK, WARN oder CRIT sein soll. Wir machen es hier so:

  • Wenn alle Slots belegt sind, soll das Ding CRIT werden.

  • Wenn weniger als 10 Slots frei sind, dann wird es WARN.

  • Ansonsten OK

Die belegten und die verfügbaren Slots kommen ja immer als Wort zwei und drei in jeder Zeile vor. Aber: es handelt sich hier um Strings, nicht um Zahlen. Diese brauchen wir aber, um vergleichen und rechnen zu können. Daher wandeln wir die Strings mit int() in Zahlen um.

Das Checkergebnis liefern wir dann, indem wir ein Objekt vom Typ Result per yield liefern. Dieses benötigt die Parameter state und summary:

def check_foobar(item, section):
    for sector, used, slots in section:
        if sector == item:
            used = int(used)   # convert string to int
            slots = int(slots)   # convert string to int
            if used == slots:
                s = State.CRIT
            elif slots - used <= 10:
                s = State.WARN
            else:
                s = State.OK
            yield Result(
                state = s,
                summary = f"Used {used} out of {slots} slots")
            return

Dazu noch folgende Hinweise:

  1. Der Befehl return sorgt dafür, dass die Check-Funktion nach dem Bearbeiten des gefundenen Items sofort abgebrochen wird. Es gibt schließlich auch nichts mehr weiter zu tun.

  2. Wird die Schleife durchlaufen, ohne das gesuchte Item zu finden, so erzeugt Checkmk automatisch das Resultat UNKNOWN - Item not found in monitoring data. Das ist so gewollt und gut so. Behandeln Sie diesen Fall nicht selbst. Wenn sie ein gesuchtes Item nicht finden, so lassen sie Python einfach aus der Funktion rauslaufen und Checkmk seine Arbeit erledigen.

  3. Mit dem Argument summary definieren Sie den Text, den der Service als Statusausgabe produziert. Er ist rein informell und wird von Checkmk nicht weiter ausgewertet.

Übrigens gibt es für den häufigen Fall, dass Sie eine einfache Metrik auf Schwellwerte prüfen wollen, die Hilfsfunktion check_levels. Diese Hilfsfunktion wird in der Check-API-Dokumentation erläutert, die Sie in Checkmk über Help > Plugin API reference > Agent based API (“Check API”) aufrufen können.

Probieren wir jetzt zunächst die Discovery aus. Der Übersicht halber beschränken wir das ganze mit der Option --detect-plugins=foobar auf unser Plugin:

OMD[mysite]:~$ cmk --detect-plugins=foobar -vI myhost123
  3 foobar
SUCCESS - Found 3 services, 1 host labels

Und jetzt können wir auch gleich das Checken ausprobieren (ebenfalls auf foobar begrenzt):

OMD[mysite]:~$ cmk --detect-plugins=foobar -v myhost123
Foobar Sector East   WARN - used 197 out of 200 slots
Foobar Sector North  OK - used 0 out of 50 slots
Foobar Sector West   CRIT - used 100 out of 100 slots

3.3. Beispiel komplett

Und hier nochmal das ganze Beispiel komplett. Damit es keine Fehler wegen nicht definierter Funktionsnamen gibt, müssen die Funktionen immer vor dem Registrieren definiert werden.

local/lib/check_mk/base/plugins/agent_based/foobar.py
from .agent_based_api.v1 import *
import pprint


def discover_foobar(section):
    for sector, used, slots in section:
        yield Service(item=sector)


def check_foobar(item, section):
    for sector, used, slots in section:
        if sector == item:
            used = int(used)    # convert string to int
            slots = int(slots)  # convert string to int
            if used == slots:
                s = State.CRIT
            elif slots - used <= 10:
                s = State.WARN
            else:
                s = State.OK
            yield Result(
                state = s,
                summary = f"used {used} out of {slots} slots")
            return


register.check_plugin(
    name = "foobar",
    service_name = "Foobar Sector %s",
    discovery_function = discover_foobar,
    check_function = check_foobar,
)

4. Messwerte

4.1. Werte in der Check-Funktion ermitteln

Nicht immer, aber oft befassen sich Checks mit Zahlen. Mit seinem Graphingsystem hat Checkmk eine Komponente, um solche Zahlen zu speichern, auszuwerten und darzustellen. Das geht dabei völlig unabhängig von der Berechnung der Zuständige OK, WARN und CRIT.

Solche Messwerte — oder auch Metriken genannt — werden von der Check-Funktion ermittelt und einfach als zusätzliches Ergebnis zurückgegeben. Dazu dient das Objekt Metric, welches mindestens die beiden Argumente name und value benötigt. Hier ist ein Beispiel:

    yield Metric("fooslots", used)

4.2. Informationen zu den Schwellwerten

Weiterhin gibt es noch zwei optionale Argumente. Mit dem Argument levels können Sie eine Information zu Schwellwerten für WARN und CRIT mitgeben, und zwar in Form eines Paares von zwei Zahlen. Diese wird dann üblicherweise im Graphen als gelbe und rote Linie eingezeichnet. Die erste Zahl steht für die Warnschwelle, die zweite für die kritische. Dabei gilt die Konvention, dass der Check beim Erreichen der Warnschwelle bereits auf WARN geht (bei CRIT analog).

Das sieht dann z.B. so aus (hier mit hartkodierten Schwellwerten):

    yield Metric("fooslots", used, levels=(190,200))

Hinweise:

  • Falls nur eine der beiden Schwellen definiert ist, tragen Sie für die andere einfach None ein, also z.B. levels=(None, 200).

  • Es sind auch Fließkommazahlen erlaubt, aber keine Strings.

  • Achtung: für die Überprüfung der Schwellwerte ist die Check-Funktion selbst verantwortlich. Die Angabe von levels dient lediglich als Randinformation für das Graphingsystem!

4.3. Der Wertebereich

Analog zu den Schwellwerten können Sie dem Graphingsystem auch die Information über den möglichen Wertebereich mitgeben. Damit ist der kleinste und größte mögliche Wert gemeint. Das geschieht im Argument boundaries, wobei auch hier optional für eine der beiden Grenzen None eingesetzt werden kann. Beispiel:

    yield Metric(name="fooslots", value=used, boundaries=(0, 200))

Und jetzt unsere Check-Funktion aus dem obigen Beispiel nochmal, aber diesmal mit der Rückgabe von Metrikinformation inklusive Schwellwerte und Wertebereich (diesmal natürlich nicht mit fixen sondern mit berechneten Werten):

def check_foobar(item, section):
    for sector, used, slots in section:
        if sector == item:
            used = int(used)    # convert string to int
            slots = int(slots)  # convert string to int

            yield Metric(
                "fooslots",
                used,
                levels=(slots-10, slots),
                boundaries=(0, slots))

            if used == slots:
                s = State.CRIT
            elif slots - used <= 10:
                s = State.WARN
            else:
                s = State.OK
            yield Result(
                state = s,
                summary = f"used {used} out of {slots} slots")
            return

5. Checks mit mehreren Teilresultaten

Um die Anzahl der Services auf einem Host nicht ins Unermessliche steigen zu lassen, sind in einem Service oft mehrere Teilresultate zusammengefasst. So prüft z.B. der Service Memory used unter Linux nicht nur den RAM- und Swap-Nutzung, sondern auch Shared memory, Page-Tabellen und alles mögliche Andere.

Die API von Checkmk bietet dafür eine sehr komfortable Schnittstelle. So darf eine Check-Funktion einfach beliebig oft ein Ergebnis mit yield erzeugen. Der Gesamtstatus des Services richtet sich dann nach dem „schlechtesten“ Teilergebnis nach dem Schema OKWARNUNKNOWNCRIT.

Hier ist ein gekürztes fingiertes Beispiel:

def check_foobar(section):
    yield Result(state=State.OK, summary="Knulf rate optimal")
    # ...
    yield Result(state=State.WARN, summary="Gnarz required")
    # ...
    yield Result(state=State.OK, summary="Last Szork was good")

Die Summary des Services in der GUI sieht dann so aus: „Knulf rate optimal, Gnarz required WARN, Last Szork was good“. Und der Gesamtstatus ist WARN.

Auf die gleiche Art können Sie auch mehrere Metriken zurückgeben. Rufen Sie einfach für jede Metrik einmal yield Metric(…​) auf.

6. Summary und Details

Im Monitoring von Checkmk hat jeder Service neben dem Status OK, WARN, usw. auch eine Zeile Text. Diese hieß bis zur Version 1.6.0 Output of check plugin. Ab 2.0.0 heißt diese Summary — hat also die Aufgabe einer knappen Zusammenfassung des Zustandes. Die Idee ist, dass dieser Text eine Länge von 60 Zeichen nicht überschreitet. Das sorgt dann immer für eine übersichtliche Tabellendarstellung ohne störende Zeilenumbrüche.

Daneben gibt es noch das Feld Details, welches früher Long output of check plugin (multiline) hieß. Hier werden alle Details des Zustandes angezeigt, wobei die Idee ist, dass alle Informationen des Summary hier auch enthalten sind.

Beim Aufruf von yield Result(…​) können Sie bestimmen, welche Informationen so wichtig sind, dass sie in der Summary angezeigt werden sollen und bei welchen es genügt, dass diese in den Details erscheinen. Dabei gilt die Regel, dass Teilergebnisse, die zu einem WARN/CRIT führen, immer in der Summary sichtbar werden.

In unseren Beispielen bisher haben wir immer folgenden Aufruf verwendet:

    yield Result(state=State.OK, summary="some important text")

Dieser führt dazu, dass some important text immer in der Summary erscheint — und zusätzlich auch in den Details. Dies sollten sie also nur für wichtige Informationen verwenden. Ist ein Teilergebnis eher untergeordnet, so ersetzen Sie summary durch notice und der Text erscheint — falls der Service OK ist nur in den Details.

    yield Result(state=State.OK, notice="some additional text")

Falls der Zustand WARN oder CRIT ist, taucht der Text dann automatisch zusätzlich in der Summary auf:

    yield Result(state=State.CRIT, notice="some additional text")

Somit wird aus der Summary sofort klar, warum der Service nicht OK ist.

Zu guter Letzt haben Sie noch — sowohl bei summary als auch bei notice die Möglichkeit, für die Details einen alternativen Text anzugeben, der evtl. mehr Informationen zu dem Teilergebnis enthält:

    yield Result(state=State.OK,
                 summary="55% used space",
                 details="55.2% of 160 GB used (82 GB)")

Zusammengefasst bedeutet das:

  • Der Gesamttext für die Summary sollte (bei Services, die OK sind) nicht länger als 60 Zeichen sein.

  • Verwenden Sie immer entweder summary oder notice — nicht beides und nicht keines davon.

  • Fügen Sie bei Bedarf details hinzu, wenn der Text für die Details ein alternativer sein soll.

7. Fehlerbehandlung

7.1. Exceptions und Crashreports

Die korrekte Behandlung von Fehlern nimmt (leider) einen großen Teil der Programmierarbeit ein. Die gute Nachricht ist: die API von Checkmk erledigt dabei bereits die meiste Arbeit. Meistens ist für Sie daher wichtig, dass Sie Fehler einfach gar nicht behandeln.

Wenn Python in eine Situation kommt, die in irgendeiner Form unerwartet ist, reagiert es mit einer sogenannten Exception. Hier sind ein paar Beispiele:

  • Sie konvertieren mit int(…​) einen String in eine Zahl, aber der String enthält keine Zahl, z.B. int("foo")

  • Sie greifen mit bar[4] auf das fünfte Element von bar zu, aber das hat nur vier Elemente.

  • Sie rufen eine Funktion auf, die es nicht gibt.

Hier gilt die generelle wichtige Regel: Fangen Sie Exceptions nicht selbst ab! Denn Checkmk übernimmt das für Sie in einer sinnvollen immer gleichen Art und Weise. Und zwar meist mit einem Crashreport. Das sieht dann z.B. so aus:

crash report 1

Durch einen Klick auf das Icon icon crash gelangt der Anwender dann auf eine Seite, auf der er:

  • die Datei angezeigt bekommt, in der der Crash stattgefunden hat;

  • alle Informationen über den Crash angezeigt bekommt (wie Fehlermeldung, Aufrufstack, Agentenausgabe, aktuelle Werte von lokalen Variablen und vieles mehr);

  • den Report zu uns als Feedback einsenden kann.

Das Einsenden des Reports macht natürlich nur Sinn für Checkplugins, welche offiziell Teil von Checkmk sind. Aber Sie können Ihre Anwender bitten, Ihnen die Daten einfach zukommen zu lassen. Diese werden Ihnen beim Finden des Fehlers helfen. Oft ist es ja so, dass das Checkplugin bei Ihnen selbst funktioniert, aber es bei anderen Anwendern vielleicht sehr sporadisch zu Fehlern kommt. Diese können Sie dann so meist sehr leicht finden.

Falls Sie stattdessen die Exception selbst abfangen würden, wären diese ganzen Informationen nicht verfügbar. Sie würden vielleicht den Service auf UNKNOWN setzen und eine Fehlermeldung ausgeben. Aber die ganzen Umstände, wie es dazu kam (z.B. die Daten vom Agenten), wäre verschleiert.

7.2. Exceptions auf der Kommandozeile ansehen

Falls Sie ihr Plugin auf der Kommandozeile ausführen, werden keine Crashreports erzeugt. Sie sehen nur die zusammengefasste Fehlermeldung:

OMD[mysite]:~$ cmk -II --detect-plugins=foobar myhost123
  WARNING: Exception in discovery function of check plugin 'foobar': invalid literal for int() with base 10: 'foo'

Aber: hängen Sie einfach die Option --debug dran. Dann bekommt Sie den Python-Stacktrace:

OMD[mysite]:~$ cmk --debug -II --detect-plugins=foobar myhost123
Traceback (most recent call last):
  File "/omd/sites/myhost123/bin/cmk", line 82, in <module>
    exit_status = modes.call(mode_name, mode_args, opts, args)
  File "/omd/sites/myhost123/lib/python3/cmk/base/modes/init.py", line 68, in call
    return handler(*handler_args)
  File "/omd/sites/myhost123/lib/python3/cmk/base/modes/check_mk.py", line 1577, in mode_discover
    discovery.do_discovery(set(hostnames), options.get("checks"), options["discover"] == 1)
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 345, in do_discovery
    _do_discovery_for(
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 397, in _do_discovery_for
    discovered_services = _discover_services(
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 1265, in _discover_services
    service_table.update({
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 1265, in <dictcomp>
    service_table.update({
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 1337, in _execute_discovery
    yield from _enriched_discovered_services(hostname, check_plugin.name, plugins_services)
  File "/omd/sites/myhost123/lib/python3/cmk/base/discovery.py", line 1351, in _enriched_discovered_services
    for service in plugins_services:
  File "/omd/sites/myhost123/lib/python3/cmk/base/api/agent_based/register/check_plugins.py", line 69, in filtered_generator
    for element in generator(*args, **kwargs):
  File "/omd/sites/myhost123/local/lib/python3/cmk/base/plugins/agent_based/foobar.py", line 5, in discover_foobar
    int("foo")
ValueError: invalid literal for int() with base 10: 'foo'

7.3. Ungültige Ausgaben vom Agenten

Die Frage ist, wie Sie reagieren sollen, wenn die Ausgaben vom Agenten nicht die Form haben, die Sie eigentlich erwarten würden - egal ob es der „echte“ Agent ist oder die Daten per SNMP kommen. Nehmen wir an, dass Sie pro Zeile immer drei Worte erwarten. Was sollen Sie tun, falls nur zwei kommen?

Nun — wenn das ein erlaubtes und bekanntes Verhalten des Agenten ist, dann müssen Sie das natürlich abfangen und mit einer Fallunterscheidung arbeiten.

Falls das aber eigentlich nicht sein darf …​ dann tun Sie am besten so, als ob die Zeile immer aus drei Worten besteht, also z.B. mit:

def check_foobar(section):
    for foo, bar, baz in section:
        # ...

Sollte jetzt mal eine Zeile dabei sein, die nicht aus genau drei Worten besteht, wird eine hübsche Exception erzeugt und Sie bekommen den gerade erwähnten sehr hilfreichen Crashreport.

7.4. Fehlende Items

Was ist, wenn der Agent korrekte Daten ausgibt, aber das Item fehlt, das überprüft werden soll? Also z.B. auf diese Art:

def check_foobar(item, section):
    for sector, used, slots in section:
        if item == sector:
            # ... Check state ...
            yield Result(...)
            return

Ist das gesuchte Item nicht dabei, so wird die Schleife durchlaufen und Python fällt am Ende der Funktion einfach hinten raus, ohne dass ein Resultat „geyieldet“ wurde. Und das ist genau das Richtige! Denn daran erkennt Checkmk, dass das zu überwachende Item fehlt und erzeugt mit UNKNOWN den richtigen Status und einen passenden Standardtext dazu.

8. SNMP-basierte Checks

8.1. Grundsätzliches

Das Entwickeln von Checks, die mit SNMP arbeiten, läuft sehr ähnlich zu den agentenbasierten, nur dass Sie hier noch angeben müssen, welche SNMP-Bereiche (OIDs) der Check benötigt. Falls Sie noch keine Erfahrung mit SNMP haben, so empfehlen wir Ihnen an dieser Stelle als Vorbereitung unbedingt den Artikel über das Monitoring via SNMP.

Der Ablauf der Discovery und des Checkens via SNMP ist etwas anders als beim normalen Agenten. Denn anders also dort — wo der Agent von sich aus alle interessanten Informationen sendet — müssen wir bei SNMP selbst genau sagen, welche Datenbereiche wir benötigen. Ein Komplettabzug aller Daten wäre zwar theoretisch möglich (via SNMP-Walk), dauert aber bei schnellen Geräten eher im Bereich von Minuten und bei komplexen Switches gern auch über eine Stunde. Daher scheidet das beim Checken und sogar auch bei der Discovery aus. Checkmk geht deswegen etwas zielgerichteter vor.

SNMP-Detection

Die Serviceerkennung teilt sich in zwei Phasen auf. Zunächst geschieht die SNMP-Detection. Diese ermittelt, welche Plugins denn überhaupt auf dem jeweiligen Gerät interessant sind. Dazu werden einige wenige SNMP-OIDs abgerufen — und zwar einzelne, ohne Walk. Die wichtigste davon ist die sysDescr (OID: 1.3.6.1.2.1.1.1.0). Unter dieser OID hält jedes SNMP-Gerät eine Beschreibung von sich selbst bereit, z.B. „Cisco NX-OS(tm) n5000, Software (n5000-uk9),…​“.

Ausgehend von diesem Text kann man für sehr viele Plugins schon definitiv entscheiden, ob diese hier Sinn ergeben. Wenn der Text noch nicht spezifisch genug ist, werden weitere OIDs geholt und geprüft. Ergebnis der SNMP-Detection ist dann eine Kandidaten-Liste von Checkplugins.

Discovery

Im zweiten Schritt werden für jeden dieser Kandidaten die jeweils nötigen Monitoring-Daten mit SNMP-Walks geholt. Diese werden dann zu einer Tabelle zusammengefasst und der Discovery-Funktion des Checks in dem Argument section bereitgestellt, welche dann daraus wie gewohnt die zu überwachenden Items ermittelt.

Checken

Beim Checken ist ja schon bekannt, welche Plugins für das Gerät ausgeführt werden sollen und die SNMP-Detection entfällt. Hier werden gleich per SNMP-Walks die für die Plugins benötigten Monitoring-Daten geholt und daraus das Argument section für die Check-Funktion befüllt.

Zusammenfassung

Was müssen Sie also bei einem SNMP-Check anders machen als bei einem agentenbasierten?

  1. Sie benötigen kein Plugin für den Agenten.

  2. Sie müssen die für die SNMP-Detection nötigen Einzel-OIDs und Suchtexte festlegen.

  3. Sie müssen festlegen, welche SNMP-Bereiche für das Monitoring geholt werden müssen.

8.2. Ein Wort zu den MIBs

Bevor wir weitermachen wollen wir hier noch ein Wort zu den berüchtigten SNMP-MIBs verlieren, denn über diese gibt es viele Vorurteile. Gleich zu Beginn eine gute Nachricht: Checkmk benötigt sie nicht. Wirklich! Sie sind aber eine wichtige Hilfe, um einen SNMP-Check entwickeln zu können.

Was ist nun eine MIB? Wörtlich bedeutet die Abkürzung Management Information Base — etwas nichtssagend. Konkret ist eine MIB eine ganz gut lesbare Textdatei, welche einen bestimmten Teilbaum der SNMP-Welt beschreibt. Und zwar steht hier, welcher Ast im Baum — also welche OID — welche Bedeutung hat. Das umfasst einen Namen für die OID, einen Hinweis, welche Werte diese annehmen kann (z.B. bei enumerierten Datentypen, wo dann Dinge wie 1=up, 2=down, etc. festgelegt sind) und manchmal auch noch einen nützlichen Kommentar.

Checkmk liefert eine Reihe von frei verfügbaren MIB-Dateien mit aus. Diese beschreiben sehr allgemeine Bereiche im globalen OID-Baum, enthalten aber keine herstellerspezifischen Bereiche. Daher helfen sie für selbst entwickelte Checks nicht viel weiter.

Versuchen Sie also, die für Ihr spezielle Gerät relevanten MIB-Dateien irgendwo auf den Webseiten vom Hersteller oder sogar auf dem Management-Interface des Geräts zu finden und installieren Sie diese in der Checkmk-Instanz nach local/share/check_mk/mibs. Dann können Sie in SNMP-Walks OID-Nummern in Namen umrechnen lassen und so schneller finden, wo die für das Monitoring interessanten Daten sind. Wie gesagt, enthalten die MIBs außerdem interessante Informationen in den Kommentaren — wenn sie sorgfältig gemacht sind. Sie können eine MIB-Datei einfach mit einem Texteditor oder mit less ansehen.

8.3. Die richtigen OIDs finden

Die entscheidende Voraussetzung, um ein Plugin zu entwickeln, ist natürlich, dass Sie wissen, welche OIDs die notwendigen Informationen enthalten. Der erste Schritt dabei ist (falls das Gerät das nicht verweigert), einen kompletten SNMP-Walk zu ziehen. Dabei werden alle per SNMP verfügbaren Daten abgerufen.

Checkmk kann das sehr einfach für Sie erledigen. Nehmen Sie dazu zunächst das Gerät (oder eines der Geräte), für das Sie ein Plugin entwickeln wollen, ins Monitoring auf. Sagen wir es heißt mydevice01. Stellen Sie sicher, dass dieses in den Grundfunktionen überwacht werden kann. Zumindest müssen die Services SNMP Info und Uptime gefunden werden und wahrscheinlich auch noch mindestens ein Interface. So stellen Sie sicher, dass der SNMP-Zugriff sauber funktioniert.

Wechseln Sie dann auf die Kommandozeile der Checkmk-Instanz. Hier können Sie mit folgendem Befehl einen kompletten Walk ziehen. Dabei empfehlen wir, gleich die Option -v (verbose) zu verwenden:

OMD[mysite]:~$ cmk -v --snmpwalk mydevice01
mydevice01:
Walk on ".1.3.6.1.2.1"...3898 variables.
Walk on ".1.3.6.1.4.1"...6025 variables.
Wrote fetched data to /omd/sites/heute/var/check_mk/snmpwalks/mydevice01.

Wie bereits erwähnt, kann so ein Komplettwalk Minuten oder sogar Stunden dauern (auch wenn letzteres eher selten ist). Werden Sie also nicht nervös, wenn es hier etwas dauert. Der Walk wurde nun in der Datei var/check_mk/snmpwalks/mydevice01 gespeichert. Es handelt sich dabei um eine gut lesbare Textdatei, die etwa so beginnt:

var/check_mk/snmpwalks/mydevice01
.1.3.6.1.2.1.1.1.0 JetStream 24-Port Gigabit L2 Managed Switch with 4 Combo SFP Slots
.1.3.6.1.2.1.1.2.0 .1.3.6.1.4.1.11863.1.1.3
.1.3.6.1.2.1.1.3.0 546522419
.1.3.6.1.2.1.1.4.0 hh@example.com
.1.3.6.1.2.1.1.5.0 sw-ks-01
.1.3.6.1.2.1.1.6.0 Core Switch Serverraum klein
.1.3.6.1.2.1.1.7.0 3
.1.3.6.1.2.1.2.1.0 27

In jeder Zeile steht eine OID und danach deren Wert. Und gleich in der ersten Zeile finden Sie die wichtigste, nämlich die sysDescr.

Nun sind die OIDs nicht sehr aussagekräftig. Wenn die richtigen MIBs installiert sind, können Sie diese in einem zweiten Schritt mit dem Befehl cmk --snmptranslate in Namen umrechnen lassen. Am besten leiten Sie das Ergebnis, was ansonsten im Terminal käme, in eine Datei um:

OMD[heute]:~$ cmk --snmptranslate mydevice01  > translated
Processing 9923 lines.
finished.

Die Datei translated liest sich wie der ursprüngliche Walk, hat aber in jeder Zeile nach dem --> einen übersetzten Wert für die OID:

translated
.1.3.6.1.2.1.1.1.0 JetStream 24-Port Gigabit L2 Managed Switch with 4 Combo SFP Slots --> SNMPv2-MIB::sysDescr.0
.1.3.6.1.2.1.1.2.0 .1.3.6.1.4.1.11863.1.1.3 --> SNMPv2-MIB::sysObjectID.0
.1.3.6.1.2.1.1.3.0 546522419 --> DISMAN-EVENT-MIB::sysUpTimeInstance
.1.3.6.1.2.1.1.4.0 hh@example.com --> SNMPv2-MIB::sysContact.0
.1.3.6.1.2.1.1.5.0 sw-ks-01 --> SNMPv2-MIB::sysName.0
.1.3.6.1.2.1.1.6.0 Core Switch Serverraum klein --> SNMPv2-MIB::sysLocation.0
.1.3.6.1.2.1.1.7.0 3 --> SNMPv2-MIB::sysServices.0
.1.3.6.1.2.1.2.1.0 27 --> IF-MIB::ifNumber.0
.1.3.6.1.2.1.2.2.1.1.1 1 --> IF-MIB::ifIndex.1
.1.3.6.1.2.1.2.2.1.1.2 2 --> IF-MIB::ifIndex.2

Beispiel: die OID .1.3.6.1.2.1.1.4.0 hat den übersetzten Namen SNMPv2-MIB::sysContact.0. Dies ist ein wichtiger Hinweis, der Rest ist dann Übung, Erfahrung und natürlich experimentieren.

8.4. Die Registrierung der SNMP-Sektion

Wenn Sie also die notwendigen OIDs herausgefunden haben, geht es an die eigentliche Entwicklung des Plugins. Das geschieht in drei Schritten:

  1. Legen Sie für die SNMP-Detection fest, welche OIDs welche Texte enthalten müssen, damit Ihr Plugin ausgeführt werden soll.

  2. Deklarieren Sie, welche OID-Zweige für das Monitoring geholt werden müssen.

  3. Schreiben Sie ein Checkplugin analog zu denjenigen für agentenbasierte Checks.

Die ersten beiden Schritte erfolgen durch die Registrierung einer SNMP-Sektion. Dies erledigen Sie durch den Aufruf von register.snmp_section(). Hier geben Sie mindestens drei Argumente an: den Namen der Sektion (name), die Angaben für die SNMP-Detection detect und die benötigten OID-Zweige für das eigentlich Monitoring (fetch). Hier ist ein Beispiel für ein fiktives Checkplugin mit dem Namen foo:

local/lib/check_mk/base/plugins/agent_based/foo.py
register.snmp_section(
    name = "foo",
    detect = startswith(".1.3.6.1.2.1.1.1.0", "foobar device"),
    fetch = SNMPTree(
        base = '.1.3.6.1.4.1.35424.1.2',
        oids = [
            '4.0',
            '5.0',
            '8.0',
        ],
    ),
)

Die SNMP-Detection

Mit dem Schlüsselwort detect geben Sie an, unter welchen Bedingungen die Discovery-Funktion überhaupt ausgeführt werden soll. In unserem Beispiel ist das der Fall, wenn der Wert der OID .1.3.6.1.2.1.1.1.0 (also die sysDescr) mit dem Text foobar device beginnt (wobei Groß-/Kleinschreibung grundsätzlich nicht unterschieden wird). Neben startswith gibt es noch eine ganze Reihe weiterer möglichen Attribute. Dabei existiert von jedem auch eine negierte Form, welche mit not_ beginnt:

AttributNegationBedeutung

equals(oid, needle)

not_equals(oid, needle)

Der Wert der OID ist gleich dem Text needle

contains(oid, needle)

not_contains(oid, needle)

Der Wert der OID enthält an irgendeiner Stelle den Text needle

startswith(oid, needle)

not_startswith(oid, needle)

Der Wert der OID beginnt mit dem Text needle

endswith(oid, needle)

not_endswith(oid, needle)

Der Wert der OID endet mit dem Text needle

matches(oid, regex)

not_matches(oid, regex)

Der Wert der OID matcht auf den regulären Ausdruck regex, und zwar hinten und vorne geankert, also mit einem exakten Match. Wenn Sie nur einen Teilstring benötigen, ergänzen Sie einfach vorne bzw. hinten noch ein .*

exists(oid)

not_exists(oid)

Erfüllt, wenn die OID auf dem Gerät verfügbar ist. Der Wert darf leer sein.

Daneben gibt es noch die Möglichkeit, mehrere Tests mit all_of oder any_of zu verknüpfen. all_of erfordert mehrere erfolgreiche Attribute für eine positive Erkennung des Plugins. Folgendes Beispiel findet auf einem Gerät das Plugin, wenn in der sysDescr der Text mit foo (oder FOO oder Foo) beginnt und die OID .1.3.6.1.2.1.1.2.0 den Text .4.1.11863. enthält:

detect = all_of(
    startswith(".1.3.6.1.2.1.1.1.0", "foo"),
    contains(".1.3.6.1.2.1.1.2.0", ".4.1.11863.")
)

any_of hingegen ist damit zufrieden, wenn auch nur eines der Kriterien erfüllt ist. Hier ist ein Beispiel, in dem verschiedene Werte für die sysDescr erlaubt sind:

detect = any_of(
    startswith(".1.3.6.1.2.1.1.1.0", "foo version 3 system"),
    startswith(".1.3.6.1.2.1.1.1.0", "foo version 4 system"),
    startswith(".1.3.6.1.2.1.1.1.0", "foo version 4.1 system"),
)

Übrigens: Kennen Sie sich gut mit regulären Ausdrücken aus? Dann würden Sie wahrscheinlich das ganze vereinfachen und doch wieder mit einer Zeile auskommen:

detect = matches(".1.3.6.1.2.1.1.1.0", "FOO Version (3|4|4.1) .*"),

Und noch ein wichtiger Hinweis: Die OIDs, die Sie bei der detect-Deklaration von einem Plugin angeben, werden im Zweifel von jedem Gerät geholt, welches per SNMP überwacht wird. Seien Sie daher sehr sparsam bei der Verwendung von herstellerspezifischen OIDs. Versuchen Sie, Ihre Erkennung unbedingt so zu machen, dass ausschließlich die sysDescr (.1.3.6.1.2.1.1.1.0) und die sysObjectID (.1.3.6.1.2.1.1.2.0) verwendet werden. Falls Sie dennoch eine weitere andere OID benötigen, dann reduzieren Sie die Anzahl der Geräte, wo diese angefragt wird, auf ein Minimum, indem Sie zuvor mittels der sysDescr so viele Geräte wie möglich bereits ausschließen, z.B. so:

detect = all_of(
    startswith(".1.3.6.1.2.1.1.1.0", "foo"),   # first check sysDescr
    contains(".1.3.6.1.4.1.4455.1.3", "bar"),  # fetch vendor specific OID
)

Das all_of() funktioniert so, dass bei einem Scheitern der ersten Bedingung die zweite gar nicht erst probiert wird (und somit die betreffende OID auch nicht geholt). Hier im Beispiel wird die OID .1.3.6.1.4.1.4455.1.3 nur bei solchen Geräten geholt, die foo in ihrer sysDescr haben.

Was geschieht, wenn Sie die Deklaration falsch oder zumindest nicht ganz zielsicher gemacht haben?

  • Falls die Detection fälschlicherweise Geräte erkennt, auf denen die nötigen OIDs gar nicht vorhanden sind, wird Ihre Discovery-Funktion dann auch keine Services erzeugen. Es passiert also nichts „Schlimmes“. Allerdings wird das die Discovery auf solchen Geräten verlangsamen, da jetzt jedes mal nutzlos versucht wird, die entsprechenden OIDs abzufragen.

  • Falls die Detection eigentlich zulässige Geräte nicht erkennt, werden dort im Monitoring bei der Discovery auch keine Services gefunden.

8.5. Die OID-Bereiche für das Monitoring

Die wichtigste Stelle der SNMP-Deklaration ist die Angabe, welche OIDs für das Monitoring geholt werden sollen. In fast allen Fällen benötigt ein Plugin dazu nur ausgewählte Äste aus einer einzigen Tabelle. Betrachten wir folgendes Beispiel:

    fetch = SNMPTree(
        base = '.1.3.6.1.4.1.35424.1.2',
        oids = [
            '4.0',
            '5.0',
            '8.0',
        ],
    ),

Das Schlüsselwort base gibt hier einen OID-Präfix an. Alle nötigen Daten liegen unterhalb. Bei oids geben Sie dann eine Liste von Sub-OIDs an, die ab dort geholt werden sollen. In obigem Beispiel werden dann insgesamt drei SNMP-Walks gemacht, nämlich ausgehend von den OIDs .1.3.6.1.4.1.35424.1.2.4.0, .1.3.6.1.4.1.35424.1.2.5.0 und .1.3.6.1.4.1.35424.1.2.8.0. Dabei ist es wichtig, dass diese Walks die gleiche Anzahl von Variablen holen und dass diese auch einander entsprechen. Damit ist gemeint, dass z.B. das n-te Element aus jedem der Walks dem selben überwachten Objekt entspricht.

Hier ist ein Beispiel vom Checkplugin snmp_quantum_storage_info:

    tree = SNMPTree(
       base=".1.3.6.1.4.1.2036.2.1.1",  # qSystemInfo
       oids=[
           "4",   # qVendorID
           "5",   # qProdId
           "6",   # qProdRev
           "12",  # qSerialNumber
       ],
    ),
)

Hier wird pro Storage-Gerät jeweils die Vendor ID, die Product ID, die Product Revision und die Seriennummer geholt.

Der Discovery- und Check-Funktion werden diese Daten als Tabelle präsentiert, also als Liste von Listen. Dabei wird die Tabelle so gespiegelt, dass Sie pro Eintrag in der äußeren Liste alle Daten zu einem Item haben. Jeder Eintrag hat so viele Elemente, wie Sie bei oids angegeben haben. So können Sie die Liste sehr praktisch mit einer Schleife durchlaufen, z.B.

    for vendor_id, prod_id, prod_rev, serial_number in section:
        ...

Bitte beachten Sie:

  • Alle Einträge sind strings, selbst wenn die betreffenden OIDs eigentlich Zahlen sind.

  • Fehlende OIDs werden als Leerstrings präsentiert

  • Denken Sie an die Möglichkeit, während der Entwicklung mit pprint die Daten formatiert auszugeben.

8.6. Weitere SNMP-Sonderheiten

Hier beschreiben wir in Zukunft noch:

  • Wie Sie mehrere unabhängige SNMP-Bereiche abrufen können

  • Was es mit OIDEnd() auf sich hat

  • Weitere Sonderfälle beim Umgang mit SNMP

9. Formatierung von Zahlen

9.1. Grundlegendes

In der Summary oder den Details eines Services werden oft Zahlen ausgegeben. Um Ihnen eine schöne und korrekte Formatierung möglichst einfach zu machen, und um auch die Ausgaben von allen Checkplugins zu vereinheitlichen, gibt es Hilfsfunktionen für die Darstellung von verschiedenen Arten von Größen. Alle diese sind Unterfunktionen vom Modul render und werden folglich mit render. aufgerufen. Z.B. ergibt render.bytes(2000) den Text 1.95 KiB.

Allen diesen Funktionen ist gemein, dass Sie ihren Wert in einer sogenannten kanonischen oder natürlichen Einheit bekommen. So muss man nie nachdenken und es gibt keine Schwierigkeiten oder Fehler bei der Umrechnung. Z.B. werden Zeiten immer in Sekunden angegeben und Größen von Festplatten, Dateien, etc. immer in Bytes und nicht in Kilobytes, Kibibytes, Blöcken oder sonstigem Durcheinander.

Bitte verwenden Sie diese Funktionen auch dann, wenn Ihnen die Darstellung nicht so gut gefällt. Immerhin ist diese dann für den Benutzer einheitlich. Und zukünftige Versionen von Checkmk können die Darstellung möglicherweise ändern oder sogar konfigurierbar für den Benutzer machen. Davon wird dann Ihr Checkplugin auch profitieren.

Nach der ausführlichen Beschreibung aller Darstellungsfunktionen (Renderfunktionen) finden Sie eine Zusammenfassung in Form einer übersichtlichen Tabelle.

9.2. Zeiten, Zeitspannen, Frequenzen

Absolute Zeitangaben (Zeitstempel) werden mit render.date() oder render.datetime() formatiert. Die Angaben erfolgen immer in Sekunden ab dem 1. Januar 1970, 00:00:00 UTC — der sogenannten Epochenzeit. Dies ist auch das Format, mit dem die Pythonfunktion time.time() arbeitet. Vorteil an dieser Darstellung ist, dass sich damit sehr einfach rechnen lässt, also z.B. die Dauer eines Vorgangs, wenn Start- und Endzeit bekannt sind. Die Formel ist dann einfach duration = end - start. Und diese Berechnungen funktionieren unabhängig von der Zeitzone, Sommerzeitumstellungen oder Schaltjahren.

render.date() gibt dabei nur das Datum aus, render.datetime() fügt noch die Uhrzeit hinzu. Die Ausgabe erfolgt dabei gemäß der aktuellen Zeitzone desjenigen Checkmk-Servers, welcher den Check ausführt! Beispiele:

AufrufAusgabe

render.date(0)

Jan 01 1970

render.datetime(0)

Jan 01 1970 01:00:00

render.date(1600000000)

Sep 13 2020

render.datetime(1600000000)

Sep 13 2020 14:26:40

Bitte wundern Sie sich jetzt nicht, dass render.date(0) als Uhrzeit nicht 00:00, sondern 01:00 ausgibt! Das liegt daran, dass wir dieses Handbuch in der Zeitzone von Deutschland schreiben, und die ist der Standardzeit UTC eine Stunde voraus (zumindest während der Normalzeit, denn der 1. Januar liegt ja bekanntlich nicht in der Sommerzeit).

Für Zeitspannen gibt es noch die Funktion render.timespan(). Diese bekommt eine Dauer in Sekunden und gibt das menschenlesbar aus. Bei größeren Zeitspannen werden Sekunden oder Minuten weggelassen.

AufrufAusgabe

render.timespan(1)

1 second

render.timespan(123)

2 minutes 3 seconds

render.timespan(12345)

3 hours 25 minutes

render.timespan(1234567)

14 days 6 hours

Eine Frequenz ist quasi der Kehrwert der Zeit. Die kanonische Einheit ist Hz, was das gleiche bedeutet wie 1 / sec. Einsatzgebiet ist z.B. die Taktrate einer CPU:

AufrufAusgabe

render.frequency(111222333444)

111 GHz

9.3. Bytes

Überall wo es um Arbeitsspeicher, Dateien, Festplatten, Dateisysteme und dergleichen geht, ist die kanonische Einheit das Byte. Da Computer so etwas meist in Zweierpotenzen organisieren, also z.B. in Einheiten zu 512, 1024 oder 65536 Bytes, hatte sich dabei von Beginn an eingebürgert, dass ein Kilobyte nicht 1000, sondern 1024 Bytes ist. An sich sehr praktisch, weil so meist runde Zahlen rauskamen. Der legendäre Commodore C64 hatte eben 64 Kilobyte Speicher und nicht 65,536.

Leider kamen irgendwann Festplattenhersteller auf die Idee, die Größen ihrer Platten in 1000’er-Einheiten anzugeben. Da bei jeder Größenordnung der Unterschied zwischen 1000 und 1024 immerhin 2,4% ausmacht, und diese sich aufmultiplizieren, wird so aus einer Platte der Größe 1 GB (1024 mal 1024 * 1024) auf einmal 1,07 GB. Das verkauft sich besser.

Diese lästige Verwirrung besteht bis heute und sorgt immer wieder für Fehler. Als Linderung wurden von der internationalen elektrotechnischen Kommission neue Präfixe auf Grundlage des Binärsystems festgelegt. Demnach ist heute offiziell ein Kilobyte 1000 Byte und ein Kibibyte 1024 Byte (2 hoch 10). Außerdem soll man Mebibyte und Gibitbyte und Tebibyte sagen (schon mal gehört?). Die Abkürzungen lauten (Achtung, hier auf einmal immer i, statt e!) KiB, MiB, GiB und TiB.

Checkmk passt sich an diesen Standard an und hilft Ihnen mit mehreren angepassten Renderfunktionen dabei, dass Sie immer korrekte Ausgaben machen. So gibt es speziell für Festplatten und Dateisysteme die Funktion render.disksize(), welche die Ausgabe in 1000’er-Potenzen macht.

AufrufAusgabe

render.disksize(1000)

1.00 kB

render.disksize(1024)

1.02 kB

render.disksize(2000000)

2.00 MB

Bei der Größe von Dateien ist es oft üblich, die genaue Größe in Bytes ohne Rundung anzugeben. Dies hat den Vorteil, dass man so sehr schnell sehen kann, wenn sich eine Datei auch nur minimal geändert hat oder dass zwei Dateien (wahrscheinlich) gleich sind. Hierfür ist die Funktion render.filesize() verantwortlich:

AufrufAusgabe

render.filesize(1000)

1,000 B

render.filesize(1024)

1,024 B

render.filesize(2000000)

2,000,000 B

Wenn Sie eine Größe ausgeben möchten, die keine Platten- oder Dateigröße ist, dann verwenden Sie einfach das generische render.bytes(). Hier bekommen sie die Ausgabe in klassischen 1024’er-Potenzen in der neuen offiziellen Schreibweise:

AufrufAusgabe

render.bytes(1000)

1000 B

render.bytes(1024)

1.00 KiB

render.bytes(2000000)

1.91 MiB

9.4. Bandbreiten, Datenraten

Die Netzwerker haben ihre eigenen Begriffe und Arten, Dinge auszudrücken. Und wie immer gibt sich Checkmk Mühe, in jeder Domäne, die dort übliche Art zu kommunizieren, zu übernehmen. Deswegen gibt es für Datenraten und Geschwindigkeiten gleich drei verschiedene Renderfunktionen. Alle haben gemeinsam, dass die Raten in Bytes pro Sekunde übergeben werden, selbst dann, wenn die Ausgabe in Bits erfolgt!

render.nicspeed() stellt die Maximalgeschwindigkeit einer Netzwerkkarte oder eines Switchports dar. Da es keine Messwerte sind, muss auch nicht gerundet werden. Obwohl kein Port einzelne Bits versenden kann, sind die Angaben aus historischen Gründen in Bits. Achtung: trotzdem müssen Sie auch hier Bytes pro Sekunde übergeben! Beispiele:

AufrufAusgabe

render.nicspeed(12500000)

100 MBit/s

render.nicspeed(100000000)

800 MBit/s

render.networkbandwidth() ist für eine tatsächlich gemessene Übertragungsgeschwindigkeit im Netzwerk. Eingabewert sind wieder Bytes pro Sekunde (Oder „Oktette“, wie der Netzwerker sagen würde):

AufrufAusgabe

render.networkbandwidth(123)

984 Bit/s

render.networkbandwidth(123456)

988 kBit/s

render.networkbandwidth(123456789)

988 MBit/s

Wo es nicht ums Netzwerk geht und dennoch Datenraten ausgegeben werden, sind wieder Bytes üblich. Prominentester Fall sind IO-Raten von Festplatten. Dafür gibt es die Renderfunktion render.iobandwidth(), die in Checkmk mit 1000’er-Potzenzen arbeitet:

AufrufAusgabe

render.iobandwidth(123)

123 B/s

render.iobandwidth(123456)

123 kB/s

render.iobandwidth(123456789)

123 MB/s

9.5. Prozentwerte

Die Funktion render.percent() stellt einen Prozentwert dar — auf zwei Nachkommastellen gerundet. Es ist insofern eine Ausnahme zu den anderen Funktionen, als hier nicht der eigentlich natürliche Wert — also das Verhältnis — übergeben wird, sondern wirklich die Prozentzahl. Wenn also etwas z.B. zur Hälfte voll ist, müssen Sie nicht 0.5 sondern 50 übergeben.

Weil es manchmal interessant sein kann zu wissen, ob ein Wert beinahe Null oder exakt Null ist, werden Werte durch Anfügen eines „<“ Zeichens markiert, die größer als Null, aber kleiner als 0.01 sind.

AufrufAusgabe

render.percent(0.004)

<0.01%

render.percent(18.5)

18.50%

render.percent(123)

123.00%

9.6. Zusammenfassung

Hier ist nochmal eine Übersicht über alle Renderfunktionen:

FunktionEingabeBeschreibungBeispielausgabe

date

Epoche

Datum

Dec 18 1970

datetime

Epoche

Datum und Uhrzeit

Dec 18 1970 10:40:00

timespan

Sekunden

Dauer / Alter

3d 5m

frequency

Hz

Frequenz (z.B. Taktrate)

110 MHz

disksize

Bytes

Größe von Festplatte, Basis 1000

1,234 GB

filesize

Bytes

Größe von Dateien, volle Genauigkeit

1,334,560 B

bytes

Bytes

Größe in Bytes, Basis 1024

23,4 KiB

nicspeed

Octets/sec

Geschwindigkeit von Netzwerkkarten

100 MBit/s

networkbandwidth

Octets/sec

Übertragungsgeschwindigkeit

23.50 GBit/s

iobandwidth

Bytes/sec

IO-Bandbreiten

124 MB/s

percent

Prozent

Prozentwert, sinnvoll gerundet

99.997%

10. Schwellwerte und Checkparameter

10.1. Ein Regelsatz für das Setup

In einem unserer bisherigen Beispiele haben wir den Zustand WARN erzeugt, falls nur noch 10 oder weniger Slots frei waren. Dabei war die Zahl 10 direkt in der Checkfunktion fest einprogrammiert - hart codiert, wie Programmierer sagen würden. In Checkmk ist man allerdings als Anwender eher gewohnt, dass man solche Schwellwerte und Parameter per Regel konfigurieren kann. Deswegen wollen wir uns als nächstes ansehen, wie auch Sie Ihren Check so verbessern können, dass er über die Setup-Oberfläche konfigurierbar ist.

Dazu müssen wir zwei Fälle unterscheiden:

  1. Es gibt bereits einen passenden Regelsatz. Das kann eigentlich nur dann der Fall sein, wenn Ihr neuer Check etwas prüft, für das Checkmk in gleicher Form bereits Checkplugins hat, z.B. das Überwachen einer Temperatur. Dafür gibt es bereits einen Regelsatz, den Sie direkt verwenden können.

  2. Es gibt keinen passenden Regelsatz. Dann müssen Sie einen neuen anlegen.

10.2. Verwenden von vorhandenen Regelsätzen

Die ausgelieferten Regelsätze für Parameter von Checks finden Sie im Verzeichnis lib/check_mk/gui/plugins/wato/check_parameters/. Nehmen wir als Beispiel die Datei memory_simple.py. Diese deklariert einen Regelsatz mit folgendem Abschnitt:

lib/check_mk/gui/plugins/wato/check_parameters/memory_simple.py
rulespec_registry.register(
    CheckParameterRulespecWithItem(
        check_group_name="memory_simple",
        group=RulespecGroupCheckParametersOperatingSystem,
        item_spec=_item_spec_memory_simple,
        match_type="dict",
        parameter_valuespec=_parameter_valuespec_memory_simple,
        title=lambda: _("Main memory usage of simple devices"),
    ))

Entscheidend für Sie ist dabei das Schlüsselwort check_group_name, welches hier auf "memory_simple" gesetzt ist. Damit wird die Verbindung zum Checkplugin hergestellt. Das machen Sie beim Registrieren des Checks mit dem Schlüsselwort check_ruleset_name, zum Beispiel:

register.check_plugin(
    name = "foobar",
    service_name = "Foobar Sector %s",
    discovery_function = discover_foobar,
    check_function = check_foobar,
    check_ruleset_name="memory_simple",
    check_default_parameters={},
)

Zwingend notwendig ist dabei auch die Definition von Defaultparametern über das Schlüsselwort check_default_parameters. Diese Parameter gelten für Ihren Check dann, wenn der Benutzer noch keine Regel angelegt hat. Falls es keine verpflichtenden Parameter gibt, können Sie einfach das leere Dictionary {} als Wert nehmen.

Wie der jeweils vom Benutzer konfigurierte Wert dann bei der Checkfunktion ankommt, werden wir dann weiter unten sehen.

10.3. Einen eigenen Regelsatz definieren

Falls es keinen passenden Regelsatz gibt (was wohl eher der Normalfall ist), müssen wir uns selbst einen neuen erzeugen. Dazu legen wir eine Datei im Verzeichnis local/share/check_mk/web/plugins/wato an. Der Name der Datei sollte sich an dem des Checks orientieren und er muss wie alle Plugindateien die Endung .py haben.

Sehen wir uns den Aufbau so einer Datei Schritt für Schritt an. Zunächst kommen einige Importbefehle. Falls die Texte in Ihrer Datei in andere Sprachen übersetzbar sein sollen, importieren Sie _ (Unterstrich). Dies ist eine Funktion und fungiert als Markierung für alle übersetzbaren Texte. Im Weiteren schreiben Sie dann z.B. anstelle von "Threshold for warn" ein _("Threshold for warn") für den Funktionsaufruf.

Das Übersetzungssystem von Checkmk, welches auf gettext basiert, findet solche Texte und übernimmt sie in die Liste der zu übersetzenden Texte auf. Falls Sie den Check nur für sich selbst bauen, können Sie darauf auch verzichten und brauchen den folgenden Importbefehl nicht:

local/share/check_mk/web/plugins/wato/foobar_parameters.py
from cmk.gui.i18n import _

Als nächstes importieren wir sogenannte ValueSpecs. Ein ValueSpec ist ein sehr praktisches und universelles Werkzeug, das Checkmk an vielen Stellen verwendet. Es dient dem Generieren von angepassten Eingabemasken, der Darstellung und Validierung der eingegebenen Werte und der Umwandlung in Python-Datenstrukturen. In folgendem Beispiel werden Dictionary, Integer und TextInput importiert.

from cmk.gui.valuespec import (
    Dictionary,
    Integer,
    TextInput,
)

Das Dictionary benötigen Sie auf jeden Fall. Denn seit Version 2.0.0 von Checkmk ist es zwingend vorgeschrieben, dass Checkparameter Python-Dictionaries sein müssen. Früher konnte es z.B. auch ein Paar (Tupel aus zwei Zahlen) sein (z.B. Warn/Crit).

Integer ist für die Eingabe einer Zahl ohne Kommastellen verantwortlich und TextInput für einen Unicode-Text.

Als nächstes werden noch Symbole importiert, die beim Registrieren benötigt werden:

from cmk.gui.plugins.wato import (
    CheckParameterRulespecWithItem,
    rulespec_registry,
    RulespecGroupCheckParametersOperatingSystem,
)

Falls Ihr Check kein Item hat, importieren Sie stattdessen CheckParameterRulespecWithoutItem. Zur RulespecGroup…​. schreiben wir weiter unten noch etwas.

Nun kommen die eigentlichen Definitionen. Zunächst deklarieren wir ein Eingabefeld, mit dem der Benutzer das Item des Checks angeben kann. Dies ist für die Regelbedingung notwendig, und auch für das manuelle Anlegen von Checks, welche ohne Discovery funktionieren sollen. Das erledigen wir mit TextInput. Dieses bekommt per title einen Titel zugewiesen, welcher dann in der Regel als Überschrift für das Eingabefeld angezeigt wird:

def _item_valuespec_foobar():
    return TextInput(title=_("Sector name"))

Den Namen der Funktion, welche diese ValueSpec zurückgibt, können Sie frei wählen, er wird nur an der Stelle weiter unten benötigt. Damit er nicht über die Modulgrenze hinaus sichtbar wird, sollte er mit einem Unterstrich beginnen.

Als nächstes kommt das ValueSpec für die Eingabe des eigentlichen Checkparameters. Auch hierfür legen wir eine Funktion an, welche dieses erzeugt. Das return Dictionary(…​) ist vorgeschrieben. Innerhalb dessen legen Sie mit elements=[…​] die Liste der Unterparameter für diesen Check an. In unserem Beispiel gibt es nur einen: die Warnschwelle für die freien Slots. Dies soll eine Ganzzahl sein, also verwenden wir hier ein Integer.

def _parameter_valuespec_foobar():
    return Dictionary(
        elements=[
            ("warning_lower", Integer(title=_("Warning below free slots"))),
        ],
    )

Zu guter Letzt registrieren wir jetzt mithilfe der importierten und selbstdefinierten Dinge einen neuen Regelsatz. Dazu gibt es die Funktion rulespec_registry.register():

rulespec_registry.register(
    CheckParameterRulespecWithItem(
        check_group_name="foobar",
        group=RulespecGroupCheckParametersOperatingSystem,
        match_type="dict",
        item_spec=_item_valuespec_foobar,
        parameter_valuespec=_parameter_valuespec_foobar,
        title=lambda: _("Free slots for Foobar sectors"),
    ))

Dazu noch einige Hinweise:

  • Falls Ihr Check kein Item verwendet, lautet die innere Funktion CheckParameterRulespecWithoutItem. Die Zeile item_spec entfällt dann.

  • Wie oben erwähnt stellt der check_group_name die Verbindung zu den Checks her, welche diese Regel verwenden sollen. Er darf auf keinen Fall identisch sein mit einer bereits existierenden Regel, weil diese damit überschrieben würde.

  • Die group legt fest, in welcher Kategorie im Setup der Regelsatz auftauchen soll. Die meisten dieser Gruppen sind in der Datei lib/check_mk/gui/plugins/wato/utils/init.py definiert. Dort finden Sie auch Beispiele, wie Sie eine eigene neue Gruppe anlegen können.

  • Der match_type ist immer "dict". In älteren Checkmk-Versionen gab es auch Parameterregeln mit anderen Typen.

  • title legt den Titel des Regelsatzes fest, wird aber nicht direkt als Text, sondern als ausführbare Funktion angegeben, welche den Text zurückliefert (deswegen das lambda:).

Test

Wenn Sie diese Datei angelegt haben, sollten Sie erstmal ausprobieren, ob alles soweit funktioniert und nicht gleich mit der Checkfunktion weiterarbeiten. Dazu müssen Sie erstmal den Apache der Instanz neu starten, damit die neue Datei gelesen wird. Das macht der Befehl:

OMD[mysite]:~$ omd restart apache

Danach sollte der Regelsatz im Setup zu finden sein. Legen Sie eine Regel in dieser Kette an und probieren Sie verschiedene Werte aus. Wenn das ohne Fehler geht, können Sie die Checkparameter jetzt in der Checkfunktion verwenden.

10.4. Die Regel im Checkplugin benutzen

Damit die Regel zum Greifen kommt, müssen wir dem Checkplugin erlauben, Checkparameter entgegenzunehmen und ihm sagen, welche Regel benutzt werden soll. Dazu muss bei der Registrierung der Eintrag check_default_parameters unbedingt vorhanden sein. Im einfachsten Fall übergeben wir ein leeres Dictionary.

Als zweites übergeben wir der Registrierungsfunktion noch den check_ruleset_name, also den Namen, den wir oben mittels check_group_name an den Regelsatz vergeben haben. So weiß Checkmk aus welchem Regelsatz die Parameter bestimmt werden sollen.

Das Ganze sieht dann z.B. so aus:

register.check_plugin(
    name = "foobar",
    service_name = "Foobar Sector %s",
    discovery_function = discover_foobar,
    check_function = check_foobar,
    check_default_parameters={},
    check_ruleset_name="foobar",
)

Nun wird Checkmk versuchen, der Checkfunktion Parameter zu übergeben. Damit das klappen kann, müssen wir die Checkfunktion so erweitern, dass sie als zweites das Argument params erwartet. Diese schiebt sich zwischen item und section (Falls Sie einen Check ohne Item bauen, entfällt das item natürlich und params steht am Anfang):

def check_foobar(item, params, section):

Es ist sehr empfehlenswert, sich jetzt als ersten Test den Inhalt der Variable params mit einem print ausgeben zu lassen (oder pprint, wenn Sie es etwas komfortabler haben wollen). Legen Sie verschiedene Regeln an, probieren Sie, welche Werte bei params ankommen:

def check_foobar(item, params, section):
    print(params)
    for sector, used, slots in ...

Und ganz wichtig: Wenn alles fertig ist, entfernen Sie unbedingt die print-Befehle wieder! Diese können die interne Kommunikation von Checkmk durcheinanderbringen.

Nun passen wir unsere Checkfunktion an, so dass der übergebene Parameter seine Wirkung entfalten kann. Wir holen uns den Wert mit dem in der Regel gewählten Key (hier "warning_lower") aus den Parametern:

def check_foobar(item, params, section):
    warn = params["warning_lower"]
    for sector, used, slots in section:
        if sector == item:
            used = int(used)    # convert string to int
            slots = int(slots)  # convert string to int
            if used == slots:
                s = State.CRIT
            elif slots - used <= warn:
                s = State.WARN
            else:
                s = State.OK
            yield Result(
                state = s,
                summary = f"used {used} out of {slots} slots")
            return

Falls eine Regel konfiguriert ist, können wir nun die „freien Slots“ in unserem Beispiel überwachen. Wenn allerdings keine Regel definiert ist, wird diese Checkfunktion crashen: Da die Default-Parameter des Plugins nicht befüllt sind, wird das Plugin bei Abwesenheit einer Regel einen KeyError erzeugen.

Dieses Problem können wir beheben, indem wir bei der Registrierung einen passenden Parameter einfügen:

register.check_plugin(
    name = "foobar",
    service_name = "Foobar Sector %s",
    discovery_function = discover_foobar,
    check_function = check_foobar,
    check_default_parameters = {"warning_lower": 10},
    check_ruleset_name = "foobar",
)

Sie sollten Defaultwerte immer auf diese Weise übergeben (und den Fall fehlender Parameter nicht im Checkplugin abfangen), da diese Defaultparameter auch in der Setup-Oberfläche angezeigt werden können. Dazu gibt es z.B. auf der Servicekonfigurationsseite eines Hosts im Menu Display den Eintrag Show Check parameters.

Ein einzelner Wert als Schwellwert ist in Checkmk übrigens sehr unüblich. Da Services in den Zuständen OK, WARN, CRIT sein können, ist es naheliegend die Parameter immer als Tuple mit zwei Einträgen zu definieren, also als Paar von Schwellen für WARN und CRIT. Dazu passen wir den Regelsatz wie folgt an:

def _parameter_valuespec_foobar():
    return Dictionary(
        elements=[
            ("warning_lower", Tuple(
                title=_("Levels on free slots"),
                elements=[
                    Integer(title=_("Warning below")),
                    Integer(title=_("Critical below")),
                ],
            )),
        ],
    )

Beachten Sie, dass eine solche Änderung des Datentyps eine inkompatible Änderung darstellt: Existierende Regeln können jetzt nicht mehr von der Oberfläche geladen werden. Und auch die Checkfunktion kann auf Probleme stoßen, wenn anstelle eines erwarteten Paar von zwei Zahlen eine einzelne Zahl in params steht. Sie können solche Regeln einfach editieren. Beim erneuten Speichern wird dann das neue Format verwendet.

10.5. Weitere ValueSpecs

In Checkmk gibt es zahlreiche ValueSpecs für alle möglichen Situationen. Hier sind noch ein paar nützliche:

Float

Float ist wie Integer, erlaubt aber die Eingabe von Zahlen mit Nachkommastellen.

Percentage

Oft möchte man Schwellen nicht in absoluten Zahlen, sondern in Prozent angeben. Dazu gibt es das ValueSpec Percentage:

def _parameter_valuespec_foobar():
    return Dictionary(
        elements=[
            ("levels_percent", Tuple(
                title=_("Relative levels"),
                elements=[
                    Percentage(title=_("Warning at"), default_value=80),
                    Percentage(title=_("Critical at"), default_value=90)
                ],
            )),
        ],
    )

Bei dieser ValueSpec würde das Checkplugin die Parameter {"levels_percent": (80.0, 90.0)} übergeben bekommen.

MonitoringState

Der MonitoringState ist nützlich, wenn Sie dem Benutzer erlauben wollen, für verschiedene Situationen jeweils einen der Zustände OK, WARN, CRIT und UNKNOWN auszuwählen. Es bietet dem Benutzer ein Dropdownfeld mit eben diesen vier Möglichkeiten, welche dann umgesetzt werden in eine der Zahlen 0, 1, 2 oder 3.

Hier können Sie z.B. einstellen, welchen Zustand der Service bekommen soll, falls kein Backup konfiguriert bzw. vorhanden ist:

def _parameter_valuespec_plesk_backups():
    return Dictionary(
        help=_("This check monitors backups configured for domains in plesk."),
        elements=[
            ("no_backup_configured_state",
             MonitoringState(title=_("State when no backup is configured"), default_value=1)),
            ("no_backup_found_state",
             MonitoringState(title=_("State when no backup can be found"), default_value=1)),
        ...

Bei dieser ValueSpec würde das Checkplugin die Parameter {"no_backup_configured_state": 1, "no_backup_found_state": 1} übergeben bekommen, falls in beiden Fällen der Default von WARN (=1) übernommen wurde. Sie können die Zahl einfach in ein State Objekt umwandeln, indem Sie es der Funktion State() übergeben:

    yield Result(
        state=State(params["no_backup_configured_state"]),
        summary="No backup is configured!",
    )

Age

Das Feld Age erlaubt die Eingabe eines Alters, welches intern als Anzahl von Sekunden gespeichert und übergeben wird:

def _parameter_valuespec_antivir_update_age():
    return Tuple(elements=[
        Age(title=_("Warning level for time since last update")),
        Age(title=_("Critical level for time since last update")),
    ],)

Filesize

Die ValueSpec Filesize erlaubt die Eingabe von Datei- (oder Festplatten)größen. Intern wird mit Bytes gerechnet, aber der Benutzer darf aus KB, MB, GB oder TB auswählen:

    Tuple(
        title=_("Maximum size of all files on backup space"),
        help=_("The maximum size of all files on the backup space. "
               "This might be set to the allowed quotas on the configured "
               "FTP server to be notified if the space limit is reached."),
        elements=[
            Filesize(title=_("Warning at")),
            Filesize(title=_("Critical at")),
        ],
    ),

Das Thema ValueSpecs ist extrem flexibel und umfangreich und würde diesen Artikel sprengen. Bitte schauen Sie sich die Beispiele der von Checkmk mitausgelieferten Regeldefinitionen in lib/check_mk/gui/plugins/wato/check_parameters/ an. Dort gibt es mehr als 500 Dateien mit Beispielen.

11. Angepasste Darstellung von Metriken

11.1. Der Sinn von Metrikdefinitionen

In unserem obigen Beispiel haben wir das Plugin foobar die Metrik fooslots erzeugen lassen. Metriken werden in der grafischen Oberfläche von Checkmk sofort sichtbar, ohne dass Sie etwas dafür tun müssten. Pro Metrik wird bei den Servicedetails automatisch ein Graph erzeugt.

Allerdings gibt es dabei ein paar Einschränkungen:

  • Es erscheint nicht automatisch ein „Perf-O-Meter“, also die grafische balkenartige Vorschau des Messwerts, wenn der Service in der Listendarstellung angezeigt wird (z.B. in der Ansicht, die alle Services eines Hosts darstellt).

  • Es werden nicht automatisch passende Metriken in einem Graphen kombiniert, sondern jede erscheint einzeln.

  • Die Metrik hat keinen richtigen Titel, sondern es wird der interne Variablenname der Metrik gezeigt.

  • Es wird keine Einheit verwendet, die eine sinnvolle Darstellung erlaubt (z.B. GB anstelle von einzelnen Bytes)

  • Es wir zufällig eine Farbe ausgewählt.

Um die Darstellung Ihrer Metriken in diesen Belangen zu vervollständigen, benötigen Sie noch einige Definitionen in einer weiteren Datei.

11.2. Vorhandene Metrikdefinitionen verwenden

Bevor Sie das tun, sollten Sie — ähnlich wie beim Regelsatz für die Parameter — zunächst prüfen, ob Checkmk nicht bereits eine geeignete Metrikdefinition mitbringt. Die vordefinierten Metrikendefinitionen finden Sie im Verzeichnis lib/check_mk/gui/plugins/metrics/. In der Datei cpu.py finden Sie beispielsweise eine Metrik für freien Platz eines Dateisystems:

metric_info["util"] = {
    "title": _("CPU utilization"),
    "unit": "%",
    "color": "26/a",
}

Falls diese für Ihr Plugin geeignet ist, müssen Sie lediglich in Ihrem Aufruf der Metric()-Klasse den Namen "util" verwenden. Alles andere leitet sich dann automatisch davon ab.

11.3. Eigene Metrikdefinitionen

Falls keine passende Metrik dabei ist, legen Sie einfach selbst eine an. In unserem Beispiel wollen wir einen eigene Metrik für unsere fooslots definieren. Dazu legen wir eine Datei in local/share/check_mk/web/plugins/metrics an`:

local/share/check_mk/web/plugins/metrics/foobar_metric.py
from cmk.gui.i18n import _
from cmk.gui.plugins.metrics import metric_info

metric_info["fooslots"] = {
    "title": _("Used slots"),
    "unit": "count",
    "color": "15/a",
}

Dazu einige Hinweise:

  • Der Schlüssel (hier "fooslots") ist der Metrikname und muss dem entsprechen, was die Checkfunktion ausgibt.

  • Das Importieren und Verwenden des Unterstrichs für die Internationalisierung ist optional, wie bereits bei den Regeln besprochen.

  • Welche Unit-Definitionen es gibt, erfahren Sie in der Datei lib/check_mk/gui/plugins/metrics/unit.py.

  • Die Farbdefinition verwendet eine Palette. Zu jeder Palettenfarbe gibt es /a und /b. Dies sind zwei Schattierungen der gleichen Farbe. In den vorhandenen Definitionen werden Sie auch viele direkte Farbkodierungen wie "#ff8800" finden. Diese werden nach und nach abgeschafft und alle durch Palettenfarben ersetzt werden, da diese ein einheitlicheres Aussehen bieten und auch leichter an die Themes der Oberfläche angepasst werden können.

Diese Definition sorgt jetzt dafür, dass Farbe, Titel und Einheit der Metrik nach unsere Wünschen angezeigt werden.

11.4. Graphen mit mehreren Metriken

Möchten Sie mehrere Metriken in einem Graphen kombinieren (was oft sehr sinnvoll ist), benötigen Sie, einfach in der gleichen Datei, eine Graphdefinition. Dies geschieht über das globale Dictionary graph_info.

Nehmen wir dazu als Beispiel an, unser Check hätte zwei Metriken und zwar fooslots und fooslots_free. Die Metrikdefinitionen wären z.B.:

local/share/check_mk/web/plugins/metrics/foobar_metric.py
from cmk.gui.i18n import _
from cmk.gui.plugins.metrics import (
    metric_info,
    graph_info,
)

metric_info["fooslots"] = {
    "title": _("Used slots"),
    "unit": "count",
    "color": "16/a",
}

metric_info["fooslots_free"] = {
    "title": _("Free slots"),
    "unit": "count",
    "color": "24/a",
}

Nun fügen wir einen Graphen an, der diese beiden Metriken als Linien einzeichnet:

graph_info["fooslots_combined"] = {
    "metrics": [
        ("fooslots", "line"),
        ("fooslots_free", "line"),
    ],
}

Hinweise dazu:

  • Leider gibt es im Handbuch noch keine Beschreibung der Möglichkeiten dieser Definition. Aber Sie finden sehr viele Beispiele in den Dateien im Verzeichnis lib/check_mk/gui/plugins/metrics.

  • Probieren Sie anstelle von line auch mal area oder stack.

11.5. Darstellung der Metriken im Perf-O-Meter

Möchten Sie zu unserer Metrik noch ein Perf-O-Meter in der Servicezeile anzeigen, benötigen Sie eine weitere Datei, diesmal im Verzeichnis local/share/check_mk/web/plugins/perfometer.

Beispiel:

local/share/check_mk/web/plugins/perfometer/foobar_perfometer.py
from cmk.gui.plugins.metrics import perfometer_info

perfometer_info.append({
    "type": "logarithmic",
    "metric": "fooslots",
    "half_value": 5,
    "exponent": 2.0,
})

Perf-O-Meter sind etwas trickreicher als Graphen, da es keine Legende gibt. Und deswegen ist das mit dem Wertebereich schwierig. Da das arme Perf-O-Meter nicht wissen kann, welche Messwerte denn überhaupt möglich sind und der Platz sehr begrenzt ist, verwenden viele eingebaute Checkplugins eine logarithmische Darstellung. Dies ist auch in unserem Beispiel so. half_value ist der Messwert, welcher genau in der Mitte des Perf-O-Meters angezeigt wird. Bei einem Wert von 5, wäre also hier der Balken halb gefüllt. Und exponent beschreibt den Faktor, welcher notwendig ist, damit weitere 10% des Bereichs gefüllt würden. Also würde hier im Beispiel ein Messwert von 10 bei 60% und einer von 20 bei 70% angezeigt werden.

Der Vorteil von dieser Methode: Wenn Sie eine Liste von Services gleicher Art haben, können Sie alle Perf-O-Meter untereinander optisch schnell vergleichen, da alle die gleiche Skala haben. Und trotz der sehr kleinen Darstellungen kann man sowohl bei sehr kleinen als auch bei der großen Messwerten die Unterschiede gut erkennen. Dafür sind die Werte allerdings nicht maßstabsgetreu.

Alternativ können Sie auch ein lineares Perf-O-Meter verwenden. Das ist immer dann sinnvoll, wenn es einen bekannten Maximalwert gibt. Ein typischer Fall sind Messwerte, welche Prozente von 0 bis 100 darstellen. Das sähe dann z.B. so aus:

perfometer_info.append({
    "type": "linear",
    "segments": ["fooslots_used_percent"],
    "total": 100.0,
})

Hier gibt es noch einen weiteren Unterschied zur logarithmischen Darstellung: segments ist hier eine Liste und erlaubt das nebeneinander Darstellen von mehreren Metriken.

Wie immer finden Sie Beispiele in den vielen von Checkmk ausgelieferten Plugins. Diese sind ebenfalls in den Dateien im Verzeichnis lib/check_mk/gui/plugins/metrics.

12. Hinweise für Nutzer der alten API

Sind Sie bereits erfahren bei der Entwicklung von Checkplugins mit der bisherigen API — derjenigen bis Version 1.6.0 von Checkmk? Dann finden Sie hier einige Hinweise über wichtige Änderungen zusammengefasst.

12.1. saveint() und savefloat()

Die beiden Funktionen saveint() und savefloat() sind weggefallen. Zur Erinnerung: saveint(x) liefert 0 wenn sich x nicht vernünftig in eine Zahl konvertieren lässt, z.B. weil es ein leerer String ist oder nicht nur aus Ziffern besteht.

Auch wenn es dafür einige wenige gute Anwendungsfälle gab, wurde es doch in der Mehrheit der Fälle falsch verwendet und hat dazu geführt, dass so viele Fehler verschleiert wurden.

Für den Fall, dass Sie bei einem Leerstring eine 0 bekommen möchten, also den häufigsten „guten“ Anwendungsfall von saveint(x), können Sie einfach Folgendes schreiben:

foo = int(x) if x else 0

Für savefloat() gilt alles analog.

13. Komplexe Agentenausgaben mittels Parsefunktion bändigen

Der nächste Schritt ist die sogenannten Parsefunktion. Diese hat die Aufgabe, die „rohen“ Agentendaten zu parsen und in eine logisch aufgeräumte Form zu bringen, die für alle weiteren Schritte einfach zu verarbeiten ist. Konvention ist, dass diese nach der Agentensektion benannt wird und mit parse_ beginnt. Sie bekommt als einziges Argument string_table. Bitte beachten Sie, dass Sie hier nicht frei in der Wahl des Arguments sind. Es muss wirklich so heißen.

Wir schreiben unsere Parsefunktion jetzt erstmal so, dass wir einfach nur die Daten, die sie bekommt, auf der Konsole ausgeben. Dazu nehmen wir einfach die print-Funktion (Achtung: seit Python 3 sind hier Klammern zwingend notwendig):

def parse_linux_usbstick(string_table):
    print(string_table)

Damit das Ganze irgendetwas bewirken soll, müssen wir unsere Parsefunktion und überhaupt die neue Agentensektion bei Checkmk bekannt machen. Dazu rufen wir eine Registrierfunktion auf:

register.agent_section(
    name = "linux_usbstick",
    parse_function = parse_linux_usbstick,
)

Hier ist es wichtig, dass der Name der Sektion wirklich exakt mit dem Sektionsheader in der Agentenausgabe übereinstimmt. Insgesamt sieht das jetzt so aus:

local/lib/check_mk/base/plugins/agent_based/linux_usbstick.py
from .agent_based_api.v1 import *

def parse_linux_usbstick(string_table):
    print(string_table)
    return string_table

register.agent_section(
    name = "linux_usbstick",
    parse_function = parse_linux_usbstick,
)

Von diesem Moment an bekommt jedes Plugin, das die Section linux_usbstick benutzt, den Rückgabewert der Parsefunktion übergeben. In der Regel wird das das gleichnamige Checkplugin sein.

Wir haben jetzt gewissermaßen das einfachste mögliche Plugin gebaut, was noch keinen wirklich Nutzen hat, aber das wir immerhin schon testen können. Dazu stoßen wir auf der Kommandozeile eine Serviceerkennung (Option -I) von dem Host an, dessen Agenten wir vorhin präpariert haben. Wenn dessen Ausgabe auch wirklich eine Sektion linux_usbstick enthält, dann müssten wir unsere Debugausgabe sehen:

OMD[mysite]:~$ cmk -I myhost123
[['ata-APPLE_SSD_SM0512F_S1K5NYBF810191'], ['wwn-0x5002538655584d30']]

Etwas übersichtlicher wird die Ausgabe, wenn wir das einfache print durch ein Pretty-print aus dem Modul pprint ersetzen. Das ist für alle weitere Debugausgaben sehr empfehlenswert:

local/lib/check_mk/base/plugins/agent_based/linux_usbstick.py
from .agent_based_api.v1 import *
*import pprint*

def parse_linux_usbstick(string_table):
    *pprint.pprint(string_table)*
    return string_table

register.agent_section(
    name = "linux_usbstick",
    parse_function = parse_linux_usbstick,
)

Das sieht dann so aus:

OMD[mysite]:~$ cmk -I myhost123
[['ata-APPLE_SSD_SM0512F_S1K5NYBF810191'],
 ['wwn-0x5002538655584d30']]

13.1. Die Parsefunktion schreiben

Wenn Sie genau hinsehen, dann erkennen Sie, dass es sich hier um verschachtelte Listen handelt. Im Argument string_table bekommen Sie eine Liste, welche pro Zeile der Agentenausgabe eine Liste von Worten beinhaltet. Dabei werden die Zeilen an Folgen von Leerzeichen getrennt. Da unsere Sektion pro Zeile nur ein Wort enthält, bestehen ergo die inneren Listen aus nur jeweils einem Eintrag.

Folgendes Beispiel macht die Struktur noch etwas klarer:

local/lib/check_mk/base/plugins/agent_based/linux_usbstick.py
from .agent_based_api.v1 import *
import pprint

def parse_linux_usbstick(string_table):
    print("Number of lines: %d" % len(string_table))
    print("Number of words in first line: %d" % len(string_table[0]))
    print("Length of first word: %d" % len(string_table[0][0]))
    return string_table

register.agent_section(
    name = "linux_usbstick",
    parse_function = parse_linux_usbstick,
)

Die Ausgabe sieht dann so aus:

OMD[mysite]:~$ cmk -I myhost123
Number of lines: 3
Number of words in first line: 1
Length of first word: 36

Für unser Beispiel benötigen wir einfach nur eine einfache Liste der Devicenamen. Also machen wir unsere Parsefunktion so, dass sie aus jeder Zeile das eine Wort auspackt und in eine hübsche neue Liste verpackt:

def parse_linux_usbstick(string_table):
    parsed = []
    for line in string_table:
        parsed.append(line[0])
    pprint.pprint(parsed)
    return string_table

Die Debugausgabe sieht dann so aus (bitte schauen Sie genau hin, es gibt jetzt nur noch ein einziges paar eckiger Klammern):

['ata-APPLE_SSD_SM0512F_S1K5NYBF810191',
 'wwn-0x5002538655584d30']

Damit die Parsefunktion vollständig ist, müssen wir jetzt noch die Debugmeldung entfernen und — ganz wichtig — das neue Ergebnis mit return zurückgeben:

def parse_linux_usbstick(string_table):
    parsed = []
    for line in string_table:
        parsed.append(line[0])
    return parsed

Natürlich müssen von diesem Moment an alle betroffenen Plugins mit dem neuen Datenformat arbeiten können.

14. Ausblick

Es gibt noch viele weitere Aspekte und Themen rund um die Entwicklung von eigenen Plugins. Checkmk hat sehr viele Schnittstellen für eigene Erweiterungen und ist dadurch sehr flexibel erweiterbar. Wir arbeiten daran, dass diese Schnittstellen nach und nach im Handbuch beschrieben werden.

Falls Sie Fragen oder Schwierigkeiten haben, steht Ihnen natürlich unser professioneller Support und auch das kostenlose Forum zur Verfügung.

15. Dateien und Verzeichnisse

local/lib/check_mk/base/plugins/agent_based

Ablageort für selbst geschriebene Checkplugins

local/share/check_mk/web/plugins/wato

Ablageort für Ihre Regelsätze für Checkparameter

local/share/check_mk/web/plugins/metrics

Ablageort für eigene Metrikdefinitionen

local/share/check_mk/web/plugins/perfometer

Ablageort für eigene Definitionen von Perf-O-Metern

local/share/check_mk/mibs

Legen Sie hier SNMP-MIB-Dateien ab, die automatisch geladen werden sollen.

lib/check_mk/gui/plugins/wato/check_parameters

Hier finden Sie die Regelsatzdefinitionen von allen mitgelieferten Checkplugins von Checkmk

lib/check_mk/gui/plugins/wato/utils/init.py

In dieser Datei sind die Gruppen der Setupoberfläche definiert, in welchen Sie neue Regelsätze ablegen können.

lib/check_mk/gui/plugins/metrics/

Hier finden Sie die Metrikdefinitionen der mitgelieferten Plugins

lib/check_mk/gui/plugins/metrics/unit.py

In dieser Datei sind die vordefinierten Einheiten für Metriken.

/usr/lib/check_mk_agent/plugins

Dieses Verzeichnis bezieht sich auf einen überwachten Linux-Host. Hier erwartet der Checkmk-Agent für Linux Erweiterungen des Agenten (Agent-Plugins).

Auf dieser Seite