#!/bin/bash # Pfade relativ zum Skript aufloesen, nicht zum Arbeitsverzeichnis: der Cronjob startet # das Skript zwar mit "cd", ein Aufruf von aussen laese sonst aber die falsche hosts.conf # (oder gar keine). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Pfad zur Textdatei mit Hostname-IP-Paaren (pro Zeile: hostname ip) HOST_FILE="$SCRIPT_DIR/hosts.conf" # Pfad zum mail_fds.py-Skript MAIL_SCRIPT="$SCRIPT_DIR/mail_fds.py" # Pfad zur Statusdatei STATUS_FILE="/var/www/html/status.txt" # Zugangsdaten fuer die Statusmeldung per REST (siehe monitor-api-sample.conf) API_CONF="$SCRIPT_DIR/monitor-api.conf" # Die Meldung per REST ist optional: fehlt monitor-api.conf oder ist sie leer, schreibt der # Check wie bisher nur status.txt und mailt bei Ausfall. Noetig ist sie dort, wo Ping-Check # und API auf verschiedenen Rechnern laufen - der Legacy-Import aus status.txt (F6) liest # eine lokale Datei und erreicht die API dann nicht. MONITOR_API_BASE="" MONITOR_API_KEY="" MONITOR_AGENT_ID="" if [ -f "$API_CONF" ]; then # shellcheck source=/dev/null . "$API_CONF" fi # Aeltere monitor-api.conf (vor Issue #75) kennen MONITOR_AGENT_ID nicht - dann blieb die # Zustandsmeldung unten stillschweigend aus. Die ID wird hier ueber GET /api/v1/agents/me mit dem # eigenen Key ermittelt und in die Datei nachgetragen, damit sich der Ping-Rechner nach einem # Update selbst repariert, ohne dass jemand register-ops-agent.sh (braucht den Admin-Key) laufen # laesst. Schlaegt das fehl, laeuft die Ueberwachung unveraendert weiter. resolve_agent_id() { if [ -n "$MONITOR_AGENT_ID" ] || [ -z "$MONITOR_API_BASE" ] || [ -z "$MONITOR_API_KEY" ]; then return 0 fi local response id if ! response=$(curl -s --fail --max-time 10 \ -H "X-Api-Key: ${MONITOR_API_KEY}" \ "${MONITOR_API_BASE%/}/api/v1/agents/me"); then echo " Warnung: MONITOR_AGENT_ID fehlt und liess sich nicht von der API ermitteln, Zustand wird nicht gemeldet." return 0 fi # Das erste "id" der Antwort {"agent":{"id":2,...}} ist die Agenten-ID. id=$(printf '%s' "$response" | grep -oE '"id": *[0-9]+' | head -n1 | grep -oE '[0-9]+' || true) if [ -z "$id" ]; then echo " Warnung: MONITOR_AGENT_ID fehlt, die API lieferte keine Agenten-ID." return 0 fi MONITOR_AGENT_ID="$id" # Angehaengt statt ersetzt: eine spaetere Zuweisung gewinnt beim naechsten Sourcen auch # gegen ein vorhandenes MONITOR_AGENT_ID="". if printf '\n# Von check_fds.sh nachgetragen (fehlte, Issue #75)\nMONITOR_AGENT_ID="%s"\n' "$id" >> "$API_CONF" 2>/dev/null; then echo " MONITOR_AGENT_ID fehlte in $API_CONF und wurde nachgetragen (ID $id)." else echo " Warnung: MONITOR_AGENT_ID ($id) liess sich nicht in $API_CONF schreiben, gilt nur fuer diesen Lauf." fi } # Issue #70: Die Liste der ueberwachten Hosts wird im Frontend gepflegt (Zahnrad am # Wandmonitor "Systeme"). Dieser Aufruf holt sie vor jedem Durchlauf ab und schreibt daraus # die lokale hosts.conf - deshalb muss auf diesem Rechner nichts mehr von Hand editiert # werden. Die Ueberwachung darf dabei nie stehenbleiben: Ist die API nicht erreichbar oder # antwortet sie mit einem Fehler, bleibt die zuletzt geschriebene hosts.conf in Kraft. sync_hosts_from_api() { if [ -z "$MONITOR_API_BASE" ] || [ -z "$MONITOR_API_KEY" ]; then return 0 fi local tmp code count tmp=$(mktemp) || return 0 if ! code=$(curl -s -o "$tmp" -w '%{http_code}' --max-time 10 \ -H "X-Api-Key: ${MONITOR_API_KEY}" \ "${MONITOR_API_BASE%/}/api/v1/hosts/conf"); then echo " Warnung: API unter $MONITOR_API_BASE nicht erreichbar, benutze die lokale $HOST_FILE." rm -f "$tmp" return 0 fi # 204 heisst: fuer diesen Rechner ist serverseitig noch nichts konfiguriert. Dann geht # die bestehende lokale Datei einmalig an die API, damit die bereits ueberwachten Hosts # nicht von Hand im Frontend nachgetragen werden muessen. Ein spaeterer Aufruf wird mit # 200 beantwortet - auch bei leerer Liste; eine im Frontend geleerte Liste fuellt dieser # Zweig also nicht wieder auf. if [ "$code" = "204" ]; then if [ -s "$HOST_FILE" ]; then local imported imported=$(curl -s --max-time 10 -X POST \ "${MONITOR_API_BASE%/}/api/v1/hosts/import" \ -H "Content-Type: text/plain" \ -H "X-Api-Key: ${MONITOR_API_KEY}" \ --data-binary "@$HOST_FILE") case "$imported" in *'"imported":0'*) : ;; *'"imported"'*) echo " Bestehende $HOST_FILE an die API uebergeben." ;; *) echo " Warnung: Uebergabe der $HOST_FILE an die API fehlgeschlagen." ;; esac fi rm -f "$tmp" return 0 fi if [ "$code" != "200" ]; then echo " Warnung: API antwortet mit HTTP $code auf die Host-Liste, benutze die lokale $HOST_FILE." rm -f "$tmp" return 0 fi # Zeilen, die check_fds.sh spaeter auch wirklich pingt (ohne Kommentare/Leerzeilen). count=$(grep -cvE '^[[:space:]]*(#|$)' "$tmp") if ! cmp -s "$tmp" "$HOST_FILE"; then # cat statt mv: so behalten Besitzer und Rechte der bestehenden Datei ihren Wert, # und ein abgebrochener Schreibvorgang trifft keine Datei aus /tmp mit 600/root. if cat "$tmp" > "$HOST_FILE"; then echo " Host-Liste aus der API uebernommen ($count Hosts)." else echo " Warnung: $HOST_FILE ist nicht schreibbar, benutze die bisherige Liste." fi fi rm -f "$tmp" } # Erzeugt aus dem Hostnamen eine URL-sichere System-ID. Der Router der API matcht auf dem # rohen, nicht dekodierten Pfad (api/src/Http/Router.php) - eine prozentkodierte ID landete # also kodiert in der Datenbank. Der lesbare Originalname geht im Feld "name" mit. system_id() { local slug slug=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \ | sed -e 's/[^a-z0-9]\+/-/g' -e 's/^-//' -e 's/-$//') printf '%s' "${slug:-unbekannt}" } json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' } # Meldet einen Host per POST /api/v1/systems/{id}/status. Fehler werden nur protokolliert: # eine unerreichbare API darf die Ping-Ueberwachung nicht anhalten. report_to_api() { local hostname="$1" local ip="$2" local status="$3" if [ -z "$MONITOR_API_BASE" ] || [ -z "$MONITOR_API_KEY" ]; then return 0 fi local api_status="error" if [ "$status" = "online" ]; then api_status="ok" fi local payload payload=$(printf '{"status":"%s","name":"%s","type":"host","detail":"%s"}' \ "$api_status" "$(json_escape "$hostname")" "$(json_escape "$ip")") local code if ! code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \ -X POST "${MONITOR_API_BASE%/}/api/v1/systems/$(system_id "$hostname")/status" \ -H "Content-Type: application/json" \ -H "X-Api-Key: ${MONITOR_API_KEY}" \ -d "$payload"); then echo " Warnung: API unter $MONITOR_API_BASE nicht erreichbar, $hostname nicht gemeldet." return 0 fi if [ "$code" != "200" ]; then echo " Warnung: API antwortet mit HTTP $code auf die Meldung fuer $hostname." fi } # Meldet den eigenen Zustand per POST /api/v1/agents/{id}/activity (Issue #75): ohne diese # Meldung faellt der OPs-Agent zehn Minuten nach seiner Registrierung aus der Praesenz # (AgentRepository::PRESENCE_TIMEOUT_SECONDS), obwohl der Ping-Check laengst laeuft - bisher # schrieb kein Aufruf in check_fds.sh je last_seen_at. MONITOR_AGENT_ID fehlt in aelteren # monitor-api.conf; ist es leer, meldet die Funktion wie report_to_api still nichts, statt # den Lauf abzubrechen. report_activity() { local state="$1" if [ -z "$MONITOR_API_BASE" ] || [ -z "$MONITOR_API_KEY" ] || [ -z "$MONITOR_AGENT_ID" ]; then return 0 fi local code if ! code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \ -X POST "${MONITOR_API_BASE%/}/api/v1/agents/$MONITOR_AGENT_ID/activity" \ -H "Content-Type: application/json" \ -H "X-Api-Key: ${MONITOR_API_KEY}" \ -d "{\"state\":\"$(json_escape "$state")\"}"); then echo " Warnung: API unter $MONITOR_API_BASE nicht erreichbar, Zustand '$state' nicht gemeldet." return 0 fi if [ "$code" != "200" ]; then echo " Warnung: API antwortet mit HTTP $code auf die Zustandsmeldung '$state'." fi } # Funktion zum Prüfen einer IP und Schreiben des Status check_ip() { local hostname="$1" local ip="$2" local success_count=0 # echo "Prüfe $hostname ($ip) ..." for i in {1..3}; do if ping -c 1 -W 2 "$ip" &> /dev/null; then ((success_count++)) else echo " Ping $i an $ip fehlgeschlagen." fi done local status="online" if [ "$success_count" -lt 2 ]; then status="offline" echo " Mehr als ein Ping fehlgeschlagen für $hostname ($ip). Führe $MAIL_SCRIPT aus." "$MAIL_SCRIPT" --subject "Ping-Fehler bei $hostname $ip" --credentials "$SCRIPT_DIR/credentials.conf" else echo " $hostname ($ip) ist erreichbar." fi report_to_api "$hostname" "$ip" "$status" # Status in status.conf schreiben/aktualisieren local now=$(date +"%Y-%m-%d %H:%M") awk -v h="$hostname" -v i="$ip" -v s="$status" -v d="$now" ' BEGIN { found=0 } { if ($1 == h && $2 == i) { print h, i, s, d; found=1; } else { print $0; } } END { if (!found && h != "" && i != "") { print h, i, s, d; } }' "$STATUS_FILE" > "$STATUS_FILE.tmp" && mv "$STATUS_FILE.tmp" "$STATUS_FILE" } # Hauptskript echo "Pruefung: $(date)" # Rahmt den ganzen Lauf, damit der Agent auch bei einem fruehen "exit 1" (fehlende # hosts.conf/mail_fds.py weiter unten) nicht dauerhaft als "working" haengen bleibt - # analog zum Cron-Skript des Entwickler-Agenten, das ebenfalls working/idle um seinen Lauf # herum meldet (Issue #75). resolve_agent_id trap 'report_activity idle' EXIT report_activity working sync_hosts_from_api if [ ! -f "$HOST_FILE" ]; then echo "Fehler: Datei $HOST_FILE nicht gefunden." exit 1 fi if [ ! -f "$MAIL_SCRIPT" ]; then echo "Fehler: Skript $MAIL_SCRIPT nicht gefunden." exit 1 fi # Statusdatei anlegen, falls nicht vorhanden touch "$STATUS_FILE" # "|| [ -n "$line" ]" nimmt auch eine letzte Zeile ohne abschliessenden Zeilenumbruch mit - # ohne sie verschluckt read den zuletzt angehaengten Host stillschweigend. while read -r line || [ -n "$line" ]; do case "$line" in \#*|"") continue ;; esac hostname=$(echo "$line" | awk '{print $1}') ip=$(echo "$line" | awk '{print $2}') if [ -n "$hostname" ] && [ -n "$ip" ]; then check_ip "$hostname" "$ip" fi done < "$HOST_FILE"