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:
Methode | So geht’s | Vorteile | Nachteile |
---|---|---|---|
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
undregister.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
zurender.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 |
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:
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.
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:
#!/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:
Plugin solo ausprobieren. Das haben wir gerade gemacht.
Agent als ganzes lokal testen.
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:
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:
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.service_name
: Der Name des Services wie er dann im Monitoring erscheinen soll.discovery_function
: Die Funktion zum Erkennen von Services dieses Typs (dazu gleich mehr).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:
Mit
for line in section
gehen wir in einer Schleife alle Zeilen der Agentenausgabe durch.Dann prüfen wir, ob das erste Wort der Zeile — das jeweilige Gerät — mit
usb-SCSI_DISK
beginnt.Falls ja, erzeugen wir ein Check-Resultat mit dem Status CRIT und dem Text
Found USB stick
. Und wir beenden dann die Funktion mit einemreturn
.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:
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:
#!/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:
#!/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:
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.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.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.
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 OK → WARN → UNKNOWN → CRIT.
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
odernotice
— 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 vonbar
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:
Durch einen Klick auf das Icon 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?
Sie benötigen kein Plugin für den Agenten.
Sie müssen die für die SNMP-Detection nötigen Einzel-OIDs und Suchtexte festlegen.
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:
.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:
.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:
Legen Sie für die SNMP-Detection fest, welche OIDs welche Texte enthalten müssen, damit Ihr Plugin ausgeführt werden soll.
Deklarieren Sie, welche OID-Zweige für das Monitoring geholt werden müssen.
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
:
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:
Attribut | Negation | Bedeutung |
---|---|---|
equals(oid, needle) |
not_equals(oid, needle) |
Der Wert der OID ist gleich dem Text |
contains(oid, needle) |
not_contains(oid, needle) |
Der Wert der OID enthält an irgendeiner Stelle den Text |
startswith(oid, needle) |
not_startswith(oid, needle) |
Der Wert der OID beginnt mit dem Text |
endswith(oid, needle) |
not_endswith(oid, needle) |
Der Wert der OID endet mit dem Text |
matches(oid, regex) |
not_matches(oid, regex) |
Der Wert der OID matcht auf den regulären Ausdruck |
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:
Aufruf | Ausgabe |
---|---|
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.
Aufruf | Ausgabe |
---|---|
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:
Aufruf | Ausgabe |
---|---|
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.
Aufruf | Ausgabe |
---|---|
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:
Aufruf | Ausgabe |
---|---|
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:
Aufruf | Ausgabe |
---|---|
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:
Aufruf | Ausgabe |
---|---|
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):
Aufruf | Ausgabe |
---|---|
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:
Aufruf | Ausgabe |
---|---|
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.
Aufruf | Ausgabe |
---|---|
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:
Funktion | Eingabe | Beschreibung | Beispielausgabe |
---|---|---|---|
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:
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.
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:
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:
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 Zeileitem_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 Dateilib/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 daslambda:
).
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`:
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.:
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 malarea
oderstack
.
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:
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:
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:
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:
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). |