Select Page

NETWAYS Blog

Jabber-Notifications mit Icinga 2

Um mit Icinga 2 einfach Notifications an Jabber-Kontakte senden zu können, habe ich mir folgendes Script geschrieben:

#!/usr/bin/env python
import xmpp, os, sys
if len(sys.argv) < 3:
    print "Syntax:", sys.argv[0], " "
    sys.exit(1)
jid = xmpp.protocol.JID(os.environ["XMPP_USER"])
cl = xmpp.Client(jid.getDomain(), debug = [])
con = cl.connect()
cl.auth(jid.getNode(), os.environ["XMPP_PASSWORD"])
cl.sendInitPresence()
msg = xmpp.Message(sys.argv[1], sys.argv[2])
msg.setAttr('type', 'chat')
cl.send(msg)

Das Script wird dabei in /etc/icinga2/scripts mit dem Dateinamen jabber-notification.py abgelegt werden. Zusätzlich muss noch die Python-XMPP-Library installiert werden, für die es z.B. in Debian das Paket python-xmpp gibt.
Um das Script mit Icinga verwenden zu können, müssen zunächst folgende allgemeine Templates definiert werden:

template NotificationCommand "jabber-template" {
  import "plugin-notification-command"
  command = [
    SysconfDir + "/icinga2/scripts/jabber-notification.py",
    "$xmpp_recipient$",
    "$xmpp_message$"
  ]
  vars.xmpp_recipient = "$jabber$"
  // Wir übergeben den Benutzernamen und das Passwort per Environment-
  // Variablen an das Script, damit diese nicht per ps(1)
  // für andere Benutzer einsehbar sind.
  env = {
    XMPP_USER = "$xmpp_user$"
    XMPP_PASSWORD = "$xmpp_password$"
  }
}
template NotificationCommand "jabber-host-notification" {
  import "jabber-template"
  vars.xmpp_message = {{{Notification Type: $notification.type$
Host: $host.display_name$
Address: $address$
State: $host.state$
Date/Time: $icinga.long_date_time$
Additional Info: $host.output$
Comment: [$notification.author$] $notification.comment$}}}
}
template NotificationCommand "jabber-service-notification" {
  import "jabber-template"
  vars.xmpp_message = {{{Notification Type: $notification.type$
Service: $service.name$
Host: $host.display_name$
Address: $address$
State: $service.state$
Date/Time: $icinga.long_date_time$
Additional Info: $service.output$
Comment: [$notification.author$] $notification.comment$}}}
}

Den beiden Templates "jabber-host-notification" und "jabber-service-notification" fehlen dabei noch die Custom Attribute für den Jabber-Benutzernamen und -Passwort. Um diese anzugeben, definieren wir zwei Commands:

object NotificationCommand "jabber-host-netways" {
  import "jabber-host-notification"
  vars.xmpp_user = "jabber-user@example.org"
  vars.xmpp_password = "passwort"
}
object NotificationCommand "jabber-service-netways" {
  import "jabber-service-notification"
  vars.xmpp_user = "jabber-user@example.org"
  vars.xmpp_password = "passwort"
}

Anschließend können wir diese Commands für Notifications verwenden:

object User "gunnar" {
  vars.jabber = "gunnar@beutner.name"
}
apply Notification "jabber-host" to Host {
  command = "jabber-host-netways"
  users = [ "gunnar" ]
  assign where true
}
apply Notification "jabber-service" to Service {
  command = "jabber-service-netways"
  users = [ "gunnar" ]
  assign where true
}

Die Command-Templates sind dabei so parametrisiert, dass in den einzelnen Notifications bei Bedarf auch die Texte für die Jabber-Messages über das Custom-Attribut xmpp_message überschrieben werden können.

Benutzerfreundliche Fehlermeldungen

Der erste Schritt bei Fehlern ist meist der Versuch, anhand von Logmeldungen nachzuvollziehen, was die Anwendung zuletzt gemacht hat, bevor das Problem aufgetreten ist. Hier zeigt sich dann schnell, wie gut die Anwendung darauf vorbereitet ist, dem Benutzer bei der Fehlersuche zu helfen. Dazu einige Beispiel:

[2014-05-28 09:47:04 +0200] <Q #0x7f5a08c68780 W #0x7f5a08c688c0> critical/remote: Cannot connect to host 'voip.beutner.name' on port '5665'

Auf den ersten Blick sieht es so aus, als würde die Verbindung nicht aufgebaut werden können. Aber es fehlen einige wichtige Informationen, um die Logmeldung in den Kontext einordnen zu können:

  • Wer hat aus welchem Grund versucht die Verbindung aufzubauen bzw. was für eine Art Verbindung ist es? (Datenbank? Cluster? Was ganz anderes?)
  • Warum ist der Verbindungsaufbau gescheitert (“Connection refused” in diesem Fall, wenn man mit strace nachschaut, warum der connect()-Aufruf fehlschlägt)?
  • Welche Auswirkungen hat dies (in diesem Fall wird der Verbindungsaufbau periodisch erneut versucht, wodurch sich auch die Frage stellt, ob der Fehler wirklich “critical” ist)?

Die andere Seite des Spektrums bietet eine wahre Informationsüberflutung und ist mindestens genauso schlecht:

Caught unhandled exception.
Current time: 2014-05-28 09:46:51 +0200
***
* Application version: v2.0.0-beta1-8-g641ff1f
* Installation root: /home/gbeutner/i2
* Sysconf directory: /home/gbeutner/i2/etc
* Local state directory: /home/gbeutner/i2/var
* Package data directory: /home/gbeutner/i2/share/icinga2
* State path: /home/gbeutner/i2/var/lib/icinga2/icinga2.state
* PID path: /home/gbeutner/i2/var/run/icinga2/icinga2.pid
* Application type: icinga/IcingaApplication
***
/home/gbeutner/icinga2/lib/base/application.cpp(671): Throw in function void icinga::Application::UpdatePidFile(const icinga::String &, pid_t)
Dynamic exception type: boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<std::runtime_error> >
std::exception::what: Could not open PID file '/home/gbeutner/i2/var/run/icinga2/icinga2.pid'
[icinga::StackTrace*] =
(0) libbase.so: void boost::throw_exception<boost::exception_detail::error_info_injector<std::runtime_error> >(boost::exception_detail::error_info_injector<std::runtime_error> const&) (+0xb3) [0x7fec91f6b323] (throw_exception.hpp:61)
(1) libbase.so: void boost::exception_detail::throw_exception_<std::runtime_error>(std::runtime_error const&, char const*, char const*, int) (+0x66) [0x7fec91f64c96] (exception.hpp:276)
(2) libbase.so: icinga::Application::UpdatePidFile(icinga::String const&, int) (+0xe7) [0x7fec91f5de87] (application.cpp:671)
(3) libbase.so: icinga::Application::Run() (+0xd9) [0x7fec91f627f9] (basic_string.h:287)
(4) icinga2: Main() (+0x740c) [0x4250cc] (??:0)
(5) icinga2: main (+0x25) [0x4252a5] (??:0)
(6) libc.so.6: __libc_start_main (+0xfd) [0x7fec9023dead] (libc-start.c:276)
(7) ./sbin/icinga2() [0x41dbf9]
[icinga::ContextTrace*] =
***
* This would indicate a runtime problem or configuration error. If you believe this is a bug in Icinga 2
* please submit a bug report at  and include this stack trace as well as any other
* information that might be useful in order to reproduce this problem.
***
Aborted

In dieser Fehlermeldung sind soviele Informationen versteckt, dass es für den Benutzer schon teilweise schwierig wird, überhaupt die eigentliche Fehlermeldung (“Could not open PID file ‘/home/gbeutner/i2/var/run/icinga2/icinga2.pid'”) zu finden. Und trotzdem fehlt hier eigentlich eine ganz entscheidende Information: Warum ist das Öffnen der Datei denn eigentlich gescheitert?
Für mich als Entwickler sind solche Fehlermeldungen natürlich sehr praktisch: Ich weiss genau – abgesehen vom fehlenden Fehlercode, in welcher Zeile und zu welchem Zeitpunkt der Fehler aufgetreten ist; die Fehlermeldung beinhaltet die Pfade zu wichtigen Dateien. Diese Details helfen mir, das Problem zu reproduzieren bzw. evtl sogar direkt anhand des Stacktraces zu finden.
Die beiden Beispiele verdeutlichen einige Eigenschaften, die Fehlermeldungen haben sollen, um den Benutzer bei der Fehlersuche zu helfen:

  • Sie sollten kurz und einfach zu verstehen sein (keine Stacktraces, Klassennamen, o.ä.)
  • Sie sollten alle wichtigen Informationen enthalten (Dateinamen, Fehlercode)
  • Sie sollten den Benutzer in Richtung der Problemlösung leiten (z.B. bei EPERM -> Hinweis auf Datei-Berechtigungen)
  • Zusätzlich sollten die für Entwickler wichtigen Informationen aber trotzdem bereitgehalten werden; vielleicht als Referenz auf eine separate Log-Datei, die dann auch gerne seitenlange Stacktraces enthalten kann

Dass bei den Logmeldungen von Icinga 2 dieses Ideal noch nicht erreicht ist, haben wir im Bugtracker zusammengefasst und arbeiten fleissig daran, dies noch zu verbessern. Wer beim Testen noch auf etwas unhandliche Fehlermeldungen stößt, sollte dazu Issues erstellen, damit wir diese für die Beta 2 noch anpassen können.

Icinga 2 und Auto-Discovery mit SNMP

Nachdem ich meine eigenen VMs lange Zeit mit “Monitoring per Jabber” überwacht habe (sprich: User schreiben mir Jabber-Nachrichten/Mails/u.ä., wenn etwas nicht mehr geht), habe ich mich endlich entschlossen, diese automatisiert zu überwachen.
Hierbei wollte ich meinen Usern auch gleich die Möglichkeit geben, zusätzlich zu den von mir vordefinierten Checks, eigene Services zu überwachen, ohne ihnen dabei direkt Zugriff auf meine Monitoring-Config zu geben.
Bei meinen Tests hat sich SNMP dabei als recht flexibel erwiesen. Zunächst bekommt jede VM eine eigene Community zugewiesen, damit die VMs nicht gegenseitig auf ihre Monitoring-Daten zugreifen können. Der jeweilige Community-Name ist dabei der HMAC-Hash der IP-Adresse. Der HMAC-Schlüssel ist dabei geheim und wird auf den VMs selbst nirgendwo gespeichert.
Die eigene Community bekommen die VMs über ein PHP-Script:

<?php
$pkey = "hmac_schlüssel_hier";
echo substr(hash_hmac("sha256", $_SERVER["REMOTE_ADDR"], $pkey), 0, 12);

Das Installationsscript für die VMs sieht dabei in etwa so aus:

#!/bin/sh
set -e
apt-get install -y snmpd nagios-plugins-basic
COMMUNITY=$(curl 
cat >/etc/snmp/snmpd.conf <<CONFIG
agentAddress udp:0.0.0.0:161
view systemonly included .1.3.6.1.4.1.8072.1.3
rocommunity $COMMUNITY default -V systemonly
rouser authOnlyUser
extend apt /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_apt
extend disk /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_disk -w 10 -c 5 -p /
extend swap /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_swap -w 50 -c 25
extend procs /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_procs -w 500 -c 1000
extend users /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_users -w 10 -c 25
extend load /opt/monitoring/exec_plugin /usr/lib/nagios/plugins/check_load -w 5 -c 10
CONFIG
service snmpd restart
mkdir -p /opt/monitoring
cat >/opt/monitoring/exec_plugin <<SCRIPT
#!/bin/sh
output=\$(\$@ 2>&1)
status=\$?
echo \$status \$output
SCRIPT
chmod +x /opt/monitoring/exec_plugin

Bei meiner SNMP-Config habe ich bewusst darauf verzichtet, “normale” OIDs abzufragen, sondern verwende ausschließlich extend. Der Vorteil davon ist, dass ich auf Seite der Monitoring-VM ein einziges Plugin zum Checken aller Services benötige.
Um zu erkennen, welche Services eine VM hat, habe ich ein Script geschrieben, das anhand eines SNMP-Walks Icinga-Templates erstellt. Dieses läuft als Cronjob und aktualisiert in regelmäßigen Abständen die Config, sofern die jeweiligen Hosts erreichbar sind:

$ /opt/monitoring/discover-snmp.py --key hmac_schlüssel_hier 213.95.47.212
IP address: 213.95.47.212, SNMP Community: xxxxxxx
template Host "snmp-extend:213.95.47.212" {
  macros["community"] = "xxxxxxx",
  services["apt"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "apt"
  },
  services["disk"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "disk"
  },
  services["load"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "load"
  },
  services["swap"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "swap"
  },
  services["procs"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "procs"
  },
  services["users"] = {
    templates = [ "snmp-extend-service" ],
    check_command = "snmp-extend",
    macros["plugin"] = "users"
  },
}

Das Check-Plugin für den “snmp-extend”-Check fragt dabei mit snmpget die entsprechende OID ab. Prinzipiell wird hier SNMP eigentlich nur als Transport für die Check-Results verwendet.
Um das ganze noch etwas benutzerfreundlicher zu gestalten, wären noch einige Änderungen notwendig:

  • idealerweise gäbe es statt dem Installationsscript ein updatebares Debian-Paket (oder RPM, MSI, etc. – wobei ich selbst eigentlich nur Debian-VMs habe)
  • Die Services sollten jeweils in einer einzelnen Datei sein, dies würde das Aktualisieren der snmpd-Config erleichtern
  • Je nach Umgebung wäre es praktisch, snmpd auf einem anderen Port zu starten – und eine separate Config-Datei zu verwenden
  • SNMPv3 unterstützt Verschlüsselung; für meinen Anwendungsfall ist dies aber nicht notwendig, da die anderen VMs entweder auf demselben Server sind oder ausschließlich per VPN erreichbar sind

Statische Code-Analyse mit Coverity

Vor nicht allzu langer Zeit hatte ich bereits über das Code-Analyse-Feature von clang gebloggt. Ein anderes, eher aus dem kommerziellen Umfeld bekanntes Tool ist Coverity SAVE, das neben C und C++ auch C# und Java unterstützt.
Interessant für Projekte wie Icinga ist es deswegen, weil Coverity es gestattet, Open Source-Projekte registrieren zu lassen und diese dann regelmäßig z.B. per Integration in das eigene Build-System gratis überprüfen zu lassen. Dieses Angebot haben inzwischen etliche prominente Projekte angenommen, so beispielsweise auch der Linux-Kernel und der Apache-Webserver.
Seit Kurzem ist dort nun auch Icinga vertreten, wobei die eigentliche Arbeit damit nun erst anfängt – nämlich, die Issues zu kategorisieren und anschließend Patches zu bauen. Dabei findet Coverity einige interessante Bugs, die sonst so vermutlich Niemandem aufgefallen wären:
Coverity Scan: Icinga 1.x

Heap-Fragmentation analysieren

Bei Dateisystemen ist der Fragmentations-Effekt ziemlich bekannt:

  1. Zunächst werden viele Dateien erstellt.
  2. Einige dieser Dateien werden anschließend gelöscht.
  3. Wenn nun eine große Datei angelegt wird und anderweitig kein Platz mehr frei ist, wird diese in mehrere Teile aufgesplittet, um die entstandenen Lücken zu füllen. – Es entsteht Fragmentation.

Während das Problem bei Dateisystemen durch Neuanordnung der Dateiteile (Defragmentation) vergleichsweise einfach gelöst werden kann, gibt es beim Arbeitsspeicher ein ähnliches Problem, das sich nicht ganz so einfach in den Griff bekommen lässt.
Für malloc() muss die libc immer mehr Speicher vom Kernel anfordern, wenn kein Speicherbereich mehr frei ist, der groß genug ist. Dabei kann die libc bestehende Allocations nicht im Speicher verschieben, da sie nicht weiss, wo die Anwendung Pointer auf diese Allocations besitzt. Eine nachträgliche Defragmentierung ist somit nicht möglich.
Um dieses Problem zu veranschaulichen, habe ich einen Profiler geschrieben (fprofile.cpp), der malloc(), realloc() und free() instrumentiert, um beobachten zu können, wie sich die Speicherbenutzung bei meinem Testprogramm (Icinga 2) mit der Zeit verändert.
Der Profiler ist eine Library, die prinzipiell aus zwei Teilen besteht:

  • Einem eigenen Allocator: Der Profiler benötigt für die Profil-Daten selbst Speicher. Wenn ich hier malloc() verwenden würde, würde der Profiler eventuell selbst zur Heap-Fragmentation beitragen. Deswegen habe ich einen vergleichsweise naiven Allocator geschrieben, der intern mmap() verwendet.
  • Dem eigentlichen Profiler: Um malloc(), realloc() und free() durch meine eigenen Funktionen ersetzen zu können, muss die Library per LD_PRELOAD geladen werden. Der Profiler merkt sich, welche Speicherbereiche von malloc() “belegt” wurden und schreibt diese Informationen sekündlich als Bild in eine Datei. Die Wahl ist dabei auf das PNM-Format gefallen, da es trivial zu schreiben ist und keine externen Libraries benötigt (http://en.wikipedia.org/wiki/Portable_anymap).

Der Profiler kann mit folgendem Befehl kompiliert werden:

g++ -fPIC -shared -g -o libfprofiler.so fprofiler.cpp -pthread

Anschließend können wir unsere eigene Anwendung wie folgt profilen:

LD_PRELOAD=./libfprofiler.so ./sbin/icinga2 -c ...

Der Profiler erstellt nun im aktuellen Verzeichnis seine Reports:

$ ls *.pnm
frame100.pnm  frame105.pnm  frame110.pnm  frame115.pnm  frame120.pnm
frame101.pnm  frame106.pnm  frame111.pnm  frame116.pnm  frame121.pnm
frame102.pnm  frame107.pnm  frame112.pnm  frame117.pnm  frame122.pnm
frame103.pnm  frame108.pnm  frame113.pnm  frame118.pnm  frame123.pnm
frame104.pnm  frame109.pnm  frame114.pnm  frame119.pnm  frame124.pnm

Die Dateien können wir nun mit einem beliebigen Bildbearbeitungsprogramm öffnen (GIMP, EOG, usw.).
Jeder Pixel steht dabei für eine Speicherseite (4kB). Weiß bedeutet, dass zumindest ein Teil der Seite belegt ist. Schwarze Pixel stehen für komplett freie Seiten. Wenn zwischen den Adressen von zwei Allocations mehr als 100MB Unterschied ist, zeichnet der Profiler eine blaue Linie.
In den folgenden beiden Beispielreports ist der Speicherverbrauch von Icinga 2 zu sehen. Im ersten Bild ist Icinga 2 dabei gerade mit dem Parsen der Config fertig geworden – während das zweite Bild den Stand zeigt, nachdem Icinga 2 alle Speicherbereiche freigegeben hat, die der Config-Parser intern benötigt hat:
frame120
frame130
Sehr schön sieht man im zweiten Bild, wie der Heap nun ziemlich viele Lücken aufweist. Auch wenn rein rechnerisch nur ein Bruchteil des Speichers tatsächlich verwendet wird, kann die libc diese Speicherbereiche nicht an den Kernel zurückgeben.
Im Allgemeinen gibt es folgende Lösungsansätze, um Heap-Fragmentation zu vermeiden:

  • Unterschiedliche Heaps für Objekte mit unterschiedlicher Lebensdauer verwenden (z.B. eigener Heap für den Config-Parser)
  • Anzahl der gleichzeitig vorhandenen Allocations verringern
  • Speicherverbrauch allgemein verringern

Abschließend noch ein Tipp: Mit ffmpeg kann man aus den *.pnm-Dateien einen Film generieren:

ffmpeg -f image2 -r 1 -pattern_type glob -i 'frame*.pnm' -r 24 output.mp4