fix(station): CLI prueft Rettungs- und Migrationsbildsatz getrennt, meldet Modus dauerhaft

Fixrunde 1, Befund 1: imageset_dir()/migrate_images()/imageset_status_lines()
pruefen bislang IMMER die Migrationsverzeichnisse, unabhaengig vom Modus - im
Rettungsbetrieb meldete "status" so faelschlich "MISSING", waehrend
rescue-nocreds voll war, oder umgekehrt "vorhanden", waehrend die Rettung
nichts hatte. Ein falsches "MISSING" dort verleitet dazu, auf mit-wlan
umzuschalten - und dann liefert die Rettung tatsaechlich Abbilder mit dem
WLAN-Passwort aus. imageset_dir() nimmt jetzt den Modus als Parameter
(Vorgabe: current_mode()), dieselbe Unterscheidung wie im Panel.

Befund 2: images() griff weiter flach auf $IMAGE_DIR zu, wo seit dem
Deploy-Umbau nichts mehr liegt - "recovery on" warnte deshalb bei JEDEM Start
"answers everything with 404". Ersetzt durch imageset_images(), das dem
Bildsatz-Umschalter UND dem Modus folgt.

Befund 3 (CLI-Seite): current_mode() liest die neue dauerhafte
LAST_MODE_FILE statt der ephemeren RUN_DIR/mode - write_mode() schreibt jetzt
beide (siehe display.py-Commit fuer die Panel-Seite derselben Ursache).

tests/test_cli.sh legte weiterhin die alte flache Rettungsablage an
(images/ninlight_foo_ota.img) - die Sandbox hat jetzt eigene
rescue-wlan/rescue-nocreds-Verzeichnisse wie ein echter Pi nach diesem
Umschalter, und run_cli() setzt NINLIGHT_LAST_MODE_FILE auf einen
Sandbox-Pfad. Mehrere bestehende Bildsatz-Szenarien liefen unbemerkt gegen
den Migrationssatz, obwohl kein Modus gesetzt war (Vorgabe: recovery) -
angepasst, damit sie den tatsaechlich geltenden Satz pruefen.
This commit is contained in:
Michael Sauer
2026-09-23 21:59:25 +02:00
parent 1562a7983a
commit f31ef879ee
2 changed files with 108 additions and 29 deletions
+94 -23
View File
@@ -130,8 +130,33 @@ need_profile() {
}
}
images() {
find "$IMAGE_DIR" -maxdepth 1 -name 'ninlight_*_ota.img' 2>/dev/null | sort
# --- Modus lesen/schreiben (Fixrunde 1, Befund 3) -----------------------------------
#
# RUN_DIR/mode (write_mode() unten schreibt sie zusaetzlich) taugt NICHT als Quelle
# fuer "welcher Modus gilt gerade/als naechstes", solange die Station AUS ist: die Unit
# setzt RuntimeDirectoryPreserve=restart, das erhaelt /run/ninlight-station nur ueber
# einen "systemctl restart" (das, was "recovery on"/"migrate on" bei einem schon
# laufenden Dienst ausloesen), NICHT ueber einen echten "systemctl stop" - und genau
# den fuehrt "ninlight-station off" aus. Fehlt die Datei, gilt ueberall stillschweigend
# "recovery" (derselbe sichere Vorgabewert wie beim Bildsatz) - eine fruehere Fassung
# dieses Skripts UND von display.py hat das mit "die Datei bleibt bei jedem Zustand
# erhalten" verwechselt.
#
# LAST_MODE_FILE ist deshalb wie IMAGESET_FILE eine EIGENE, dauerhafte Ablage unter
# /var/lib - dieselbe Datei, die display.py:LAST_MODE_FILE liest (Panel und CLI muessen
# sich hier einig sein, sonst zeigt das eine "Rettung: MISSING" waehrend das andere
# "Migration: 3 image(s)" meldet).
LAST_MODE_FILE=${NINLIGHT_LAST_MODE_FILE:-/var/lib/ninlight-station/mode}
# Der zuletzt angeforderte Modus, oder "recovery" bei fehlender/unlesbarer/unbekannter
# Datei - dieselbe Vorgabe und dieselbe Begruendung wie imageset_get() unten. Scheitert
# nie: eine kaputte Datei darf weder "status" noch "bildsatz" aufhalten.
current_mode() {
_m=$(cat "$LAST_MODE_FILE" 2>/dev/null | tr -d ' \t\r\n') || true
case "$_m" in
recovery|migration) printf '%s' "$_m" ;;
*) printf '%s' "recovery" ;;
esac
}
# --- Bildsatz lesen, schreiben, benennen --------------------------------------------
@@ -153,12 +178,27 @@ imageset_get() {
esac
}
# Das Verzeichnis zum Namen des Bildsatzes ($1).
# Das Verzeichnis zum Namen des Bildsatzes ($1), fuer den Modus $2 (Vorgabe:
# current_mode()). Rettung und Migration liegen in getrennten Verzeichnispaaren
# (server.py:RESCUE_SET_DIRS/IMAGE_SET_DIRS begruendet das) - dieselbe Unterscheidung
# wie display.py:_read_imageset(). Fixrunde 1, Befund 1: vor dieser Aenderung pruefte
# dieses Skript hier IMMER die Migrationsverzeichnisse, auch im Rettungsbetrieb - ein
# falsches "MISSING" dort verleitet dazu, auf mit-wlan umzuschalten, und DANN liefert
# die Rettung tatsaechlich Abbilder mit dem WLAN-Passwort aus.
imageset_dir() {
case "$1" in
mit-wlan) printf '%s' "$IMAGE_DIR/migrate-wlan" ;;
*) printf '%s' "$IMAGE_DIR/migrate-nocreds" ;;
esac
_name=$1
_mode=${2:-$(current_mode)}
if [ "$_mode" = "recovery" ]; then
case "$_name" in
mit-wlan) printf '%s' "$IMAGE_DIR/rescue-wlan" ;;
*) printf '%s' "$IMAGE_DIR/rescue-nocreds" ;;
esac
else
case "$_name" in
mit-wlan) printf '%s' "$IMAGE_DIR/migrate-wlan" ;;
*) printf '%s' "$IMAGE_DIR/migrate-nocreds" ;;
esac
fi
}
imageset_label() {
@@ -179,8 +219,14 @@ imageset_set() {
sudo mv -f "$_tmp" "$IMAGESET_FILE"
}
migrate_images() {
find "$(imageset_dir "$(imageset_get)")" -maxdepth 1 -name '*.img' 2>/dev/null | sort
# Die Abbilder des eingestellten Bildsatzes fuer den Modus $1 (Vorgabe: current_mode()).
# Ersetzt das fruehere migrate_images() (nur Migration) UND das fruehere images() (immer
# flach in $IMAGE_DIR, Fixrunde 1, Befund 2: nach dem Deploy-Umbau liegt dort nichts
# mehr - recovery_on() meldete seither bei JEDEM Start faelschlich "answers everything
# with 404", ununterscheidbar vom echten Fehlerfall).
imageset_images() {
_mode=${1:-$(current_mode)}
find "$(imageset_dir "$(imageset_get)" "$_mode")" -maxdepth 1 -name '*.img' 2>/dev/null | sort
}
# "bildsatz" ohne Argument: nur berichten. Mit Argument: umschalten.
@@ -204,6 +250,9 @@ bildsatz_cmd() {
}
echo "Image set: $(imageset_label "$_want")"
[ "$_now" = "$_want" ] && echo " (was already set)"
# Der Umschalter gilt fuer BEIDE Betriebsarten - hier wird deshalb der Satz gemeldet,
# der fuer den Modus zaehlt, in dem die Station gerade steht bzw. als naechstes
# anlaufen wuerde (current_mode()), nicht pauschal die Migration.
_dir=$(imageset_dir "$_want")
if [ "$(find "$_dir" -maxdepth 1 -name '*.img' 2>/dev/null | wc -l | tr -d ' ')" -eq 0 ]; then
# Kein stiller Rueckfall auf den anderen Satz - server.py liefert dann gar nichts
@@ -212,18 +261,20 @@ bildsatz_cmd() {
echo "Warning: $_dir missing or empty - this image set serves NOTHING. Run tools/station/deploy.sh again." >&2
fi
if [ "$_want" = "mit-wlan" ]; then
echo "Caution: migrated lamps join the Wi-Fi from config.local.cmake right away - including lamps that are not yours and got picked up in automatic mode." >&2
echo "Caution: any lamp migrated OR rescued while this is set joins the Wi-Fi from config.local.cmake right away - including lamps that are not yours and got picked up in automatic mode." >&2
fi
}
# Die zwei Zeilen, die "status" und "bildsatz" ohne Argument gemeinsam ausgeben.
# Die zwei Zeilen, die "status" und "bildsatz" ohne Argument gemeinsam ausgeben - fuer
# den Modus, in dem die Station gerade steht bzw. als naechstes anlaufen wuerde.
imageset_status_lines() {
_mode=$(current_mode)
_set=$(imageset_get)
_dir=$(imageset_dir "$_set")
_dir=$(imageset_dir "$_set" "$_mode")
# "Vorhanden" heisst: mindestens ein Image darin - dieselbe Regel wie
# server.py:migrate_set_present() und display.py:_read_imageset(). Ein leeres
# Verzeichnis liefert genauso nichts aus wie ein fehlendes.
_n=$(migrate_images | wc -l | tr -d ' ')
# server.py:migrate_set_present()/rescue_set_present() und display.py:_read_imageset().
# Ein leeres Verzeichnis liefert genauso nichts aus wie ein fehlendes.
_n=$(imageset_images "$_mode" | wc -l | tr -d ' ')
if [ "$_n" -gt 0 ]; then
_state="$_n image(s)"
else
@@ -293,12 +344,22 @@ ap_switch() {
}
# Schreibt $RUN_DIR/mode atomar mit 0644 (Temp-Datei im selben Verzeichnis, dann
# umbenennen) - server.py liest sie bei jedem Dienststart per --mode-file.
# umbenennen) - server.py liest sie bei jedem Dienststart per --mode-file. ZUSAETZLICH
# dieselbe Zeichenkette nach $LAST_MODE_FILE (dauerhaft, siehe dort) - RUN_DIR/mode
# ueberlebt nur einen "systemctl restart", nicht das "systemctl stop" von
# "ninlight-station off" (Fixrunde 1, Befund 3). Beide Schreibvorgaenge muessen
# gelingen: schlaegt der dauerhafte fehl, wuerden Panel und CLI nach einem "off" auf
# den falschen (naemlich den vorherigen) Modus zurueckfallen, ohne dass das hier
# auffiele.
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"
sudo mv -f "$_tmp" "$RUN_DIR/mode" || return 1
sudo mkdir -p "$(dirname "$LAST_MODE_FILE")"
_tmp2="$(dirname "$LAST_MODE_FILE")/.mode-$$"
printf '%s' "$1" | sudo tee "$_tmp2" >/dev/null && sudo chmod 0644 "$_tmp2" || return 1
sudo mv -f "$_tmp2" "$LAST_MODE_FILE"
}
# Schreibt $RUN_DIR/control.json atomar mit 0644 - dasselbe Muster (Temp-Datei im
@@ -612,8 +673,12 @@ autooff_arm_if_requested() {
recovery_on() {
parse_for_option recovery "$@"
need_profile "$AP_CONNECTION"
n=$(images | wc -l | tr -d ' ')
[ "$n" -gt 0 ] || echo "Warning: $IMAGE_DIR holds no image - the station answers everything with 404" >&2
# Explizit "recovery" statt current_mode(): dieser Aufruf ENTSCHEIDET gerade, dass
# der naechste Start Rettung ist - write_mode() darunter schreibt das erst gleich.
# current_mode() koennte hier noch "migration" von einem vorherigen Lauf zeigen.
_setdir=$(imageset_dir "$(imageset_get)" recovery)
n=$(imageset_images recovery | wc -l | tr -d ' ')
[ "$n" -gt 0 ] || echo "Warning: $_setdir holds no image - the station answers everything with 404" >&2
ap_switch "$AP_CONNECTION"
write_mode "recovery"
start_and_verify "$AP_CONNECTION" "recovery"
@@ -656,8 +721,10 @@ migrate_on() {
[ "$_for_seen" -eq 1 ] || _for_minutes=""
need_profile "$MIGRATE_AP_CONNECTION"
_set=$(imageset_get)
_setdir=$(imageset_dir "$_set")
n=$(migrate_images | wc -l | tr -d ' ')
# Explizit "migration", aus demselben Grund wie in recovery_on(): dieser Aufruf legt
# den naechsten Modus gerade erst fest.
_setdir=$(imageset_dir "$_set" migration)
n=$(imageset_images migration | wc -l | tr -d ' ')
[ "$n" -gt 0 ] || echo "Warning: $_setdir holds no migration images - the station answers everything with 404" >&2
# Faehrt ninlight-recovery (falls es laeuft) herunter, BEVOR qconnex-firmware
# hochkommt, und prueft danach nach, dass wirklich nur eines aktiv ist.
@@ -1052,8 +1119,12 @@ PY
autooff_status_line
printf 'wlan0 : %s\n' "$(nmcli -t --escape no -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2-)"
printf 'Address : %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')"
# Die Rettungsabbilder des eingestellten Bildsatzes - unabhaengig von $_modus (der
# Migrationsbildsatz hat kein passendes Trailer-Format). Fixrunde 1, Befund 2:
# frueher stand hier "images" (flach in $IMAGE_DIR) - dort liegt seit dem
# Deploy-Umbau nichts mehr.
echo "Images :"
images | while read -r f; do
imageset_images recovery | while read -r f; do
printf ' %-34s %s\n' "$(basename "$f")" \
"$(python3 - "$f" <<'PY'
import pathlib, sys
@@ -1092,7 +1163,7 @@ display_cmd() {
usage() {
echo "Usage: ninlight-station on [--for <minutes>]|recovery {on [--for <minutes>]|off}|migrate {on [--listen] [--for <minutes>]|off}|arm [<target>]|listen|bildsatz [ohne-wlan|mit-wlan]|renew [<minutes>]|hold|display {test|clear|off}|off|status" >&2
echo "" >&2
echo " bildsatz Switch: which set of migration images is served." >&2
echo " bildsatz Switch: which set of recovery/migration images is served." >&2
echo " Without an argument it only reports. 'ohne-wlan' (default) serves" >&2
echo " the builds from tools/build_nocreds.sh - the migrated lamp comes" >&2
echo " up in its own onboarding access point. 'mit-wlan' serves the" >&2