monitor/check_fds.sh
fds2610-bot 55d5c34ca6 Ueberwachte Hosts ueber ein Zahnrad am System-Monitor pflegen (Issue #70)
Wer einen Server aus der Ping-Ueberwachung nehmen wollte, musste sich bisher auf den
Rechner mit check_fds.sh einloggen und die hosts.conf editieren - und die Kachel blieb
danach trotzdem im System-Monitor stehen, weil ein einmal gemeldetes System nirgends
wieder geloescht werden konnte.

Die Liste steht jetzt in der Datenbank und ist ueber ein Zahnrad neben der Ueberschrift
"Systeme" pflegbar, analog zum Zahnrad am PO-Platz (Issue #27). Jeder Host gehoert dem
OPs-Agenten (Issue #66), der ihn anpingt: API und Ping-Check laufen nicht zwangslaeufig
auf demselben Rechner, die API kann die hosts.conf also nicht selbst schreiben.
check_fds.sh holt sie stattdessen bei jedem Lauf mit seinem Agenten-Key ab und schreibt
daraus die lokale Datei; ist die API nicht erreichbar, pingt es unveraendert nach der
zuletzt geschriebenen Liste weiter.

Beim ersten Lauf laedt der Check seine bestehende hosts.conf hoch, damit die bereits
ueberwachten Server nicht von Hand nachgetragen werden muessen. Dass die API das nur
einmal annimmt (Marker in host_config_imports) und "noch nie konfiguriert" per 204 von
"im Frontend geleert" per 200 unterscheidet, ist noetig, damit ein Ping-Rechner mit alter
lokaler Datei die dort geloeschten Hosts nicht wieder anlegt.

Loeschen raeumt mit auf: Ein entfernter Host nimmt seine Kachel mit - beide Ids, denn der
status.txt-Import benennt ihn nach dem Hostnamen, die REST-Meldung nach dessen Slug.
Kacheln ohne zugehoerigen Host (Selbstmelder oder Reste laengst entfernter Hosts) stehen
im selben Dialog unter "Weitere Kacheln im System-Monitor" und sind dort einzeln
loeschbar - dafuer gibt es neu DELETE /api/v1/systems/{id}.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vNjbfr8M3HSfWMee92GTE
2026-09-01 13:14:11 +02:00

214 lines
7.5 KiB
Bash
Executable file

#!/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=""
if [ -f "$API_CONF" ]; then
# shellcheck source=/dev/null
. "$API_CONF"
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
}
# 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)"
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"