Fixrunde 2 zu Task 4:
- N1 (wichtig): file_mode() in test_cli.sh/test_setup_pi.sh probierte
"stat -f '%Lp'" vor "stat -c '%a'" - auf GNU-coreutils (dem Pi) deutet
"-f" dort "Dateisystemstatus" und liefert Muell statt der Rechte-Zahl,
faellt aber trotzdem in den "||"-Zweig durch, der zusaetzlich "644"
anhaengt. Reihenfolge umgedreht und mit einem per gstat erzwungenen
GNU-Testlauf zusaetzlich abgesichert.
- N2 (wichtig): status zeigte nach einem Wechsel zurueck in den
Rettungsmodus weiterhin die letzte Migrationsphase aus einer liegen
gebliebenen control.json (moeglich seit RuntimeDirectoryPreserve=
restart aus Fixrunde 1). Phase wird jetzt nur noch angezeigt, wenn
Modus tatsaechlich "migration" ist.
- N3 (klein): die Schluesselerkennung in setup_pi.sh war rein
zeilenverankert ("^KEY=") und haette einen eingerueckten Bestandswert
als fehlend gewertet - beim naechsten Sourcen der Konfiguration haette
die zusaetzlich angehaengte Vorlagenzeile das eigene Passwort
ueberschrieben. Muster auf fuehrenden Leerraum/export gehaertet.
- N4 (klein): test_setup_pi.sh liess setup_pi.sh ueber den scp-Stub echte
Dateien im /tmp des Entwicklungsrechners anlegen und wieder loeschen.
Neue Variable NINLIGHT_STAGE_DIR biegt das auf eine Sandbox um.
Alle vier Punkte mit Regressionstests, N2 und N3 zusaetzlich per
Mutationsprobe (Fix zurueckgenommen, Test wird rot) verifiziert.
481 lines
20 KiB
Bash
Executable File
481 lines
20 KiB
Bash
Executable File
#!/bin/sh
|
|
# Schaltet die ninlight-Station (Rettungs- oder Migrationsmodus) an und aus.
|
|
#
|
|
# Die Station ist absichtlich nicht dauerhaft an: Solange sie laeuft, kann jeder
|
|
# in Funkreichweite mit dem Passwort einer Lampe beim naechsten Boot beliebige
|
|
# Firmware unterschieben, und der Werksreset per fuenf Stromzyklen funktioniert
|
|
# bei keiner Lampe in Reichweite mehr (die Firmware loescht den Zaehler vor
|
|
# jedem Recovery-Versuch). Der Migrationsmodus (Zugangspunkt "qconnex-firmware",
|
|
# oeffentlich bekanntes Passwort aus der alten Klyqa-Firmware) verschiebt diese
|
|
# Vertrauensgrenze noch weiter nach aussen - siehe README.md.
|
|
#
|
|
# Die beiden Zugangspunkte schliessen sich gegenseitig aus: ap_switch() faehrt
|
|
# immer erst jedes andere Stationsprofil herunter, bevor es das gewuenschte
|
|
# hochfaehrt, und prueft danach per nmcli nach, dass wirklich nur eines aktiv
|
|
# ist.
|
|
set -eu
|
|
|
|
# Ueberschreibbar fuer Tests (dasselbe Prinzip wie NINLIGHT_STATION_ROOT/_BIN/_UNIT
|
|
# in deploy.sh): so kann tests/test_cli.sh gegen ein Sandbox-Verzeichnis statt
|
|
# gegen echte Systempfade laufen.
|
|
CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf}
|
|
RUN_DIR=${NINLIGHT_RUN_DIR:-/run/ninlight-station}
|
|
STATION_LIB=${NINLIGHT_STATION_LIB:-/opt/ninlight-station}
|
|
|
|
if [ -r "$CONF" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$CONF"
|
|
elif [ -e "$CONF" ]; then
|
|
# $CONF ist 0600 root:root, damit das Passwort nicht fuer jeden lesbar
|
|
# ist. Der dokumentierte Aufruf "ssh <pi-host> ninlight-station on" laeuft
|
|
# aber als normaler Benutzer - "sudo cat" liest die Datei ein, ohne ihre
|
|
# Rechte aufzuweichen.
|
|
eval "$(sudo cat "$CONF")"
|
|
else
|
|
echo "Fehler: $CONF fehlt - zuerst tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for _key in SSID PSK IMAGE_DIR HOME_CONNECTION AP_CONNECTION \
|
|
MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
|
|
eval "test -n \"\${$_key-}\"" || {
|
|
echo "Fehler: $CONF: Schluessel $_key fehlt oder ist leer - tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
unset _key
|
|
|
|
need_profile() {
|
|
# $1 = NetworkManager-Profilname, der existieren muss.
|
|
# -F: der Profilname ist ein Konfigurationswert, kein BRE-Muster - ein Name mit
|
|
# "." oder "[" wuerde mit "grep -x" sonst falsch (oder gar nicht) treffen.
|
|
nmcli -t -f NAME con show | grep -Fqx "$1" || {
|
|
echo "Fehler: NetworkManager-Profil '$1' fehlt - tools/station/setup_pi.sh ausfuehren" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
images() {
|
|
find "$IMAGE_DIR" -maxdepth 1 -name 'ninlight_*_ota.img' 2>/dev/null | sort
|
|
}
|
|
|
|
migrate_images() {
|
|
find "$IMAGE_DIR/migrate" -maxdepth 1 -name '*.img' 2>/dev/null | sort
|
|
}
|
|
|
|
# Faehrt erst jedes andere Stationsprofil herunter, dann (falls $1 nicht leer ist)
|
|
# das gewuenschte hoch, und prueft danach per nmcli nach, dass wirklich genau
|
|
# dieses Profil auf wlan0 aktiv ist. Weicht es ab, geht alles wieder herunter und
|
|
# die Funktion bricht mit Fehler ab - das ist die Absicherung fuer Fall 15 der
|
|
# Testliste: zwei gleichzeitig aktive Stationsprofile auf demselben Interface
|
|
# duerfen nie vorkommen.
|
|
#
|
|
# $1 leer bedeutet "nur alles herunterfahren" (Grundlage fuer "off").
|
|
ap_switch() {
|
|
_target=${1:-}
|
|
for _p in "$AP_CONNECTION" "$MIGRATE_AP_CONNECTION"; do
|
|
if [ -n "$_target" ] && [ "$_p" = "$_target" ]; then
|
|
continue
|
|
fi
|
|
sudo nmcli con down "$_p" >/dev/null 2>&1 || true
|
|
done
|
|
if [ -z "$_target" ]; then
|
|
# "off": jedes "con down" oben ist mit "|| true" abgesichert, weil ein
|
|
# haengender NetworkManager den Aufruf sonst zum Abbruch braechte, bevor
|
|
# ueberhaupt versucht wurde, beide Profile herunterzufahren. Das darf aber
|
|
# nicht heissen, dass ein haengengebliebenes Profil stillschweigend als
|
|
# "aus" durchgeht: nachsehen, ob wlan0 wirklich kein Stationsprofil mehr
|
|
# zeigt, und erst dann Erfolg melden (Fixrunde 1, I2) - sonst kann z.B. der
|
|
# Migrations-AP mit oeffentlich bekanntem Passwort weitersenden, waehrend
|
|
# das CLI "Station aus" behauptet.
|
|
_active=$(nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2)
|
|
if [ "$_active" = "$AP_CONNECTION" ] || [ "$_active" = "$MIGRATE_AP_CONNECTION" ]; then
|
|
echo "Fehler: wlan0 zeigt nach dem Ausschalten immer noch '$_active' aktiv - NetworkManager pruefen (Zugangspunkt bleibt an)" >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
fi
|
|
sudo nmcli con up "$_target" >/dev/null
|
|
_active=$(nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2)
|
|
if [ "$_active" != "$_target" ]; then
|
|
sudo nmcli con down "$AP_CONNECTION" >/dev/null 2>&1 || true
|
|
sudo nmcli con down "$MIGRATE_AP_CONNECTION" >/dev/null 2>&1 || true
|
|
echo "Fehler: wlan0 zeigt nach dem Umschalten auf '$_target' unerwartet '$_active' - beide Stationsprofile wieder heruntergefahren" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Schreibt $RUN_DIR/mode atomar mit 0644 (Temp-Datei im selben Verzeichnis, dann
|
|
# umbenennen) - server.py liest sie bei jedem Dienststart per --mode-file.
|
|
write_mode() {
|
|
sudo mkdir -p "$RUN_DIR"
|
|
_tmp="$RUN_DIR/.mode-$$"
|
|
printf '%s' "$1" | sudo tee "$_tmp" >/dev/null && sudo chmod 0644 "$_tmp" || return 1
|
|
sudo mv -f "$_tmp" "$RUN_DIR/mode"
|
|
}
|
|
|
|
# Schreibt $RUN_DIR/control.json atomar mit 0644 - dasselbe Muster (Temp-Datei im
|
|
# selben Verzeichnis anlegen, Rechte setzen, dann umbenennen) und dieselben Rechte
|
|
# wie server.py's eigenes write_control(), sonst kann der Dienst die Datei
|
|
# anschliessend nicht mehr ueberschreiben oder das CLI sie nicht mehr lesen.
|
|
#
|
|
# $1 = phase (auto|listen|armed), $2 = armed_target oder leer (-> null im JSON).
|
|
write_control() {
|
|
sudo mkdir -p "$RUN_DIR"
|
|
_tmp="$RUN_DIR/.control-$$.json"
|
|
if [ -n "${2:-}" ]; then
|
|
_json=$(printf '{"phase": "%s", "armed_target": "%s"}' "$1" "$2")
|
|
else
|
|
_json=$(printf '{"phase": "%s", "armed_target": null}' "$1")
|
|
fi
|
|
printf '%s' "$_json" | sudo tee "$_tmp" >/dev/null && sudo chmod 0644 "$_tmp" || return 1
|
|
sudo mv -f "$_tmp" "$RUN_DIR/control.json"
|
|
}
|
|
|
|
# Startet (neu) und wartet kurz auf Lebenszeichen, dann prueft es nach, dass der
|
|
# eben gestartete Prozess wirklich den beabsichtigten Modus/die beabsichtigte
|
|
# Phase sieht.
|
|
#
|
|
# $1 = das gerade hochgefahrene AP-Profil (fuer Rollback bei Fehlschlag)
|
|
# $2 = erwarteter Inhalt von $RUN_DIR/mode nach dem Neustart
|
|
# $3 = erwartete Phase in control.json (nur im Migrationsmodus angeben, sonst leer)
|
|
#
|
|
# Der Grund fuer $2/$3: die systemd-Unit traegt RuntimeDirectory=ninlight-station,
|
|
# und systemd leert dieses Verzeichnis bei jedem Stop/Restart, sofern die Unit
|
|
# nicht RuntimeDirectoryPreserve=restart setzt. write_mode()/write_control() oben
|
|
# laufen VOR diesem "systemctl restart" - ein Neustart, der das Verzeichnis doch
|
|
# leert (weil die Unit die Zeile verliert, oder auf einem Pi mit alter Unit-Datei
|
|
# installiert wird), wischt also genau die Dateien wieder weg, die gerade erst
|
|
# geschrieben wurden. Der frisch gestartete Serverprozess faende dann keine
|
|
# Modusdatei und faellt laut server.py:resolve_mode() still auf "recovery"
|
|
# zurueck (bzw. read_control() auf "auto" bei fehlender Steuerdatei) - ein
|
|
# ausdrueckliches "migrate on --listen" wuerde so unbemerkt zu "Rettungsmodus,
|
|
# antwortet 404" bzw. zu "automatische Auslieferung" (Fixrunde 1, C1). Die Unit
|
|
# traegt deshalb RuntimeDirectoryPreserve=restart; diese Nachpruefung faengt es
|
|
# zusaetzlich ab, falls das je auseinanderlaeuft, statt sich allein auf die
|
|
# Unit-Datei zu verlassen.
|
|
start_and_verify() {
|
|
_ap=$1
|
|
_expect_mode=$2
|
|
_expect_phase=${3:-}
|
|
# Eine fruehere Absturzserie darf den naechsten ehrlichen Versuch nicht
|
|
# blockieren (systemd verweigert "start"/"restart" sonst sofort mit
|
|
# "start-limit-hit").
|
|
sudo systemctl reset-failed ninlight-station 2>/dev/null || true
|
|
if ! sudo systemctl restart ninlight-station; then
|
|
# Der Neustart selbst ist gescheitert (z.B. start-limit-hit) - unter
|
|
# set -eu wuerde das Skript hier sonst abbrechen, bevor die
|
|
# Lebendigkeitspruefung unten je erreicht wird, und der eben
|
|
# hochgefahrene Zugangspunkt bliebe offen ohne Server dahinter stehen.
|
|
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
|
echo "Fehler: ninlight-station liess sich nicht (neu) starten - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
|
exit 1
|
|
fi
|
|
# "systemctl restart" meldet bei Type=simple schon Erfolg, sobald der Prozess
|
|
# gestartet wurde - nicht, sobald er ueberlebt hat. Kurz warten und wirklich
|
|
# nachsehen, sonst bleibt ein offener AP ohne Server dahinter stehen.
|
|
sleep 1
|
|
if ! systemctl is-active --quiet ninlight-station; then
|
|
# Mit Restart=always laeuft ein abgestuerzter Dienst sonst im Hintergrund
|
|
# weiter in seiner Neustartschleife - erst stoppen, dann den AP runter,
|
|
# sonst suggeriert die Meldung unten einen saubereren Zustand als er ist.
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
|
echo "Fehler: ninlight-station ist nach dem Start nicht aktiv - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
|
exit 1
|
|
fi
|
|
_actual_mode=$(cat "$RUN_DIR/mode" 2>/dev/null || true)
|
|
if [ "$_actual_mode" != "$_expect_mode" ]; then
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
|
echo "Fehler: $RUN_DIR/mode enthaelt nach dem Neustart '$_actual_mode' statt '$_expect_mode' - RuntimeDirectory wurde vermutlich geleert (RuntimeDirectoryPreserve=restart in der Unit pruefen). Zugangspunkt wieder abgeschaltet." >&2
|
|
exit 1
|
|
fi
|
|
if [ -n "$_expect_phase" ]; then
|
|
_actual_phase=$(python3 - "$RUN_DIR/control.json" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
try:
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
except (OSError, ValueError):
|
|
d = {}
|
|
print(d.get("phase", ""))
|
|
PY
|
|
)
|
|
if [ "$_actual_phase" != "$_expect_phase" ]; then
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
|
echo "Fehler: $RUN_DIR/control.json enthaelt nach dem Neustart Phase '$_actual_phase' statt '$_expect_phase' - RuntimeDirectory wurde vermutlich geleert. Zugangspunkt wieder abgeschaltet." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
recovery_on() {
|
|
need_profile "$AP_CONNECTION"
|
|
n=$(images | wc -l | tr -d ' ')
|
|
[ "$n" -gt 0 ] || echo "Warnung: $IMAGE_DIR enthaelt kein Image - die Station antwortet auf alles mit 404" >&2
|
|
ap_switch "$AP_CONNECTION"
|
|
write_mode "recovery"
|
|
start_and_verify "$AP_CONNECTION" "recovery"
|
|
echo "Station an: SSID $SSID auf 192.168.8.1, $n Image(s)"
|
|
# Mit dem oeffentlichen Default-Passwort (im Repo dokumentiert) kann jeder in
|
|
# Funkreichweite, der es kennt, jeder Lampe beliebige Firmware unterschieben - das ist
|
|
# die eine Haelfte der Betriebsregeln aus README.md/§9, die andere (Station wieder
|
|
# ausschalten) steht unten.
|
|
if [ "$PSK" = "ninlight-recovery" ]; then
|
|
echo "Warnung: PSK ist noch der oeffentliche Default 'ninlight-recovery' - eigenes Passwort in /etc/ninlight-station.conf setzen." >&2
|
|
fi
|
|
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
|
|
}
|
|
|
|
migrate_on() {
|
|
# $1 = "--listen" oder leer.
|
|
_phase=auto
|
|
if [ "${1:-}" = "--listen" ]; then
|
|
_phase=listen
|
|
elif [ -n "${1:-}" ]; then
|
|
usage
|
|
exit 2
|
|
fi
|
|
need_profile "$MIGRATE_AP_CONNECTION"
|
|
n=$(migrate_images | wc -l | tr -d ' ')
|
|
[ "$n" -gt 0 ] || echo "Warnung: $IMAGE_DIR/migrate enthaelt keine Migrationsimages - die Station antwortet auf alles mit 404" >&2
|
|
# Faehrt ninlight-recovery (falls es laeuft) herunter, BEVOR qconnex-firmware
|
|
# hochkommt, und prueft danach nach, dass wirklich nur eines aktiv ist.
|
|
ap_switch "$MIGRATE_AP_CONNECTION"
|
|
write_mode "migration"
|
|
write_control "$_phase" "" || {
|
|
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
|
exit 1
|
|
}
|
|
start_and_verify "$MIGRATE_AP_CONNECTION" "migration" "$_phase"
|
|
echo "Migration an: SSID $MIGRATE_SSID auf 192.168.8.1, Phase $_phase, $n Migrationsimage(s)"
|
|
echo "Achtung: das Passwort dieses Zugangspunkts ist aus der alten Firmware oeffentlich bekannt - siehe README.md." >&2
|
|
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
|
|
}
|
|
|
|
full_off() {
|
|
sudo systemctl stop ninlight-station 2>/dev/null || true
|
|
ap_switch ""
|
|
if nmcli -t -f NAME con show | grep -Fqx "$HOME_CONNECTION"; then
|
|
if sudo nmcli con up "$HOME_CONNECTION" >/dev/null; then
|
|
echo "Station aus, wlan0 zurueck im Heim-WLAN ($HOME_CONNECTION)"
|
|
else
|
|
echo "Station aus. Zurueckschalten zu '$HOME_CONNECTION' fehlgeschlagen - wlan0-Status pruefen." >&2
|
|
fi
|
|
else
|
|
echo "Station aus. Profil '$HOME_CONNECTION' fehlt, wlan0 bleibt ungenutzt - der Zugang ueber Kabel ist davon nicht betroffen." >&2
|
|
fi
|
|
}
|
|
|
|
listen_cmd() {
|
|
write_control "listen" "" || {
|
|
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
|
exit 1
|
|
}
|
|
echo "Phase: listen"
|
|
}
|
|
|
|
arm_cmd() {
|
|
_target=${1:-}
|
|
if [ -z "$_target" ]; then
|
|
_status_file="$RUN_DIR/status.json"
|
|
if ! _target=$(python3 - "$_status_file" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
try:
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
except (OSError, ValueError):
|
|
sys.exit(1)
|
|
t = (d.get("last_seen") or {}).get("target")
|
|
if not t:
|
|
sys.exit(1)
|
|
print(t)
|
|
PY
|
|
); then
|
|
echo "Fehler: kein zuletzt erkanntes Modell (last_seen) in $_status_file - Ziel angeben: ninlight-station arm <target>" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
if ! python3 - "$STATION_LIB" "$_target" <<'PY'
|
|
import sys
|
|
|
|
sys.path.insert(0, sys.argv[1])
|
|
import products
|
|
|
|
target = sys.argv[2]
|
|
sys.exit(0 if any(p.target == target for p in products.PRODUCTS.values()) else 1)
|
|
PY
|
|
then
|
|
echo "Fehler: unbekanntes Ziel '$_target' - gueltige Ziele stehen in products.py" >&2
|
|
exit 1
|
|
fi
|
|
write_control "armed" "$_target" || {
|
|
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
|
exit 1
|
|
}
|
|
echo "Freigegeben: $_target"
|
|
}
|
|
|
|
status_cmd() {
|
|
# "systemctl is-active" schreibt den Zustand ("inactive", "failed", ...) auf stdout und
|
|
# liefert dabei einen Rueckgabewert ungleich 0, sobald der Dienst nicht "active" ist -
|
|
# ein "|| echo inaktiv" wuerde also zusaetzlich zur ersten Zeile ausgeloest und beide
|
|
# landen in der Ausgabe. Der Zustand selbst ist die Auskunft; "status" ist damit
|
|
# erfolgreich, sobald es ihn ermitteln konnte, unabhaengig vom Dienstzustand.
|
|
_status_file="$RUN_DIR/status.json"
|
|
_control_file="$RUN_DIR/control.json"
|
|
# "existiert nicht" (Station lief noch nie) und "existiert, ist aber nicht lesbar"
|
|
# sind zwei verschiedene Zustaende und duerfen nicht beide stillschweigend im
|
|
# "-r"-Test verschwinden: sonst faellt ein Rechte-Fehler auf status.json (siehe
|
|
# server.py, schreibt als root) nie auf, weil die Ausgabe genauso aussieht wie
|
|
# "noch nie ausgeliefert". Das gilt auch fuer Modus/Erkannt unten und fuer
|
|
# Phase/control.json weiter unten.
|
|
if [ -r "$_status_file" ]; then
|
|
_info=$(python3 - "$_status_file" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
try:
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
except (OSError, ValueError):
|
|
d = {}
|
|
last_seen = d.get("last_seen")
|
|
if last_seen:
|
|
seen = "{} ({})".format(
|
|
last_seen.get("product_name") or last_seen.get("target", "?"),
|
|
last_seen.get("lamp_version", "?"),
|
|
)
|
|
else:
|
|
seen = "-"
|
|
print(d.get("mode", "-"))
|
|
print(seen)
|
|
PY
|
|
)
|
|
_modus=$(printf '%s\n' "$_info" | sed -n '1p')
|
|
_erkannt=$(printf '%s\n' "$_info" | sed -n '2p')
|
|
elif [ -e "$_status_file" ]; then
|
|
_modus="unbekannt (Datei nicht lesbar)"
|
|
_erkannt="unbekannt (Datei nicht lesbar)"
|
|
else
|
|
_modus="-"
|
|
_erkannt="-"
|
|
fi
|
|
# Die Phase kommt bewusst aus control.json, nicht aus status.json: "arm" und
|
|
# "listen" starten den Dienst absichtlich nicht neu, also schreibt server.py's
|
|
# write_state() status.json in der Zwischenzeit nicht neu (Fixrunde 1, I1) -
|
|
# wer die Phase dort ablesen wuerde, saehe nach einer erfolgreichen Freigabe
|
|
# weiterhin die Phase von vor dem letzten Dienststart und koennte "arm" fuer
|
|
# gescheitert halten oder faelschlich glauben, nichts sei mehr scharf.
|
|
#
|
|
# ABER: Phase ist ein Konzept, das nur im Migrationsmodus existiert - control.json
|
|
# kann von einem frueheren "arm"/"migrate on" liegen bleiben (RuntimeDirectoryPreserve=
|
|
# restart aus Fixrunde 1, C1 laesst es Neustarts jetzt ausdruecklich ueberleben),
|
|
# auch nachdem laengst wieder in den Rettungsmodus zurueckgeschaltet wurde -
|
|
# "recovery on" raeumt die Datei bewusst nicht auf, weil der Dienst sie im
|
|
# Rettungsmodus ohnehin ignoriert (server.py liest sie nur im Migrationsmodus).
|
|
# Ohne diese Bedingung wuerde status nach "migrate on --listen; arm X; on" ein
|
|
# irrefuehrendes "Phase: armed" im Rettungsmodus zeigen (Fixrunde 2, N2) - genau
|
|
# in der sicherheitsrelevanten Dimension "scharf/nicht scharf", die I1 eigentlich
|
|
# klarstellen sollte. Deshalb wird control.json nur gelesen, wenn Modus wirklich
|
|
# "migration" ist; sonst ist die Phase per Definition "-", unabhaengig davon, was
|
|
# noch in control.json steht.
|
|
if [ "$_modus" != "migration" ]; then
|
|
_phase="-"
|
|
elif [ -r "$_control_file" ]; then
|
|
_phase=$(python3 - "$_control_file" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
try:
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
except (OSError, ValueError):
|
|
d = {}
|
|
phase = d.get("phase", "-")
|
|
target = d.get("armed_target")
|
|
if phase == "armed" and target:
|
|
print("{} (Ziel: {})".format(phase, target))
|
|
else:
|
|
print(phase)
|
|
PY
|
|
)
|
|
elif [ -e "$_control_file" ]; then
|
|
_phase="unbekannt (Datei nicht lesbar)"
|
|
else
|
|
_phase="-"
|
|
fi
|
|
printf 'Modus : %s\n' "$_modus"
|
|
printf 'Phase : %s\n' "$_phase"
|
|
printf 'Erkannt: %s\n' "$_erkannt"
|
|
printf 'Dienst : %s\n' "$(systemctl is-active ninlight-station 2>/dev/null || true)"
|
|
printf 'wlan0 : %s\n' "$(nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2)"
|
|
printf 'Adresse: %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')"
|
|
echo "Images :"
|
|
images | while read -r f; do
|
|
printf ' %-34s %s\n' "$(basename "$f")" \
|
|
"$(python3 - "$f" <<'PY'
|
|
import pathlib, sys
|
|
t = pathlib.Path(sys.argv[1]).read_bytes()[-512:]
|
|
print(t[21:37].rstrip(b"\x00").decode("ascii", "replace") if t[:4] == b"NINL" else "kein Trailer")
|
|
PY
|
|
)"
|
|
done
|
|
if [ -r "$_status_file" ]; then
|
|
echo "Zuletzt ausgeliefert:"
|
|
sed 's/^/ /' "$_status_file"
|
|
elif [ -e "$_status_file" ]; then
|
|
echo "Fehler: $_status_file existiert, ist aber nicht lesbar (Rechte pruefen)" >&2
|
|
fi
|
|
# "status" soll mit 0 enden, sobald es seine Auskunft geben konnte, auch wenn noch nie
|
|
# ausgeliefert wurde - ohne das explizite exit 0 waere der Exit-Code des Skripts hier
|
|
# der des fehlgeschlagenen "-r"-Tests, weil das die letzte Anweisung im Zweig ist.
|
|
exit 0
|
|
}
|
|
|
|
usage() {
|
|
echo "Aufruf: ninlight-station on|recovery {on|off}|migrate {on [--listen]|off}|arm [<target>]|listen|off|status" >&2
|
|
}
|
|
|
|
case "${1:-}" in
|
|
on)
|
|
recovery_on
|
|
;;
|
|
recovery)
|
|
case "${2:-}" in
|
|
on) recovery_on ;;
|
|
off) full_off ;;
|
|
*) usage; exit 2 ;;
|
|
esac
|
|
;;
|
|
migrate)
|
|
case "${2:-}" in
|
|
on) migrate_on "${3:-}" ;;
|
|
off) full_off ;;
|
|
*) usage; exit 2 ;;
|
|
esac
|
|
;;
|
|
arm)
|
|
arm_cmd "${2:-}"
|
|
;;
|
|
listen)
|
|
listen_cmd
|
|
;;
|
|
off)
|
|
full_off
|
|
;;
|
|
status)
|
|
status_cmd
|
|
;;
|
|
*)
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|