Ampel / ampel (push) Successful in 26s
Beim ersten Einschalten von mc2-autoupdate.timer holte systemd (Persistent=true) den seit August verpassten Sonntagslauf sofort nach. Der Lauf spielte nichts ein, startete die Box wegen des wartenden Kernel-Updates aber am Donnerstag um 14:51 neu. Laeufe ausserhalb des Fensters melden einen noetigen Neustart jetzt nur an; von Hand erzwingen mit MC_AUTOUPDATE_REBOOT_JETZT=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
257 lines
14 KiB
Bash
257 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
# Wöchentlicher Auto-Update-Lauf der Box (Autonomie E2). Läuft als User via
|
|
# mc2-autoupdate.timer (So 04:30, nach dem 03:30-Backup) oder manuell.
|
|
#
|
|
# Prinzip „Haushaltsgerät": Fremd-Software (Router → Engine → Hermes) updatet sich
|
|
# selbst über die BESTEHENDEN Pfade (update-swap.sh / update-engine.sh / hermes-update-Job).
|
|
# Jede Ebene: Update-Check → Update → Postcheck. Rot ⇒ Rollback + SELBST-PINNING im
|
|
# Pin-Register + Telegram-Meldung. Grün ⇒ Telegram „eingespielt X→Y".
|
|
# Gepinnte Ebenen werden übersprungen, bis der Pin manuell gelöst wird.
|
|
#
|
|
# OS-Ebene: bewusst NICHT hier — unattended-upgrades (nur Security) wird in der
|
|
# einmaligen sudo-Session eingerichtet (siehe deploy/sudoers-mc2-autonomie).
|
|
# Modelle: NIE automatisch (User entscheidet nach Bench, Etappe E5).
|
|
#
|
|
# Router/Engine laufen als root: braucht die NOPASSWD-Regel aus
|
|
# deploy/sudoers-mc2-autonomie. Fehlt sie, wird die Ebene übersprungen + gemeldet.
|
|
set -uo pipefail # bewusst KEIN -e: jede Ebene wird kontrolliert abgearbeitet
|
|
|
|
API="${MC_API:-http://127.0.0.1:9001}"
|
|
PINS="${MC_PINS_FILE:-/srv/models/mc2-pins.json}"
|
|
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
HERMES_DIR="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
|
BACKUP_DIR="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
|
|
JOB_TIMEOUT="${MC_AUTOUPDATE_JOB_TIMEOUT:-1200}" # Sekunden für den Hermes-Job
|
|
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
|
|
|
say(){ echo "[autoupdate] $*"; }
|
|
notify(){ bash "$SRC_DIR/notify.sh" -s "[Box-Update]" "$1" || true; }
|
|
# Dringend = geht auch nachts sofort raus (sonst sammelt notify.sh bis zur Morgenmeldung um 07:00).
|
|
notify_dringend(){ bash "$SRC_DIR/notify.sh" -d -s "[Box-Update]" "$1" || true; }
|
|
|
|
SUMMARY=() # eine Zeile je Ebene für die Abschluss-Meldung
|
|
|
|
# ── Pin-Register ─────────────────────────────────────────────────────────────
|
|
pins_init(){ [ -f "$PINS" ] || echo '{}' > "$PINS"; }
|
|
is_pinned(){ jq -e --arg k "$1" '.[$k].pinned == true' "$PINS" >/dev/null 2>&1; }
|
|
pin_info(){ jq -r --arg k "$1" '.[$k] | "\(.version // "?") seit \(.datum // "?") (\(.grund // "?"))"' "$PINS" 2>/dev/null; }
|
|
set_pin(){ # $1 komponente $2 version(known-good) $3 grund
|
|
local tmp; tmp="$(mktemp)"
|
|
jq --arg k "$1" --arg v "$2" --arg g "$3" --arg d "$(date +%F)" \
|
|
'.[$k] = {pinned: true, version: $v, grund: $g, datum: $d}' "$PINS" > "$tmp" && mv "$tmp" "$PINS"
|
|
}
|
|
|
|
# ── Helfer ───────────────────────────────────────────────────────────────────
|
|
details(){ curl -sf --max-time 60 "$API/api/maintenance/update-details?kind=$1"; }
|
|
|
|
# „darf ich das PASSWORTLOS?" — sudo -n -l <cmd> reicht nicht (Exit 0 auch wenn nur
|
|
# MIT Passwort erlaubt); deshalb explizit die NOPASSWD-Zeilen der Whitelist greppen.
|
|
sudo_ok(){ sudo -n -l 2>/dev/null | grep -F "NOPASSWD" | grep -qF "$1"; }
|
|
|
|
# Router und Engine teilen denselben Ablauf: root-Skript mit eingebautem
|
|
# Postcheck+Rollback (Exit 0 grün / 1 zurückgerollt / 2 kaputt).
|
|
run_root_update(){ # $1 komponente(swap|engine) $2 skript $3 anzeigename $4 installed $5 latest
|
|
local comp="$1" script="$2" name="$3" installed="$4" latest="$5" rc
|
|
say "$name: Update $installed → $latest wird eingespielt…"
|
|
sudo -n /usr/bin/bash "$SRC_DIR/$script"; rc=$?
|
|
case "$rc" in
|
|
0) SUMMARY+=("$name: $installed → $latest eingespielt, Stack-Check grün.")
|
|
notify "$name aktualisiert: $installed → $latest. Stack-Check grün, alles läuft." ;;
|
|
1) set_pin "$comp" "$installed" "Update auf $latest zerschoss den Stack-Check; Rollback grün"
|
|
SUMMARY+=("$name: $latest FEHLGESCHLAGEN → zurückgerollt auf $installed + GEPINNT.")
|
|
notify "$name-Update auf $latest fehlgeschlagen (Stack-Check rot). Automatisch zurückgerollt auf $installed — läuft wieder. Ebene ist jetzt GEPINNT, künftige Läufe überspringen sie." ;;
|
|
*) SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
|
|
notify_dringend "KRITISCH: $name-Update auf $latest UND Rollback fehlgeschlagen — Stack möglicherweise kaputt. Bitte melden, ich brauche Hilfe (Backup liegt bereit, restore.sh existiert)." ;;
|
|
esac
|
|
}
|
|
|
|
check_layer_root(){ # $1 komponente $2 skript $3 anzeigename $4 details-kind
|
|
local comp="$1" script="$2" name="$3" kind="$4" det installed latest
|
|
if is_pinned "$comp"; then
|
|
say "$name: GEPINNT ($(pin_info "$comp")) — übersprungen."
|
|
SUMMARY+=("$name: gepinnt, übersprungen.")
|
|
return
|
|
fi
|
|
det="$(details "$kind")" || { say "$name: Update-Check nicht erreichbar."; SUMMARY+=("$name: Check fehlgeschlagen."); return; }
|
|
installed="$(jq -r '.installed_build // empty' <<<"$det")"
|
|
latest="$(jq -r '.latest_build // empty' <<<"$det")"
|
|
if [ -z "$installed" ] || [ -z "$latest" ]; then
|
|
say "$name: Versionen nicht ermittelbar (installed='$installed' latest='$latest')."
|
|
SUMMARY+=("$name: Versions-Check unklar, nichts getan.")
|
|
return
|
|
fi
|
|
if [ "$latest" -le "$installed" ] 2>/dev/null; then
|
|
say "$name: aktuell ($installed)."
|
|
SUMMARY+=("$name: aktuell ($installed).")
|
|
return
|
|
fi
|
|
if ! sudo_ok "$script"; then
|
|
say "$name: Update $installed → $latest verfügbar, aber sudo-Freischaltung fehlt."
|
|
SUMMARY+=("$name: Update $installed → $latest wartet — sudo-Freischaltung fehlt (einmalig deploy/sudoers-mc2-autonomie installieren).")
|
|
return
|
|
fi
|
|
run_root_update "$comp" "$script" "$name" "$installed" "$latest"
|
|
}
|
|
|
|
# ── Hermes-Agent (User-Space, via MC2-Job + Polling; Rollback machen WIR) ────
|
|
check_hermes(){
|
|
local name="Hermes-Agent" det behind old_head resp job_id state t
|
|
if is_pinned hermes; then
|
|
say "$name: GEPINNT ($(pin_info hermes)) — übersprungen."
|
|
SUMMARY+=("$name: gepinnt, übersprungen.")
|
|
return
|
|
fi
|
|
det="$(details hermes)" || { SUMMARY+=("$name: Check fehlgeschlagen."); return; }
|
|
behind="$(jq -r '.behind // 0' <<<"$det")"
|
|
if [ "${behind:-0}" -eq 0 ] 2>/dev/null; then
|
|
say "$name: aktuell."
|
|
SUMMARY+=("$name: aktuell.")
|
|
return
|
|
fi
|
|
old_head="$(git -C "$HERMES_DIR" rev-parse HEAD 2>/dev/null)"
|
|
say "$name: $behind Commits hinterher — Update-Job startet (Backup→update→doctor→Restart→Gehirn-Check)…"
|
|
resp="$(curl -sf --max-time 30 -X POST "$API/api/maintenance/hermes-update")" || { SUMMARY+=("$name: Job-Start fehlgeschlagen."); return; }
|
|
job_id="$(jq -r '.job_id // empty' <<<"$resp")"
|
|
if [ -z "$job_id" ]; then
|
|
say "$name: kein Job gestartet: $resp"
|
|
SUMMARY+=("$name: Job-Start abgelehnt ($(jq -r '.error // .detail // "unbekannt"' <<<"$resp" 2>/dev/null)).")
|
|
return
|
|
fi
|
|
t=0
|
|
while [ "$t" -lt "$JOB_TIMEOUT" ]; do
|
|
state="$(curl -sf --max-time 15 "$API/api/jobs" | jq -r --arg id "$job_id" '.jobs[] | select(.id==$id) | .state' 2>/dev/null)"
|
|
case "$state" in done|failed|canceled) break ;; esac
|
|
sleep 10; t=$((t + 10))
|
|
done
|
|
if [ "$state" = "done" ]; then
|
|
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
|
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Patches, Tools, Voice) grün — alles läuft."
|
|
return
|
|
fi
|
|
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
|
# Autonomie 0g. Nur Config, reversibel, hart gegatet; scheitert es, fällt es sauber in den Rollback.
|
|
say "$name: Job endete '$state' — versuche Selbstreparatur (Config) vor dem Rollback…"
|
|
local sr_out sr_rc sugg=""
|
|
sr_out="$(bash "$SRC_DIR/self-repair.sh" 2>&1)"; sr_rc=$?
|
|
echo "$sr_out"
|
|
if [ "$sr_rc" -eq 0 ]; then
|
|
SUMMARY+=("$name: Config-Bruch nach Update SELBST repariert — neue Version läuft (kein Rollback).")
|
|
return
|
|
fi
|
|
sugg="$(echo "$sr_out" | sed -n 's/^SELFREPAIR_SUGGESTION=//p' | tail -1)"
|
|
|
|
# Rollback: Code auf alten Stand, Config aus dem frischen Backup, Neustart, Gehirn-Check.
|
|
say "$name: Selbstreparatur griff nicht — ROLLBACK auf $old_head…"
|
|
if [ -n "$old_head" ]; then
|
|
git -C "$HERMES_DIR" reset --hard "$old_head" >/dev/null 2>&1
|
|
local bak stage
|
|
bak="$(ls -1t "$BACKUP_DIR"/mc2-state-*.tar.gz 2>/dev/null | head -1)"
|
|
if [ -n "$bak" ]; then
|
|
stage="$(mktemp -d)"
|
|
tar -xzf "$bak" -C "$stage" ./hermes 2>/dev/null || tar -xzf "$bak" -C "$stage" hermes 2>/dev/null
|
|
[ -f "$stage/hermes/config.yaml" ] && cp -a "$stage/hermes/config.yaml" "$HOME/.hermes/config.yaml"
|
|
[ -f "$stage/hermes/.env" ] && cp -a "$stage/hermes/.env" "$HOME/.hermes/.env"
|
|
rm -rf "$stage"
|
|
fi
|
|
systemctl --user restart hermes-gateway; sleep 6
|
|
if bash "$SRC_DIR/hermes-postcheck.sh" >/dev/null 2>&1; then
|
|
set_pin hermes "$old_head" "Update ($behind Commits) riss den Gehirn-Check; Rollback grün"
|
|
SUMMARY+=("$name: Update FEHLGESCHLAGEN → zurückgerollt + GEPINNT auf ${old_head:0:9}.")
|
|
notify "Hermes-Update fehlgeschlagen (Gehirn-Check rot). Selbstreparatur der Config griff nicht, deshalb automatisch zurückgerollt auf ${old_head:0:9} — läuft wieder. Hermes ist jetzt GEPINNT.${sugg:+
|
|
|
|
Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}"
|
|
return
|
|
fi
|
|
fi
|
|
SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
|
|
notify_dringend "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)"
|
|
}
|
|
|
|
# ── Neustart, wenn das OS einen verlangt ─────────────────────────────────────
|
|
# Kernel- und libc-Updates werden erst nach einem Neustart wirksam; bis dahin
|
|
# laeuft die Box weiter auf dem alten Kernel. Das faellt niemandem auf — am
|
|
# 27.08.2026 lag die Box 7 Wochen mit drei ungenutzten Kerneln da.
|
|
#
|
|
# Der Neustart haengt bewusst am WOECHENTLICHEN Lauf (So 04:30) und nicht an
|
|
# einem eigenen Timer: ein Ort fuer alle Automatik (Lehre vom 20.08.).
|
|
# Er passiert NUR, wenn Ubuntu ihn selbst anfordert (/var/run/reboot-required).
|
|
#
|
|
# WICHTIG: Die Linger-Pruefung ist die entscheidende Zeile hier. Ohne Linger=yes
|
|
# startet systemd die User-Dienste nach einem Neustart NICHT — die Box kaeme ohne
|
|
# Steuerpult, Gateway und Waechter hoch, und niemand koennte sie aus der Ferne
|
|
# wieder anschalten. Im Zweifel lieber nicht neustarten.
|
|
#
|
|
# Abschalten: MC_AUTOUPDATE_REBOOT=0 in der Umgebung des Cron-Jobs.
|
|
# Ausserhalb des Wartungsfensters erzwingen (von Hand): MC_AUTOUPDATE_REBOOT_JETZT=1.
|
|
reboot_wenn_noetig(){
|
|
[ "${MC_AUTOUPDATE_REBOOT:-1}" = "1" ] || { say "Neustart per Env abgeschaltet."; return 0; }
|
|
[ -f /var/run/reboot-required ] || { say "Kein Neustart noetig."; return 0; }
|
|
|
|
local pakete; pakete="$(sort -u /var/run/reboot-required.pkgs 2>/dev/null | tr "
|
|
" " ")"
|
|
|
|
# Sicherung 0 (24.09.2026): neu gestartet wird nur im Wartungsfenster, sonntags 04:00-06:59.
|
|
# Anlass: Ein nachgeholter Lauf (Persistent=true beim Einschalten des Timers) startete die Box
|
|
# an einem Donnerstagnachmittag neu. Laeufe ausserhalb des Fensters melden den Neustart nur an.
|
|
local tag stunde
|
|
tag="$(date +%u)"; stunde="$((10#$(date +%H)))"
|
|
if [ "${MC_AUTOUPDATE_REBOOT_JETZT:-0}" != "1" ] && { [ "$tag" != "7" ] || [ "$stunde" -lt 4 ] || [ "$stunde" -ge 7 ]; }; then
|
|
notify "Neustart steht an (${pakete:-Kernel/libc}), passiert aber erst im Wartungsfenster am Sonntag frueh."
|
|
return 0
|
|
fi
|
|
|
|
# Sicherung 1: kommen die Dienste ohne Login von selbst wieder hoch?
|
|
# $USER ist in systemd-/Cron-Umgebungen NICHT gesetzt — mit set -u waere das ein
|
|
# sofortiger Abbruch des ganzen Update-Laufs. id -un funktioniert immer.
|
|
local nutzer; nutzer="$(id -un)"
|
|
if [ "$(loginctl show-user "$nutzer" -p Linger --value 2>/dev/null)" != "yes" ]; then
|
|
notify "Neustart NICHT ausgefuehrt: die Box laeuft ohne linger — nach einem Neustart wuerden Steuerpult, Gateway und Waechter nicht von selbst starten. Erst 'loginctl enable-linger $nutzer' setzen, dann von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
|
|
return 0
|
|
fi
|
|
|
|
# Sicherung 2: kein halbfertiges Update mitten im Neustart abwuergen.
|
|
local laufend
|
|
laufend="$(curl -sf --max-time 20 "$API/api/jobs" | jq -r '[.jobs[]? | select(.state=="running")] | length' 2>/dev/null || echo 0)"
|
|
if [ "${laufend:-0}" -gt 0 ]; then
|
|
notify "Neustart verschoben: es laufen noch $laufend Job(s). Ausstehend: ${pakete:-Kernel/libc}. Naechster Versuch beim Lauf in einer Woche."
|
|
return 0
|
|
fi
|
|
|
|
# Sicherung 3: die Dienste muessen fuer den Autostart vorgemerkt sein.
|
|
local fehlend=""
|
|
for dienst in mission-control-2 mc2-gateway mc2-steward; do
|
|
systemctl --user is-enabled "$dienst" >/dev/null 2>&1 || fehlend="$fehlend $dienst"
|
|
done
|
|
if [ -n "$fehlend" ]; then
|
|
notify "Neustart NICHT ausgefuehrt: diese Dienste sind nicht fuer den Autostart eingetragen —$fehlend. Erst 'systemctl --user enable' nachholen. Ausstehend: ${pakete:-Kernel/libc}"
|
|
return 0
|
|
fi
|
|
|
|
notify "Die Box startet jetzt neu — das OS verlangt es nach dem Update (${pakete:-Kernel/libc}). Sie ist ein paar Minuten weg und meldet sich, sobald alles wieder laeuft. Die Modelle muessen danach neu geladen werden, die erste Anfrage dauert also laenger."
|
|
say "Neustart wird ausgeloest (ausstehend: ${pakete:-Kernel/libc})."
|
|
sleep 20 # der Meldung Zeit lassen, rauszugehen
|
|
sudo -n systemctl reboot || {
|
|
notify "Neustart FEHLGESCHLAGEN: 'sudo systemctl reboot' wurde abgelehnt. Bitte von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
|
|
}
|
|
}
|
|
|
|
# ── Lauf ─────────────────────────────────────────────────────────────────────
|
|
say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
|
|
pins_init
|
|
if ! curl -sf --max-time 20 "$API/api/health" >/dev/null; then
|
|
notify "Auto-Update abgebrochen: MC2-API ($API) nicht erreichbar — bitte Box prüfen."
|
|
exit 1
|
|
fi
|
|
|
|
check_layer_root swap update-swap.sh "Router (llama-swap)" swap
|
|
check_layer_root engine update-engine.sh "Engine (llama.cpp)" engine
|
|
check_hermes
|
|
|
|
notify "Commander, die Wochenpflege der Box ist durch — kurz für dich:
|
|
$(printf '• %s\n' "${SUMMARY[@]}")"
|
|
say "Fertig."
|
|
|
|
# Ganz zuletzt: der Neustart wuerde die Abschlussmeldung oben sonst verschlucken.
|
|
# Die Funktion meldet in jedem Fall selbst, ob sie neustartet oder warum nicht.
|
|
reboot_wenn_noetig
|