#!/usr/bin/env bash # Testvorgehen für den Cluster knaster (docs/BETRIEB.md, Abschnitt 13). Läuft auf dem Admin-Laptop # (Git-Bash) oder auf ops, nur lesend. Jede Zeile ist eine Prüfung mit OK/FEHLER, Exit 1 bei Fehlern. # # platform/proxmox/scripts/healthcheck.sh # alle Knoten # platform/proxmox/scripts/healthcheck.sh pve44 pve52 set -uo pipefail nodes=("$@"); [ ${#nodes[@]} -gt 0 ] || nodes=(pve44 pve46 pve48 pve52 pve61) fail=0 ok() { printf ' OK %s\n' "$1"; } bad() { printf ' FEHLER %s\n' "$1"; fail=1; } check() { local msg=$1; shift; if "$@" >/dev/null 2>&1; then ok "$msg"; else bad "$msg"; fi; } r() { ssh -T -o BatchMode=yes -o ConnectTimeout=8 "$@"; } echo "== Cluster (über ${nodes[0]})" status=$(r "${nodes[0]}" pvecm status 2>/dev/null) grep -q 'Quorate: *Yes' <<<"$status" && ok "Quorum vorhanden" || bad "kein Quorum" votes=$(awk '/^Total votes/ {print $3}' <<<"$status"); expected=$(awk '/^Expected votes/ {print $3}' <<<"$status") [ -n "$votes" ] && [ "$votes" = "$expected" ] && ok "alle Knoten im Cluster ($votes/$expected)" || bad "Knoten fehlen ($votes/$expected)" ha=$(r "${nodes[0]}" ha-manager status 2>/dev/null) grep -q 'quorum OK' <<<"$ha" && ok "HA-Manager: Quorum OK" || bad "HA-Manager meldet kein Quorum" grep -E '^service' <<<"$ha" | grep -vq 'started' && bad "HA-Dienst nicht gestartet: $(grep -E '^service' <<<"$ha" | grep -v started | tr '\n' ';')" || ok "HA-Dienste gestartet" repl=$(r "${nodes[0]}" pvesr status 2>/dev/null | tail -n +2) [ -n "$repl" ] && { grep -vqE ' OK$' <<<"$repl" && bad "Replikation mit Fehler: $(grep -vE ' OK$' <<<"$repl" | tr '\n' ';')" || ok "Replikation: alle Jobs OK"; } fw=$(r "${nodes[0]}" cat /etc/pve/firewall/cluster.fw 2>/dev/null) grep -q '^enable: 1' <<<"$fw" && ok "Datacenter-Firewall aktiv" || bad "Datacenter-Firewall nicht aktiv" grep -q 'IN ACCEPT -source +admin' <<<"$fw" && ok "Firewall: Regel für +admin vorhanden" || bad "Firewall: Regel für +admin fehlt" matchers=$(r "${nodes[0]}" pvesh get /cluster/notifications/matchers --output-format json 2>/dev/null) grep -q '"name":"knaster-alles"' <<<"$matchers" && ok "Benachrichtigung: Matcher knaster-alles" || bad "Benachrichtigung: Matcher knaster-alles fehlt" users=$(r "${nodes[0]}" pveum user list --output-format json 2>/dev/null) for u in r.rapp@pve healer@pve tofu@pve monitoring@pve; do grep -q "\"userid\":\"$u\"" <<<"$users" && ok "Benutzer $u" || bad "Benutzer $u fehlt"; done tfa=$(r "${nodes[0]}" pvesh get /access/tfa/r.rapp@pve --output-format json 2>/dev/null) grep -q '"type":"totp"' <<<"$tfa" && ok "2FA (TOTP) für r.rapp@pve" || bad "2FA für r.rapp@pve fehlt" for n in "${nodes[@]}"; do echo "== $n" out=$(r "$n" 'echo "kernel=$(uname -r)"; echo "failed=$(systemctl --failed --no-legend | wc -l)"; echo "fw=$(pve-firewall status)"; echo "ntp=$(timedatectl show -p NTPSynchronized --value)"; echo "vmbr0=$(ip -br a show vmbr0 | awk "{print \$2}")"; echo "root=$(df --output=pcent / | tail -1 | tr -d " %")"; echo "zpool=$(zpool list -H -o health tank 2>/dev/null || echo none)"; echo "cert=$(openssl x509 -in /etc/pve/local/pve-ssl.pem -noout -checkend 2592000 >/dev/null && echo ok || echo laeuft-ab)"; echo "gpu=$(lspci -nnk -d 10de: 2>/dev/null | grep -c "in use: vfio-pci")"; echo "gpus=$(lspci -n -d 10de: 2>/dev/null | wc -l)"; echo "hang=$(dmesg 2>/dev/null | grep -ci "unit hang")"' 2>/dev/null) || { bad "nicht erreichbar per SSH"; continue; } v() { awk -F= -v k="$1" '$1==k {print $2}' <<<"$out"; } ok "erreichbar, Kernel $(v kernel)" [ "$(v failed)" = 0 ] && ok "keine fehlgeschlagenen Dienste" || bad "$(v failed) fehlgeschlagene Dienste (systemctl --failed)" [[ "$(v fw)" == *enabled/running* ]] && ok "Knoten-Firewall läuft" || bad "Knoten-Firewall: $(v fw)" [ "$(v ntp)" = yes ] && ok "Zeit per NTP synchron" || bad "Zeit nicht synchron" [ "$(v vmbr0)" = UP ] && ok "vmbr0 UP" || bad "vmbr0 nicht UP" [ "$(v root)" -lt 85 ] && ok "Systemplatte $(v root) % belegt" || bad "Systemplatte $(v root) % belegt" case "$(v zpool)" in none) ok "kein ZFS-Pool (erwartet auf pve46/pve61)";; ONLINE) ok "zpool tank ONLINE";; *) bad "zpool tank: $(v zpool)";; esac [ "$(v cert)" = ok ] && ok "Knotenzertifikat länger als 30 Tage gültig" || bad "Knotenzertifikat läuft in 30 Tagen ab" if [ "$(v gpus)" -gt 0 ]; then [ "$(v gpu)" = "$(v gpus)" ] && ok "alle $(v gpus) NVIDIA-Funktionen an vfio-pci" || bad "nur $(v gpu) von $(v gpus) NVIDIA-Funktionen an vfio-pci"; fi [ "$(v hang)" = 0 ] && ok "keine NIC-Hänger (e1000e/r8152)" || bad "$(v hang) NIC-Hänger im Kernel-Log" ip=$(ssh -G "$n" | awk '$1=="hostname"{print $2}') check "Weboberfläche https://$ip:8006 antwortet" bash -c "curl -sk -m 6 -o /dev/null -w '%{http_code}' https://$ip:8006/ | grep -q 200" done echo "== Firewall-Negativtest (aus GPU-VM 246 auf pve46, nicht in +admin)" neg=$(r pve46 'qm guest exec 246 --timeout 15 -- bash -c "curl -sk -m 4 -o /dev/null -w %{http_code} https://192.168.100.210:8006/ || echo blockiert"' 2>/dev/null) grep -q blockiert <<<"$neg" && ok "VM erreicht 8006 nicht" || bad "VM erreicht 8006 (Firewall wirkt nicht)" [ $fail = 0 ] && echo "ERGEBNIS: alles OK" || { echo "ERGEBNIS: Fehler, siehe oben"; exit 1; }