Seite wählen

NETWAYS Blog

Understanding commands in Icinga 2

Icinga 2 command definitions can seem daunting at first. This blog post provides a quick introduction to some of the concepts you need to be familiar with when writing your own command definitions.
In their most basic form command definitions need a command line:

object CheckCommand "my_http" {
  import "plugin-check-command"
  command = [ PluginDir + "/check_http" ]
}

The „plugin-check-command“ template tells Icinga how to execute commands, i.e. by executing an external plugin. There are a few other „*-check-command“ templates but for virtually all of your own check commands you’ll need to use „plugin-check-command“.
The check_http plugin needs at least one more argument to work:

$ /opt/local/libexec/nagios/check_http -I 127.0.0.1
HTTP OK: HTTP/1.1 200 OK - 342 bytes in 0.001 second response time |time=0.001344s;;;0.000000 size=342B;;;0

We can add this argument to our check command like this:

object CheckCommand "my_http" {
  import "plugin-check-command"
  command = [ PluginDir + "/check_http" ]
  arguments = {
    "-I" = {
      value = "$my_http_address$"
      description = "IP address or name."
      required = true
    }
  }
  vars.my_http_address = "$address$"
}

The ‚required‘ option tells Icinga to verify that the user specified a value for this argument.
We’re prefixing our custom attributes (my_http_address) with the name of the CheckCommand. This allows us to override specific custom attributes for HTTP checks on a per-host or per-service basis. If all
commands had the same custom attribute names (e.g. ‚timeout‘) this wouldn’t be possible:

object Host "test" {
  ...
  // This affects all services on this host which use the my_http command
  vars.my_http_address = "127.0.0.1"
}

In our next step we’re going to add a few optional arguments. The check_http plugin lets us specify the ‚Host‘ header and the URL that should be used. Adding optional arguments is rather simple:

    "-H" = {
      value = "$my_http_vhost$"
      description = "Host name argument for servers using host headers"
    }
    "-u" = {
      value = "$my_http_url$"
      description = "URL to GET or POST (default: /)"
    }

When Icinga encounters a command argument which uses an unresolvable macro (for example, because the user didn’t set a value for vars.my_http_vhost in their command, service or host) the entire argument is omitted.
Icinga can also add arguments only when certain conditions are met. In the next example I’m adding a new option ‚–sni‘ which is only added when the custom attribute my_http_sni is set to true:

    "--sni" = {
      description = "Enable SSL/TLS hostname extension support (SNI)"
      set_if = "$my_http_sni$"
    }

Note that the ‚–sni‘ option does not take an argument. Therefore we don’t need the ‚value‘ attribute for this argument.
When the ‚my_http_sni‘ custom attribute isn’t set at all Icinga defaults to not adding the argument.
There are a few more advanced topics for command arguments which aren’t covered in this blog post:

  • Ordering arguments
  • Using arrays for custom variables (with repeat_key/skip_key)
  • Using functions for set_if/value
  • Specifying an alternative ‚key‘

I might write another blog post at a later point in time which deals with those features. In the meantime these things are explained in the documentation.

Neue IDE: CLion

Alle paar Monate wage ich einen neuen Versuch, eine bessere Entwicklungsumgebung als vim/make zu finden. Diesmal habe ich mir CLion angeschaut:
clion
Einige interessante Features, die CLion vor allem für die Entwicklung mit Icinga 2 interessant machen sind z.B.:

  • Integrierte Unterstützung für GDB als Debugger
  • CLion verwendet direkt CMake als Buildsystem (was wir zufälligerweise auch für Icinga 2 verwenden)
  • Intelligente Auto-Completion

Die nächsten Wochen werden dann wohl zeigen, ob CLion wirklich taugt oder ich bereits nach kürzester Zeit wieder auf vim zurückfalle.

JSON-Streams über HTTP

Viele Anwendungen sind darauf angewiesen, in Echtzeit Benachrichtigungen zu bestimmten Ereignissen zu erhalten:

  • Der Benutzer soll sofort über neue E-Mails benachrichtigt werden.
  • Notifications von einem Monitoring-System sollen durch eine Anwendung in der Taskleiste angezeigt werden.
  • Nachrichten, die per Instant Messaging versendet werden, sollten zeitnah beim Empfänger ankommen.

Zur Umsetzung solcher Anwendungen bieten sich durch die Verwendung von bekannten Standardprotokollen wie HTTP signifikante Vorteile:

Vorhandene Tools und Infrastruktur

Für HTTP gibt es in nahezu jeder Programmiersprache nativ integrierte Libraries. Hierdurch sinkt für Entwickler, die einen neuen Client für das Protokoll schreiben wollen der initiale Aufwand. Zwar unterscheiden sich die Anwendungen trotzdem noch an einigen Stellen (Wie sehen einzelne Messages aus? Welche URLs werden verwendet?), dafür werden andere Themen wie Authentifizierung bereits durch den Standard abgehandelt.
Um im Fehlerfall bzw. während der Entwicklung Probleme zu analysieren gibt es bereits etliche Tools. Diese reichen von einfachen konsolen-basierten HTTP-Clients wie curl und wget über Protokoll-Analyse-Tools wie Wireshark hin zu speziell für HTTP entwickelten Debug-Hilfsmitteln wie Fiddler.
Zusätzlich unterstützt HTTP mittels HTTP-Proxies „Routing“, da es in jeder Anfrage alle notwendigen Adressinformationen bereithält.

Streaming über HTTP

Nun ist HTTP zunächst ein Protokoll, das darauf basiert, dass für eine Anfrage jeweils genau eine Antwort übermittelt wird. Wie können wir nun also dem HTTP-Client unaufgefordert Events schicken?
Die einfache Grundlage hierfür ist eine Technologie, die als „Long Polling“ bekannt ist: Der HTTP-Client sendet hierbei zunächst einen Request und bekommt vom Server auch eine Antwort. Diese Antwort hat allerdings kein Ende: Der Server sendet über die noch bestehende Verbindung JSON-kodierte Ereignisse sobald sie vorliegen.
Der Client muss dazu die Möglichkeit haben, zwischen den einzelnen JSON-kodierten Ereignissen unterscheiden zu können. Am Beispiel von zwei JSON-Ereignissen möchte ich hier auf die unterschiedlichen Ansätze eingehen.
Beispiel:

  • { „id“: 1, „message“: „Hallo Welt“ }
  • { „id“: 2, „message“: „Dies ist ein Test.“ }

Inkrementeller JSON-Parser

Ein JSON-Parser, der den Datenstrom inkrementell parsen kann, hat die Möglichkeit, zu erkennen, an welcher Stelle eine JSON-Message aufhört und die nächste beginnt. Die Messages werden hierbei also einfach aneinander gehängt. Der Body der HTTP-Antwort würde dabei etwa so aussehen:

{ "id": 1, "message": "Hallo Welt" }{ "id": 2, "message": "Dies ist ein Test." }

Vorteil hierbei ist, dass die Implementation des Servers sehr einfach ist. Allerdings können nur wenige JSON-Parser inkrementell parsen, was die Entwicklung des Clients erschwert.

Eine JSON-Message pro Zeile

Hierbei werden einzelne JSON-Messages per Newline („\n“) getrennt. Beim Encoden der Messages muss darauf geachtet werden, dass diese selbst keine Newlines beinhalten.

{ "id": 1, "message": "Hallo Welt" }
{ "id": 2, "message": "Dies ist ein Test." }

Auf Clientseite ist dies im Regelfall recht einfach umzusetzen. Die Entwicklung des Servers wird minimal dadurch erschwert, dass mit Newlines innerhalb der JSON-Messages richtig umgegangen werden muss.

Längenangabe vor jeder JSON-Message

Bei dieser Möglichkeit sendet der Server vor jeder eigentlichen Message die Länge der JSON-Daten gefolgt von einem Newline:

37
{ "id": 1, "message": "Hallo Welt" }
45
{ "id": 2, "message": "Dies ist ein Test." }

(Wer nachzählt kommt möglicherweise darauf, dass die Längenangaben um ein Byte zu groß sind. Hierbei ist zu beachten, dass ich für dieses Beispiel aus Gründen der Lesbarkeit nach den JSON-Messages ein Newline eingefügt habe, das dann auch Bestandteil der Message ist und mitgezählt werden muss.)
Im Regelfall sollte es sowohl für Client als auch Server recht einfach sein, dies umzusetzen.

Neue Distributionen für das Icinga 2-Paket-Repository

Vor Kurzem wurden Debian 8.0 („jessie“) und Ubuntu 15.04 („Vivid“) veröffentlich. Für diese beiden Plattformen gibt es nun auch auf packages.icinga.org Pakete für Icinga 2 und Icinga Web 2.
Dabei wird für Debian auch die armhf-Architektur unterstützt, die beispielsweise der Raspberry Pi 2 verwendet (für Raspbian gibt es ein separates Repository, das allerdings nichts mit diesem Debian-Repo zu tun hat):
root@dashboard:~# uname -a
Linux dashboard.beutner.name 3.18.0-trunk-rpi2 #1 SMP PREEMPT Debian 3.18.5-1~exp1.co1 (2015-02-02) armv7l GNU/Linux
root@dashboard:~# icinga2 --version
icinga2 - The Icinga 2 network monitoring daemon (version: r2.3.4-1)
Copyright (c) 2012-2015 Icinga Development Team (https://www.icinga.org)
License GPLv2+: GNU GPL version 2 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Application information:
Installation root: /usr
Sysconf directory: /etc
Run directory: /run
Local state directory: /var
Package data directory: /usr/share/icinga2
State path: /var/lib/icinga2/icinga2.state
Objects path: /var/cache/icinga2/icinga2.debug
Vars path: /var/cache/icinga2/icinga2.vars
PID path: /run/icinga2/icinga2.pid
Application type: icinga/IcingaApplication
System information:
Operating system: Linux
Operating system version: 3.18.0-trunk-rpi2
Architecture: armv7l
Distribution: Debian GNU/Linux 8.0 (jessie)
root@dashboard:~#

Unterschiedliche Datentypen mit boost::variant

In C++ gibt es Fälle, in denen eine Funktion Werte zurückliefern soll, die nicht alle denselben Datentyp besitzen. Ein Beispiel hierfür ist eine Funktion, die einen JSON-String parsen soll und den deserialisierten Wert zurückgeben soll:

<Datentyp?> JsonDeserialize(const std::string& json);

Wenn wir die Funktion in C schreiben würden, könnten wir uns mit einem einfachen Trick behelfen: wir verwenden ein „Tagged Union“ – also ein Union, zu dem wir uns zusätzlich merken, welches Feld darin aktuell gültig ist:

typedef struct {
  int tag; /* Das aktuell gültige Feld in dem Union */
  union {
    double value_double;
    char *value_string;
    ...
  };
} my_variant;

Mit C++ würde dies allerdings nicht funktionieren, da in einem Union nur POD-Typen erlaubt sind – also Datentypen, die keine Konstruktoren, Destruktoren oder andere Methoden enthalten. Der Hintergrund dabei ist, dass C++ nicht entscheiden kann, welchen Konstruktor und Destruktor es für das Union aufrufen soll, da es nicht weiss, welches Feld gültig ist.
Die Boost-Library bietet für dieses Problem eine elegante Lösung an: das boost::variant-Template kann verwendet werden, um eigene Typen zu definieren, die sich wie unser my_variant-Struct verhalten, aber zusätzlich auch nicht-triviale Typen (mit Konstruktor, Destruktor, usw.) enthalten kann:

typedef boost::variant<double, std::string, ...> my_variant;

Der my_variant-Typ verfügt dabei für jeden angegebenen Template-Parameter über einen passenden Konstruktor, der eine einfache Zuweisung erlaubt:

my_variant val = 7;

In diesem Fall wird der interne Typ des Variants auf „double“ gesetzt und der Wert 7 gespeichert.
Um zu prüfen, welchen Typ ein Variant-Wert hat, kann die Methode „which“ verwendet werden. Sie liefert einen null-basierten Index in die Template-Parameter zurück. Mit boost::get kann der Wert eines Variants extrahiert werden:

double dval = boost::get<double>(val);

Alternativ (und von der Boost-Dokumentation vorgeschlagen) kann auch das Visitor-Pattern verwendet werden, um auf die eigentlichen Werte zuzugreifen. Mehr Informationen gibt es in der Boost-Dokumentation.