Fakten sammelt ein Skript, Prosa schreibt das Modell. stack-ist.sh prueft Ergebnisse statt Lebenszeichen und fand beim ersten Lauf sofort zwei echte Befunde. Abgeschaltet: 4 Crons + 4 Timer, davon einer nie gelaufen und einer 15 Naechte ohne Wirkung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
178 lines
7.4 KiB
Bash
178 lines
7.4 KiB
Bash
#!/usr/bin/env bash
|
|
# stack-ist.sh — der IST-Zustand der Box, in Fakten (21.08.2026).
|
|
#
|
|
# Wird von Hermes-Cron "stack-radar" ueber --script eingespeist: die Ausgabe hier
|
|
# landet im Prompt des Agenten, der daraus den Bericht schreibt.
|
|
#
|
|
# GRUNDSATZ: Dieses Skript prueft ERGEBNISSE, nicht Lebenszeichen.
|
|
# Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren:
|
|
# das Kritiker-Gate zeigte auf ein umbenanntes Modell (404), Lucys SOUL.md war
|
|
# blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
|
# "Dienst antwortet" haette das nie gefunden. Deshalb wird hier nachgesehen,
|
|
# ob Dinge STIMMEN — nicht ob sie laufen.
|
|
#
|
|
# Nie mit set -e: ein einzelner fehlschlagender Test darf den Bericht nicht
|
|
# abschneiden. Fehler werden gemeldet, nicht verschwiegen.
|
|
set -uo pipefail
|
|
|
|
MC2="${MC2_URL:-http://127.0.0.1:9001}"
|
|
SWAP="${SWAP_URL:-http://127.0.0.1:8080}"
|
|
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
|
OEFFENTLICH="${OEFFENTLICH_URL:-https://mc.tobisniceshomelab.ddnsfree.com}"
|
|
|
|
ok() { printf ' OK %s\n' "$*"; }
|
|
warn() { printf ' ACHTUNG %s\n' "$*"; }
|
|
bad() { printf ' ROT %s\n' "$*"; }
|
|
|
|
echo "IST-ZUSTAND DER BOX — $(date '+%d.%m.%Y %H:%M')"
|
|
echo
|
|
|
|
# ---------------------------------------------------------------- Dienste ---
|
|
echo "DIENSTE"
|
|
# systemd stellt gescheiterten Units ein "●" voran — das muss weg, sonst meldet
|
|
# der Bericht lauter Aufzaehlungspunkte als kaputte Dienste.
|
|
# EGAL: Units, die dauerhaft scheitern und niemanden stoeren. Ein Bericht, der
|
|
# jeden Tag denselben harmlosen Fehler zeigt, erzieht zum Wegsehen.
|
|
EGAL="at-spi-dbus-bus.service"
|
|
fehl=$(systemctl --user list-units --type=service --state=failed --no-legend --no-pager 2>/dev/null \
|
|
| sed 's/^[^a-zA-Z]*//' | awk '{print $1}')
|
|
gefunden=0
|
|
for d in $fehl; do
|
|
case " $EGAL " in *" $d "*) continue ;; esac
|
|
bad "Dienst gescheitert: $d"; gefunden=1
|
|
done
|
|
[ "$gefunden" = "0" ] && ok "kein Dienst im Fehlerzustand"
|
|
|
|
for d in mc2-gateway mission-control-2 hermes-gateway voice-service; do
|
|
systemctl --user is-active --quiet "$d.service" 2>/dev/null \
|
|
&& ok "$d laeuft" || bad "$d laeuft NICHT"
|
|
done
|
|
systemctl is-active --quiet llama-swap.service 2>/dev/null \
|
|
&& ok "llama-swap laeuft" || bad "llama-swap laeuft NICHT"
|
|
echo
|
|
|
|
# ------------------------------------------------------------------ Timer ---
|
|
# Nicht "ist der Timer aktiv", sondern "ist der letzte Lauf gutgegangen".
|
|
echo "TIMER — letzter Lauf"
|
|
for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk '{print $NF}'); do
|
|
dienst="${t%.service}.service"
|
|
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
|
code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null)
|
|
wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3)
|
|
if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen"
|
|
elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann"
|
|
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
|
done
|
|
echo
|
|
|
|
# --------------------------------------------------------- Modell-Aliase ---
|
|
# Der Fund vom 19./20.08.: Skripte und Configs nennen Modelle beim Eigennamen,
|
|
# das Modell wird umbenannt, und alles laeuft still in 404. Hier wird geprueft,
|
|
# ob jeder Alias, den irgendwer benutzt, heute noch antwortet.
|
|
echo "MODELL-ALIASE (Rollen muessen aufloesen)"
|
|
liste=$(curl -s -m 10 "$MC2/v1/models" 2>/dev/null \
|
|
| python3 -c 'import sys,json; print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null)
|
|
if [ -z "$liste" ]; then bad "Modell-Liste nicht abrufbar ueber $MC2/v1"
|
|
else
|
|
ok "verfuegbar: $liste"
|
|
for rolle in hermes fast heavy; do
|
|
case " $liste " in
|
|
*" $rolle "*) ok "Rolle '$rolle' loest auf" ;;
|
|
*) bad "Rolle '$rolle' FEHLT — Skripte, die sie nennen, laufen in 404" ;;
|
|
esac
|
|
done
|
|
fi
|
|
echo
|
|
|
|
# ------------------------------------------------------------- Erreichbar ---
|
|
echo "ERREICHBARKEIT"
|
|
for paar in "MC2:$MC2/api/health" "llama-swap:$SWAP/v1/models" "Gitea:$GITEA/api/v1/version"; do
|
|
name="${paar%%:*}"; url="${paar#*:}"
|
|
code=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
|
[ "$code" = "200" ] && ok "$name antwortet (200)" || bad "$name antwortet mit '$code'"
|
|
done
|
|
echo
|
|
|
|
# ------------------------------------------------ Offen im Internet? --------
|
|
# Der Fund vom 21.08.: MC2 hing ueber den Nginx Proxy Manager ungeschuetzt im
|
|
# Netz. ufw sah nichts davon, weil der Proxy im LAN steht. Also von aussen
|
|
# nachsehen — und zwar auf einem Endpunkt, der ECHTE Daten liefert.
|
|
echo "OEFFENTLICHE ERREICHBARKEIT"
|
|
code=$(curl -s -m 15 -o /tmp/.ist-oeff -w '%{http_code}' "$OEFFENTLICH/api/system/status" 2>/dev/null)
|
|
if [ "$code" = "200" ] && head -c1 /tmp/.ist-oeff 2>/dev/null | grep -q '{'; then
|
|
bad "MC2 liefert Systemdaten OHNE Anmeldung ins Internet ($OEFFENTLICH)"
|
|
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
|
ok "oeffentlicher Zugang verlangt Anmeldung ($code)"
|
|
elif [ "$code" = "000" ]; then
|
|
ok "von aussen nicht erreichbar"
|
|
else
|
|
warn "oeffentlicher Zugang antwortet mit '$code' — nachsehen"
|
|
fi
|
|
rm -f /tmp/.ist-oeff
|
|
echo
|
|
|
|
# ----------------------------------------------------------------- Ports ---
|
|
echo "OFFENE PORTS (ufw)"
|
|
sudo -n ufw status 2>/dev/null | grep ALLOW | sed 's/^/ /' || warn "ufw-Status nicht lesbar"
|
|
echo
|
|
|
|
# --------------------------------------------------------------- CI-Ampel ---
|
|
echo "CI-AMPEL (Gitea)"
|
|
T=$(tr -d '[:space:]' < "$HOME/.config/gitea/create-token" 2>/dev/null)
|
|
if [ -z "$T" ]; then warn "kein Gitea-Token — CI nicht pruefbar"
|
|
else
|
|
curl -s -m 20 -H "Authorization: token $T" "$GITEA/api/v1/user/repos?limit=100" 2>/dev/null \
|
|
| python3 -c '
|
|
import sys, json
|
|
try:
|
|
repos = json.load(sys.stdin)
|
|
except Exception as e:
|
|
print(" ACHTUNG Repo-Liste nicht lesbar:", e); sys.exit(0)
|
|
print(f" OK {len(repos)} Repos in Gitea")
|
|
' 2>/dev/null || warn "Repo-Liste nicht lesbar"
|
|
fi
|
|
echo
|
|
|
|
# ------------------------------------------------------------- Sicherung ---
|
|
echo "SICHERUNGEN"
|
|
# Pfad aus backup.sh: DEST_DIR="$MODELS_DIR/mc2-backups"
|
|
SICHER="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
|
|
neuestes=$(find "$SICHER" -maxdepth 2 -type f -newermt '-2 days' 2>/dev/null | head -1)
|
|
anzahl=$(find "$SICHER" -maxdepth 2 -type f 2>/dev/null | wc -l)
|
|
if [ -n "$neuestes" ]; then
|
|
ok "Sicherung juenger als 2 Tage vorhanden ($anzahl Staende in $SICHER)"
|
|
elif [ "$anzahl" -gt 0 ]; then
|
|
bad "juengste Sicherung ist AELTER als 2 Tage ($anzahl Staende in $SICHER)"
|
|
else
|
|
bad "gar keine Sicherung in $SICHER"
|
|
fi
|
|
echo
|
|
|
|
# ------------------------------------------------------------------ Platz ---
|
|
echo "PLATZ UND LAST"
|
|
# Nur eindeutige Dateisysteme: / und /srv liegen hier auf demselben Geraet,
|
|
# doppelt gemeldet las sich das wie ein Befund.
|
|
df -h --output=source,size,used,avail,pcent,target / /srv 2>/dev/null \
|
|
| awk '!gesehen[$1]++ {printf " %s\n", $0}'
|
|
echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')"
|
|
echo
|
|
|
|
# ------------------------------------------------------------- Warm/Kalt ---
|
|
echo "MODELLE IM SPEICHER"
|
|
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
|
| python3 -c '
|
|
import sys, json
|
|
try:
|
|
d = json.load(sys.stdin)
|
|
except Exception:
|
|
print(" ACHTUNG llama-swap-Status nicht lesbar"); sys.exit(0)
|
|
lauf = d.get("running", [])
|
|
if not lauf:
|
|
print(" OK kein Modell geladen (alles kalt)")
|
|
for m in lauf:
|
|
print(" OK " + str(m.get("model")) + " - " + str(m.get("state")))
|
|
'
|
|
|
|
echo
|
|
echo "ENDE IST-ZUSTAND"
|