Mittlerweile gibt es fast 2.000 offizielle Checkplugins, die als Erweiterungen für das IT-Monitoring mit Checkmk bereitstehen. Die Check-API ist jedoch noch aus einer Zeit, als es noch wenige Plugins gab – und hat sich seitdem auch nicht verändert. Mit Checkmk 2.0 führen wir nun eine neue Check-API ein, die die Checkplugins neu strukturiert, um ihre Handhabung auf verschiedenen Ebenen zu vereinheitlichen und zu vereinfachen.

Darüber hinaus wechseln wir mit der neuen Produktversion von Python 2 zu Python 3, dieser Schritt hat mit der neuen Check-API zunächst nichts zu tun – bedeutet aber, dass Sie den Code Ihrer Plugins möglicherweise ohnehin anfassen müssen.

Die Chancen stehen zwar gut, dass die Portierung von bestehenden Checkplugins auf die neue Check-API reibungslos funktioniert, durch die Umstellung auf Python 3 ist es dennoch nötig, dass Sie Ihre eigenen Plugins anschauen und möglicherweise einige Anpassungen vornehmen müssen. Wie Sie überprüfen können, ob die Auto-Migration eines Plugins fehlschlägt, können Sie im Werk #10601 nachschlagen.

In diesem Blogpost werden wir detailliert erklären, wie Sie ihre selbstgeschriebenen Checkplugins auf die neue Check-API problemlos migrieren. Der Artikel richtet sich an fortgeschrittene Nutzer und ist in drei Segmente strukturiert: „Vorbereitung des Codes“, „Migration des Plugins“ und „Der Königsweg“.

Vorbereitung des Codes

Zunächst befassen wir uns mit der Portierung von Python 2 auf Python 3. Hierzu gibt es bereits viele Anleitungen. Für unseren Fall sind die wichtigsten Punkte die „Division“ und die Behandlung von „Text versus Binary Data“. Da str unter Python 3 Unicode ist, lautet die Faustregel für den Code der Checkplugins: Sooft wie möglich str zu verwenden. Da sich, wie in der Dokumentation beschrieben wird, ab Python 3 die Division ändert, müssen Sie den Code darauf hin kontrollieren, ob der errechnete Wert immer noch dem gewünschten Typ entspricht (float/int). Sie können in diesem Commit sehen, wie Sie die Division in Checks korrigieren können.

Code aufräumen

Bevor wir mit der eigentlichen Migration starten können, müssen wir zuerst den Code aufräumen und ihn möglichst „nah“ an die neue API anpassen – jedoch ohne ihn dabei tatsächlich zu portieren. Hierzu ergänzen Sie die benötigten „import“-Befehle. Dies war bei den „alten“ Bibliotheken zuvor nicht nötig, da Checkmk dafür gesorgt hat, dass sie „magisch“ verfügbar sind. Mit der neuen Check-API ist jedoch jeder User dafür zuständig, dass er die benötigten Python-Bibliotheken regulär importiert.

An dieser Stelle ist es eine gute Idee, dass Sie diese „magischen“ Importe explizit machen. Durch den Import der Python-Bibliotheken im ersten Schritt reduzieren Sie bei der Verwendung einer IDE mit integriertem Linter oder Typechecker die falschpositiven Meldungen. Dadurch können Sie sich außerdem besser auf mögliche Probleme bei der Migration konzentrieren.

Das fällt mit der neuen API alles weg

Ab Python 3 ist die Methode iteritems weggefallen. Dies erfordert ebenso einige Anpassungen am Plugin-Code, genauer gesagt das Ersetzen von iteritems durch items.

iteritems durch items im Code ersetzen

Ebenfalls weggefallen sind die beiden Funktionen saveint() und savefloat(). saveint(x) lieferte in der Vergangenheit den Wert 0, wenn sich x nicht in eine Zahl konvertieren lies, etwa weil es sich um einen leeren String handelt oder nicht nur aus Ziffern besteht. Dies mag zwar in einigen Fällen ein sinnvoller Anwendungsfall sein, jedoch wurde es in den meisten Fällen falsch verwendet, sodass dadurch viele Fehler verschleiert wurden. Wenn Sie jetzt bei einem leeren String eine 0 bekommen möchten – was unserer Erfahrung nach der häufigste Anwendungsfall von saveint(x) ist – schreiben Sie einfach folgendes:

foo = int(x) if x else 0

Für savefloat()gilt dies analog.

Prominentere Rolle für die Parsefunktion

Mit der neuen Check-API fällt der Parsefunktion eine prominentere Rolle zu. Daher sollten Sie immer eine implementieren, falls noch nicht geschehen. Die Umsetzung ist einfach, wie der Commit zur Implementierung einer Parsefunktion zeigt.

Implementierung einer Parsefunktion

Die Parsefunktion hat die Aufgabe, sämtliche Rohdaten in verwertbare Datensätze zu konvertieren und unnütze Daten zu verwerfen.

Darüber hinaus ist es hilfreich, die Checkfunktion in eine geeignete Datenstruktur zu bringen. Wird der Item-Datensatz beispielsweise nicht in den geparsten Abschnitten gefunden, kann die Check-Engine eine generische und konsistente Meldung erzeugen. Wenn Sie den Key zum Item machen, können Sie einfach einen Service pro Key entdecken und die entsprechenden Daten in der Checkfunktion suchen, wie der Commit "parse data into a dictionary" veranschaulicht.

Check- und Discoveryfunktion müssen Generatorfunktionen sein

Achten Sie jedoch darauf, dass die Check- und die Discoveryfunktion Generatorfunktionen sein müssen. Dies ist in der neuen API vorgeschrieben. In der alten API ist dies aber auch schon möglich, sodass wir dies problemlos umstellen können. Außerdem ist zu berücksichtigen, dass die Parameter der Checkfunktion – falls sie welche hat – immer ein Dictionary sein müssen. Sollte dies im Moment noch anders sein, müssen Sie die Implementierung in der WATO-Regelkette entsprechend anpassen.

Als letzten Schritt der Migrations-Vorbereitung überprüfen wir nun, ob sich check_levels verwenden lässt. Dies hat zwar mit der Migration an sich nichts zu tun, ist aber eine nützliche Hilfsfunktion, die Ihnen die Arbeit erleichtern kann. Viele Checkplugins vergleichen einen (nummerischen) Wert mit den gesetzten Schwellwerten, um einen Status festzulegen. Hierzu sollten Sie immer die bereitgestellte Funktion check_levels verwenden, die sowohl in der alten als auch in der neuen API verfügbar ist. Wir empfehlen, prinzipiell möglichst viele API-Funktionen zu verwenden, um ein möglichst einheitlicheres und standardisiertes Gesamterscheinungsbild zu erhalten.

Damit haben wir die Vorbereitungen abgeschlossen und können nun zur eigentlichen Migration übergehen.

Migration des Plugins

Bei der Migration eines Plugins auf die neue Check-API gibt es einige Dinge zu berücksichtigen, die sich im Vergleich zur alten API geändert haben. Wie bereits erwähnt, sind die Plugins Python-Module. Die eigenen Plugins sind nun im Verzeichnis local/lib/check_mk/base/plugins/agent_based abgelegt und haben eine .py-Endung. Das muss man beim Ablegen der Datei auf jeden Fall berücksichtigen. Viele der nun folgenden Erklärungen lassen sich in diesem Commit gut nachvollziehen.

So registriert die neue Check-API die Sektionen und eigentlichen Checks nun getrennt voneinander. Unter der alten API war dies noch unter check_info zusammengefasst.

Bei der agent_section handelt es sich unter der neuen API um die Registrierungsfunktion. In der API-Dokumentation kann der Nutzer alles über den Vorgang und die Möglichkeiten der agent_section erfahren. Sektionen definieren Sie entweder mit register.agent_section oder mit register.snmp_sections, je nach dem, um welchen Datenquellentyp es sich handelt. In der Sphinx-Dokumentation, die über das Help-Menü in Ihrer Checkmk-Umgebung aufrubar ist, findet sich eine Liste aller erlaubten Argumente.

Code-Beispiel register.agent_section

Im einfachsten Fall benötigen Sie nur zwei Argumente: Den Namen der Sektion, er stimmt in der Regel mit dem Namen des Plugins überein, und die Parsefunktion. Beachten Sie, dass der Variablen-Name Ihres Arguments string_table lauten muss.

SNMP-Detection löst SNMP-Scanfunktion ab

Die alte SNMP-Scanfunktion heißt unter der neuen Check-API nun SNMP-Detection und ist eine deskriptive Spezifikation. Der Nutzer muss für die SNMP-Detection festlegen, welche Einzel-OIDs und Suchtexte er benötigt und welchen SNMP-Bereich der SNMP-Check für das Monitoring abholen soll. Weitere Informationen hierzu und nach welcher Logik man die Scanfunktion umbauen muss, finden sich auch im Checkmk-Handbuch.

Darüber hinaus gibt es nun mit supersedes eine eigene Möglichkeit, mit der Sektionen andere ausschließen können. Das geschieht nicht mehr über die SNMP-Scanfunktion.

Ebenfalls umbenannt haben wir die OIDBytes-Funktionen. Dies betrifft folgende Änderungen: Aus OID_END wird OIDEnd(), aus BINARY(‘2‘) wird OIDBytes(‘2‘) und aus CACHED_OID(‘3‘) wird OIDCached(‘3‘). Geändert hat sich jedoch nur der Name, die Funktionalität bleibt gleich. Alle weiteren OID-Funktionen, etwa OID_STRING, gibt es mit der neuen Check-API nicht mehr. Die drei genannten Funktionen benötigen Sie, um den SNMPtree anzugeben. Während Sie in der Vergangenheit nur eine Liste angeben mussten, ist es jetzt nötig, die Klasse SNMPTree zu benutzen. Ausführliche Informationen hierzu finden sich im Checkmk-Handbuch.

Wir haben mit der neuen Check-API außerdem etliche Funktions- und Argumentnamen umbenannt. So sprechen wir nun immer konsequent von Discovery, wenn es um die Discovery (Erkennung) von Services oder Host-Labels geht. Inventory wird künftig nur noch für Funktionen verwendet, die mit der Hard- und Software-Inventory zusammenhängen.

Code-Beispiel yield Result

Die Discoveryfunktion und die Checkfunktion müssen, wie bereits erwähnt, nun auch immer als Generator arbeiten, also müssen Sie yield verwenden. Es lassen sich der Discoveryfunktion nun auch Parameter hinzufügen, sodass keine host_extra_conf mehr nötig ist.

Mit der neuen API gibt es außerdem den bisherigen include-Mechanismus nicht mehr. Dies funktioniert nun über Python-Importe. Code, den mehrere Plugins benutzen sollen, kann im Verzeichnis local/lib/check_mk/base/plugins/agent_based/utils abgelegt und anschließend von den Plugins regulär importiert werden.

ValueStore explizit übergeben

Geändert hat sich auch die item_state-Funktion, die dafür gesorgt hat, dass ein Wert über verschiedene Checks persistiert wurde. Hierfür kamen in der alten API die Funktionen set_item_rate, get_rate und get_average zum Einsatz. An ihrer Stelle hat die neue API nun die Funktion get_value_store. Sie gibt ein sogenanntes „mutable mapping“ zurück – also ein Objekt, das sich wie ein Dictionary benutzen lässt. Hierbei bekommt jeder Service sein individuelles Objekt, so dass Sie weder den Namen des Checkplugins noch das Item im Key unterbringen müssen. Dieses Objekt (der ValueStore) hat die Aufgabe, die persistierten Werte zu übernehmen.

get_average und get_rate gibt es zwar noch, diesen muss man den ValueStore jedoch explizit übergeben. Sie haben früher die Funktion im Hintergrund mitbenutzt. Unter der neuen API handelt es sich hierbei jedoch lediglich um reine Hilfsfunktionen ohne Backend-Interaktion, denen man den ValueStore explizit übergeben muss.

Einen neuen Namen hat auch die bisherige Fehlerklasse MKCounterWrapped erhalten. Damit war es möglich, Exceptions zu raisen, die einen Service „stale“ werden lassen. Dieses Verhalten erreichen Sie nun, indem Sie eine Exception vom Typ IgnoreResultsError raisen oder ein IgnoreResults-Objekt yielden.

Wie in der API-Dokumentation nachzulesen ist, gibt die neue Check-API statt magischen Rückgabewerten, wie beispielsweise 2 für CRIT, jetzt die Konstanten State.OK, State.WARN, State.CRIT, State.UNKOWN zurück. Das heißt, dass ein Result vorher so aussah:

return 0, "All OK",[("foo",42)]

Mit der neuen API ändert sich das Result nun folgendermaßen:

yield Result(state=State.OK, summary="Everything OK")
yield Metric("foo",42)

Bei mehreren Results übernimmt Checkmk automatisch den Worst State.

In der API-Dokumentation sowie im Handbuch findet sich zudem eine detaillierte Erklärung über die ehemaligen get_<type>_human_readable-Funktionen, die jetzt in einem gemeinsamen Render-Modul zusammengefasst sind.

Der Königsweg

Wenn Sie alle bisherigen Schritte befolgt haben, funktionieren Ihre selbstgeschriebenen Plugins bereits unter der neuen Check-API. An dieser Stelle wollen wir jedoch noch auf einige weiterführende Themen – also den Königsweg bei der Plugin-Migration – eingehen. Dies richtet sich in erster Linie an erfahrene Anwender, die möglicherweise noch mehr aus Ihrem Code herausholen wollen.

So ist es unter anderem möglich, Typdefinitionen zu nutzen, um die Intention klar zu machen, welchen Typ eines Werts man in seinem Code erwartet. Diese Typehints sorgen für eine saubere Code-Struktur und ermögliche es, einen Typechecker über den Code laufen zu lassen und so mögliche Fehler aufzudecken. In der Sphinx-Dokumentation können Sie unter „Type annotations“ die verschiedenen Typdefinitionen der API einsehen. Dort lassen sich auch alle Informationen zum „Type Definitions“-Modul finden, das vor allem bei der Entwicklung von eigenen Plugins weiterhilft.

Die Nutzung von Typdefinitionen ist wie gesagt kein Muss, wir empfehlen es aber zu machen. Spätestens, wenn Sie den Code mainlinen wollen, ist dieser Schritt notwendig. Prinzipiell gilt, wenn Sie sich dafür entscheiden, Ihren Code zu mainlinen, dass dieser mit Unittests abgedeckt sein muss. Da die Plugins jetzt reguläre Module sind, ist es wesentlich einfacher, Unittests zu schreiben. Dies hilft außerdem dabei, saubere Plugins zu programmieren.

Ein weiterer Punkt, auf den wir an dieser Stelle noch eingehen wollen ist das Clustering von Plugins. Unter der alten API konnte prinzipiell jedes Plugins automatisch auf einem Cluster zum Einsatz kommen. Dies hat sich mit der neuen API nun geändert. Wenn ein Plugin nun für einen Cluster funktionieren soll, muss man explizit eine Cluster-Funktion implementieren. Die Vorgehensweise lässt sich im Commit "make a plugin fit for clustering" gut nachvollziehen.

Wir hoffen, dass wir damit alle offenen Fragen Rund um die Migration von selbstgeschriebenen Plugins auf die neue Check-API beantworten konnten. Weitere Informationen zur neuen Check-API finden Sie im Checkmk-Handbuch. Sollten Sie noch offene Fragen haben, können Sie diese auch im Checkmk-Forum stellen.