Der OPs-Agent war seit seiner Registrierung am 31.08. nie praesent: seine monitor-api.conf stammt von vor Issue #75 und kennt MONITOR_AGENT_ID nicht, report_activity() in check_fds.sh meldete deshalb stillschweigend nichts - waehrend die Systemmeldungen mit demselben Key weiter ankamen. - API: jeder mit X-Api-Key authentifizierte Request frischt last_seen_at auf (AgentRepository::touch(), Zustand/Ticket bleiben). Greift auch, wenn auf dem Ping-Rechner noch ein aelteres check_fds.sh liegt. - API: neuer Endpunkt GET /api/v1/agents/me liefert den Agenten zum Key. - check_fds.sh: fehlt MONITOR_AGENT_ID, wird sie ueber /agents/me ermittelt und in monitor-api.conf nachgetragen (ohne Admin-Key). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F5rnDxZCwLYiEVyfrAvsAA
287 lines
11 KiB
Bash
Executable file
287 lines
11 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=""
|
|
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"
|