Vor und nach der Geburt unserer Tochter bin ich dem Gedanken verfallen, dass ich meine Infrastruktur vereinfachen und reduzieren muss. In dem Zuge habe ich meine bis dahin laufende Nagios-Instanz abgeschafft und durch eine selbstgeschriebene PHP-Anwendung für die Darstellung ersetzt. Auf den Servern lief vorher schon ein Python-Script was die Nagioschecks ausgeführt hat und die Ergebnisse an das Nagios geschickt hat.

Dieses Python-Script habe ich angepasst, so dass es diese nun an die PHP-Anwendung geschickt hat. In die PHP-Anwendung habe ich eine relativ simple Logik eingebaut, wann ich per XMPP informiert werden will, wenn ein Host offline geht (also keine Daten mehr sendet) oder ein Service unzufrieden ist. Das senden der Nachricht habe ich mit go-sendxmpp realisiert.

Dann stand die Geburt unseres Sohnes an und ich hab mir wieder Gedanken darüber gemacht, ob ich wohl zukünftig noch Zeit und Energie haben werde Infrastruktur zu betreuen. Also habe ich mir wieder Gedanken gemacht was ich an Anwendungen abschaffen kann.

Inzwischen läuft bei uns ohnehin ein Home Assistant und ich stellte mir die Frage, ob man damit nicht eigentlich sinnvoll Servermonitoring machen kann.

Mein Gedanke war, dass Home Assistant ja Sensoren kennt und für die Werte dieser auch Graphen zeichnen kann.

Also habe ich das Python-Script angepasst/neugeschrieben und daraus ist nag2ha entstanden. nag2ha führt, genau wie das alte Python-Script, die konfigurierten Nagios-Plugins lokal aus und schickt deren Exit-Codes und Ausgaben über die API an das Home Assistant, wobei für jeden Check ein eigener Sensor entsteht.

Vorhandene Perfdata wird ebenfalls ausgewertet und als eigene Sensoren angelegt, wodurch Metriken als Graphen dargestellt werden können.

Zusammen mit dem Skript liegt im Repository die dazugehörige Dashboard-Karte (nag2ha-card.js), die sich optisch an die Darstellung in Nagios anlehnt.

Die XMPP-Benachrichtigungen habe ich mithilfe von Automations und der bereits existierenden XMPP-Integration von Home Assistant umgesetzt.

Die Alarm-Automation für Services reagiert im unten stehenden Beispiel bei CRITICAL, WARNING oder UNKNOWN, schickt aber erst eine Nachricht, bis der im lokalen State-File hochgezählte failure_count drei erreicht hat (Also der jeweilige Check drei mal nicht OK zurück gegeben hat):

alias: "nag2ha: Service alert"
description: "Sends an XMPP notification when a service enters an alert state"
triggers:
  - event_type: state_changed
    trigger: event
conditions:
  - condition: template
    value_template: "{{ trigger.event.data.new_state is not none }}"
  - condition: template
    value_template: >-
      {{ trigger.event.data.entity_id.startswith('sensor.nag2ha_') and
         trigger.event.data.new_state.attributes.check is defined and
         trigger.event.data.new_state.attributes.metric is not defined }}
  - condition: template
    value_template: "{{ trigger.event.data.new_state.state in ['CRITICAL', 'WARNING', 'UNKNOWN'] }}"
  - condition: template
    value_template: "{{ trigger.event.data.new_state.attributes.failure_count == 3 }}"
actions:
  - action: notify.xmpp_notify
    data:
      target: jabberid@example.com
      message: >-
        [{{ trigger.event.data.new_state.state }}] {% if trigger.event.data.new_state.attributes.fqdn %}{{ trigger.event.data.new_state.attributes.fqdn }}{% else %}{{ trigger.event.data.new_state.attributes.host }}{% endif %} / {{ trigger.event.data.new_state.attributes.check }}

        {{ trigger.event.data.new_state.attributes.output }}
mode: queued

Die Recovery-Meldung wird gesendet, sobald das Attribut recovered == true liefert und der Status wieder auf OK wechselt.

alias: "nag2ha: Service recovery"
description: "Sends an XMPP notification when a service recoveries back to OK"
triggers:
  - event_type: state_changed
    trigger: event
conditions:
  - condition: template
    value_template: "{{ trigger.event.data.new_state is not none }}"
  - condition: template
    value_template: "{{ trigger.event.data.new_state.state == 'OK' }}"
  - condition: template
    value_template: >-
      {{ trigger.event.data.entity_id.startswith('sensor.nag2ha_') and
         trigger.event.data.new_state.attributes.check is defined and
         trigger.event.data.new_state.attributes.recovered == true }}
actions:
  - action: notify.xmpp_notify
    data:
      target: jabberid@example.com
      message: >-
        [RECOVERY] {% if trigger.event.data.new_state.attributes.fqdn %}{{ trigger.event.data.new_state.attributes.fqdn }}{% else %}{{ trigger.event.data.new_state.attributes.host }}{% endif %} / {{ trigger.event.data.new_state.attributes.check }}

        {{ trigger.event.data.new_state.attributes.output }}
mode: queued

Während die Services ihren Zustand senden (wenn der Host online ist), wird ein Host, wenn er offline ist, nicht mitteilen können, dass er offline ist.

Ich will aber nicht jedes Mal, wenn ein Host sein last_seen updated, eine Nachricht bekommen, dass er online ist und genauso will ich nur einmal eine Nachricht bekommen, wenn ein Host offline ist.

Deswegen läuft die zuständige Automation einmal die Minute und guckt, ob das last_seen eines Servers älter als 5 Minuten (300 Sekunden) ist. Dann schickt die Automation eine DOWN-Meldung und trägt den Server in einen Helper ein. Beim nächsten Mal guckt die Automation, ob der Server schon im Helper steht und da er das tut, wird keine weitere DOWN-Meldung geschickt.

Bei der Meldung, dass ein Server wieder erreichbar ist, guckt die Automation, ob der Server im Helper steht, schickt dann eine UP-Meldung und entfernt den Server aus dem Helper.

Den Helper legen wir in den Einstellungen vom Home Assistant so an:

  • Pfad: Einstellungen -> Geräte & Dienste -> Helfer -> Helfer erstellen -> Text (input_text)
  • Name: nag2ha_alert_status
  • Maximale Länge: 255
alias: "nag2ha: Host status"
description: >-
  Checks host heartbeats every minute. Alerts once per host on failure and
  recovery.
triggers:
  - trigger: time_pattern
    minutes: /1
conditions: []
actions:
  - repeat:
      for_each: >
        {# Find all hosts that are overdue (>300s) AND not yet marked as down in
        our helper #} {% set alert_list =
        states('input_text.nag2ha_alert_status').split(',') | map('trim') | list
        %} {% set now_ts = as_timestamp(now()) | int %} {% set hosts =
        states.sensor 
           | selectattr('entity_id', 'match', '^sensor\.nag2ha_[^_]+$') 
           | selectattr('attributes.last_seen', 'defined') | list %}
        {% set ns = namespace(overdue=[]) %} {% for h in hosts %}
          {% if now_ts - (h.attributes.last_seen | int) > 300 %}
            {% set ns.overdue = ns.overdue + [h.entity_id] %}
          {% endif %}
        {% endfor %} {{ ns.overdue | reject('in', alert_list) | list }}
      sequence:
        - action: notify.xmpp_notify
          data:
            target: jabberid@example.com
            message: >-
              [DOWN] {{ state_attr(repeat.item, 'fqdn') or
              state_attr(repeat.item, 'host') }}
        - action: input_text.set_value
          target:
            entity_id: input_text.nag2ha_alert_status
          data:
            value: >
              {% set current = states('input_text.nag2ha_alert_status') %} {{
              (current + ',' + repeat.item) if current != 'unknown' and current
              != '' else repeat.item }}
  - repeat:
      for_each: >
        {# Find all hosts that are online again (<300s) BUT are still marked as
        down in our helper #} {% set alert_list =
        states('input_text.nag2ha_alert_status').split(',') | map('trim') | list
        %} {% set now_ts = as_timestamp(now()) | int %} {% set hosts =
        states.sensor 
           | selectattr('entity_id', 'match', '^sensor\.nag2ha_[^_]+$') 
           | selectattr('attributes.last_seen', 'defined') | list %}
        {% set ns = namespace(online=[]) %} {% for h in hosts %}
          {% if now_ts - (h.attributes.last_seen | int) <= 300 %}
            {% set ns.online = ns.online + [h.entity_id] %}
          {% endif %}
        {% endfor %} {{ ns.online | select('in', alert_list) | list }}
      sequence:
        - action: notify.xmpp_notify
          data:
            target: jabberid@example.com
            message: >-
              [UP] {{ state_attr(repeat.item, 'fqdn') or state_attr(repeat.item,
              'host') }}
        - action: input_text.set_value
          target:
            entity_id: input_text.nag2ha_alert_status
          data:
            value: >
              {% set alert_list =
              states('input_text.nag2ha_alert_status').split(',') | map('trim')
              | list %} {{ alert_list | reject('eq', repeat.item) | join(',') }}
mode: single
max_exceeded: silent