216 lines
7.9 KiB
Bash
216 lines
7.9 KiB
Bash
# shellcheck shell=sh
|
|
#
|
|
# Melden und Nachreichen. Diese Datei kennt als Einzige den Weg zu CluPilot.
|
|
#
|
|
# Zwei Regeln, aus denen sich alles andere ergibt:
|
|
#
|
|
# 1. `report` schreibt IMMER zuerst lokal, dann erst der Sendeversuch. Vor
|
|
# `wireguard_joined` gibt es keinen Weg zu CluPilot (Spec §5) — die Zeilen
|
|
# sammeln sich und werden von `flush_reports` nachgereicht.
|
|
# 2. Ein gescheiterter Sendeversuch ist NORMAL und darf den Lauf nie abbrechen.
|
|
# Das Skript läuft unter `set -e`; jede Netzoperation hier ist deshalb
|
|
# ausdrücklich abgefangen.
|
|
#
|
|
# Der Zeitstempel ist der des Ereignisses, nicht der der Ankunft. Sonst sieht
|
|
# eine zwanzigminütige Installation in der Konsole aus wie eine Sekunde, und
|
|
# niemand kann erkennen, welcher Abschnitt lange gedauert hat.
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
# Der Umschlag, gegen den die Plattform schreibt
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# Die Abschnittsschlüssel sind laut Plan die einzige Absprache zwischen den zwei
|
|
# Plänen. Der Umschlag darum war nirgends festgelegt — er wird hier festgelegt,
|
|
# damit die Plattformseite EINE Stelle zum Nachlesen hat:
|
|
#
|
|
# POST {api}/host/progress
|
|
# Content-Type: application/json
|
|
#
|
|
# {"code": "<einmal-code>", "events": [
|
|
# {"section": "rescue_checked", "state": "done",
|
|
# "message": "...", "occurred_at": "2026-07-30T18:04:11Z"}
|
|
# ]}
|
|
#
|
|
# `state` ist `done` oder `failed`. Einen Zustand für „läuft gerade" gibt es
|
|
# absichtlich nicht: die Konsole leitet „offen" aus dem Fehlen einer Meldung ab
|
|
# (Spec §7), und ein dritter Zustand wäre eine zweite Wahrheit über dieselbe
|
|
# Sache.
|
|
#
|
|
# `message` ist immer vorhanden, notfalls leer — ein Feld, das mal da ist und
|
|
# mal nicht, ist die teurere Variante.
|
|
|
|
# Überschreibbar, damit sich die Melde-Mechanik ohne root und ohne echte
|
|
# Maschine durchspielen lässt. Im Betrieb setzt niemand die Variable: der
|
|
# Adminbereich schreibt sie nicht in die kopierte Zeile, und das Skript läuft
|
|
# ohnehin als root.
|
|
CLUPILOT_STATE_DIR="${CLUPILOT_STATE_DIR:-/var/lib/clupilot}"
|
|
CLUPILOT_PROGRESS_LOG="${CLUPILOT_STATE_DIR}/progress.jsonl"
|
|
CLUPILOT_PROGRESS_SENT="${CLUPILOT_STATE_DIR}/progress.sent"
|
|
|
|
# Kurz gehalten, weil das hier im Regelfall gegen eine Adresse läuft, die es
|
|
# noch nicht gibt. Ein langer Zeitablauf je Abschnitt summiert sich zu Minuten,
|
|
# in denen nichts passiert und niemand sieht warum.
|
|
CLUPILOT_HTTP_CONNECT_TIMEOUT=5
|
|
CLUPILOT_HTTP_MAX_TIME=15
|
|
|
|
# Legt das Zustandsverzeichnis an. 0700, weil hier später auch die
|
|
# Aufrufargumente liegen und der WireGuard-Schlüssel darin steht.
|
|
init_reporting() {
|
|
mkdir -p "$CLUPILOT_STATE_DIR"
|
|
chmod 700 "$CLUPILOT_STATE_DIR"
|
|
[ -f "$CLUPILOT_PROGRESS_LOG" ] || : > "$CLUPILOT_PROGRESS_LOG"
|
|
[ -f "$CLUPILOT_PROGRESS_SENT" ] || echo 0 > "$CLUPILOT_PROGRESS_SENT"
|
|
}
|
|
|
|
# Macht aus beliebigem Text den Rumpf eines JSON-Strings.
|
|
#
|
|
# Der Text kommt oft aus der Ausgabe eines fehlgeschlagenen Befehls, ist also
|
|
# nicht unter unserer Kontrolle. Reihenfolge zählt: der Backslash zuerst, sonst
|
|
# verdoppelt der Lauf danach die gerade gesetzten Fluchtzeichen wieder mit.
|
|
# Zeilenumbrüche und Tabulatoren werden zu Leerzeichen — eine Meldung ist eine
|
|
# Zeile, und eine eingebettete Zeile zerlegte die JSONL-Datei.
|
|
json_escape() {
|
|
printf '%s' "$1" \
|
|
| tr '\n\r\t' ' ' \
|
|
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
}
|
|
|
|
# Der einzige Zeitstempel-Erzeuger im ganzen Skript. UTC mit `Z`, weil das
|
|
# Skript auf Maschinen mit jeder denkbaren Zonen-Einstellung läuft und die
|
|
# Konsole nach R19 ohnehin in UTC speichert und erst bei der Anzeige umrechnet.
|
|
now_iso() {
|
|
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
}
|
|
|
|
# report <section> <state> [message]
|
|
#
|
|
# Gibt immer 0 zurück. Ein Melden, das den Lauf abbricht, wäre die Diagnose, die
|
|
# den Patienten umbringt.
|
|
report() {
|
|
_section="$1"
|
|
_state="$2"
|
|
_message="${3:-}"
|
|
|
|
printf '{"section":"%s","state":"%s","message":"%s","occurred_at":"%s"}\n' \
|
|
"$(json_escape "$_section")" \
|
|
"$(json_escape "$_state")" \
|
|
"$(json_escape "$_message")" \
|
|
"$(now_iso)" \
|
|
>> "$CLUPILOT_PROGRESS_LOG"
|
|
|
|
log "[${_state}] ${_section}${_message:+ — ${_message}}"
|
|
|
|
flush_reports
|
|
return 0
|
|
}
|
|
|
|
# Reicht alles nach, was noch nicht bestätigt gesendet wurde.
|
|
#
|
|
# Der Stand steht als Zeilenzahl in einer eigenen Datei, statt die Zeilen in der
|
|
# Protokolldatei umzuschreiben: `progress.jsonl` ist damit reines Anhängen und
|
|
# übersteht einen Stromausfall mitten im Schreiben ohne Sonderbehandlung.
|
|
#
|
|
# Gibt immer 0 zurück.
|
|
flush_reports() {
|
|
[ -f "$CLUPILOT_PROGRESS_LOG" ] || return 0
|
|
|
|
_sent="$(cat "$CLUPILOT_PROGRESS_SENT" 2>/dev/null || echo 0)"
|
|
_total="$(wc -l < "$CLUPILOT_PROGRESS_LOG")"
|
|
_sent="$(printf '%s' "$_sent" | tr -cd '0-9')"
|
|
[ -n "$_sent" ] || _sent=0
|
|
|
|
[ "$_total" -gt "$_sent" ] || return 0
|
|
|
|
_pending="$(sed -n "$((_sent + 1)),\$p" "$CLUPILOT_PROGRESS_LOG")"
|
|
[ -n "$_pending" ] || return 0
|
|
|
|
_events="$(printf '%s' "$_pending" | tr '\n' ',' | sed -e 's/,$//')"
|
|
_body="$(printf '{"code":"%s","events":[%s]}' "$(json_escape "$CLUPILOT_CODE")" "$_events")"
|
|
|
|
if post_json "${CLUPILOT_API}/host/progress" "$_body"; then
|
|
# Erst nach der bestätigten Antwort. Andersherum ginge genau der
|
|
# Abschnitt verloren, dessen Zustellung gescheitert ist.
|
|
echo "$_total" > "$CLUPILOT_PROGRESS_SENT"
|
|
log "nachgereicht: $((_total - _sent)) Meldung(en)"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# section_done <section> — 0, wenn dieser Abschnitt schon einmal `done` gemeldet
|
|
# hat.
|
|
#
|
|
# Die Fortschrittsdatei IST der Zustand. Es gibt keine zweite Buchführung
|
|
# darüber, was schon lief — eine zweite wäre eine zweite Wahrheit, und nach dem
|
|
# Neustart in Task 3 wäre genau sie diejenige, die fehlt.
|
|
#
|
|
# Bewusst gegen den festen Rumpf geprüft, den `report` schreibt, statt gegen ein
|
|
# lose passendes Muster: `grep rescue_checked` fände auch eine Meldung, in deren
|
|
# `message` der Name zufällig vorkommt.
|
|
section_done() {
|
|
[ -f "$CLUPILOT_PROGRESS_LOG" ] || return 1
|
|
|
|
grep -q "^{\"section\":\"$1\",\"state\":\"done\"," "$CLUPILOT_PROGRESS_LOG"
|
|
}
|
|
|
|
# run_section <section> <funktion> — führt einen Abschnitt aus, wenn er noch
|
|
# nicht erledigt ist, und meldet sein Ergebnis.
|
|
#
|
|
# Das `if` statt `&&` ist Absicht: unter `set -e` bricht ein `&&`-Ausdruck den
|
|
# Lauf ab, bevor `report ... failed` je liefe — und dann stünde in der Konsole
|
|
# nichts, obwohl genau dieser Fall der ist, für den sie gebaut wurde.
|
|
run_section() {
|
|
_key="$1"
|
|
_fn="$2"
|
|
|
|
if section_done "$_key"; then
|
|
log "übersprungen: ${_key} (schon erledigt)"
|
|
return 0
|
|
fi
|
|
|
|
if "$_fn"; then
|
|
report "$_key" done "${CLUPILOT_SECTION_NOTE:-}"
|
|
CLUPILOT_SECTION_NOTE=''
|
|
return 0
|
|
fi
|
|
|
|
report "$_key" failed "${CLUPILOT_SECTION_NOTE:-abgebrochen in ${_key}}"
|
|
CLUPILOT_SECTION_NOTE=''
|
|
return 1
|
|
}
|
|
|
|
# post_json <url> <body> — 0 bei 2xx, sonst 1. Schreibt nichts nach stdout.
|
|
#
|
|
# Der Rückfall auf wget ist kein Luxus: Rettungssysteme sind unterschiedlich
|
|
# bestückt, und genau hier ist noch keine Gelegenheit gewesen, etwas
|
|
# nachzuinstallieren.
|
|
post_json() {
|
|
_url="$1"
|
|
_payload="$2"
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
# `-f` lässt curl bei 4xx/5xx mit Fehler enden, statt die Fehlerseite
|
|
# als Erfolg zu behandeln.
|
|
curl -sS -f \
|
|
--connect-timeout "$CLUPILOT_HTTP_CONNECT_TIMEOUT" \
|
|
--max-time "$CLUPILOT_HTTP_MAX_TIME" \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'Accept: application/json' \
|
|
-X POST --data "$_payload" \
|
|
"$_url" >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
|
|
if command -v wget >/dev/null 2>&1; then
|
|
wget -q -O /dev/null \
|
|
--timeout="$CLUPILOT_HTTP_MAX_TIME" \
|
|
--header='Content-Type: application/json' \
|
|
--header='Accept: application/json' \
|
|
--post-data="$_payload" \
|
|
"$_url"
|
|
return $?
|
|
fi
|
|
|
|
return 1
|
|
}
|