feat(station): Umschalter fuer Migrationsimages mit/ohne WLAN-Daten

Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware, es gibt also
zwei Abzuege je Ziel. Die Station haelt jetzt beide vor - images/
migrate-nocreds (aus tools/build_nocreds.sh, Lampe startet im
Onboarding-AP) und images/migrate-wlan (aus build/, Lampe haengt sofort
im eigenen WLAN) - und der Bediener waehlt per Umschalter, welcher
ausgeliefert wird: "ninlight-station bildsatz" oder der Menueeintrag am
Taster.

Der Zustand liegt in /var/lib/ninlight-station/imageset, ueberlebt
Neustarts und steht dauerhaft in der Fusszeile des Panels - in jedem
Modus, auch bei ausgeschalteter Station: man soll vor dem Einschalten
sehen, was gilt.

Vorgabe und Rueckfallwert in JEDEM Zweifelsfall ist "ohne-wlan". Im
Automatikmodus wird jede Klyqa-Lampe in Reichweite migriert, auch
fremde; ein Abzug ohne Zugangsdaten ist dann ein harmloser Fehlgriff,
einer mit Zugangsdaten traegt das eigene WLAN-Passwort in ein fremdes
Geraet.

Fehlt der gewaehlte Satz (Verzeichnis weg oder leer), wird NICHTS
ausgeliefert - kein stilles Ausweichen auf den anderen Satz. Der Server
antwortet 404 mit Status "noimageset", CLI und Panel sagen "FEHLT".

server.py und die Stations-Unit muessen mit: die Auswahl der
ausgelieferten Datei ist genau die Aufgabe des Servers, und ein
Symlink-Tausch haette "gewaehlt, aber nicht vorhanden" von "dieses
Modell fehlt" nicht mehr unterscheidbar gemacht.
This commit is contained in:
Michael Sauer
2026-09-15 02:17:36 +02:00
parent 3642672c71
commit 300edafc3e
15 changed files with 1381 additions and 200 deletions
+142 -16
View File
@@ -217,8 +217,12 @@ EOF
setup_sandbox() {
local sandbox=$1 unitfile=${2:-$REAL_UNIT}
rm -rf "$sandbox"
mkdir -p "$sandbox/run" "$sandbox/images/migrate" "$sandbox/bin" "$sandbox/nmstate" \
"$sandbox/lib/display"
mkdir -p "$sandbox/run" "$sandbox/images/migrate-nocreds" "$sandbox/images/migrate-wlan" \
"$sandbox/bin" "$sandbox/nmstate" "$sandbox/lib/display" "$sandbox/var"
# Je ein Image je Bildsatz - "vorhanden" heisst fuer CLI, Server und Anzeige
# gleichermassen "mindestens ein Image darin".
echo "ohne" >"$sandbox/images/migrate-nocreds/foo_dev.img"
echo "mit" >"$sandbox/images/migrate-wlan/foo_dev.img"
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
# NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst.
@@ -292,6 +296,7 @@ run_cli() {
NINLIGHT_STATION_CONF="$sandbox/station.conf" \
NINLIGHT_RUN_DIR="$sandbox/run" \
NINLIGHT_STATION_LIB="$sandbox/lib" \
NINLIGHT_IMAGESET_FILE="$sandbox/var/imageset" \
"$CLI" "$@"
}
@@ -334,7 +339,7 @@ echo
echo "=== Fall 3: 'migrate on' faehrt ninlight-recovery herunter, BEVOR qconnex-firmware hochkommt; control.json phase=auto ==="
SBX="$TMPROOT/t3"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" on >/dev/null 2>&1 # Recovery vorher an
: >"$SBX/cmd.log"
set +e
@@ -356,7 +361,7 @@ echo
echo "=== Fall 4: 'migrate on --listen' wie Fall 3, aber phase=listen ==="
SBX="$TMPROOT/t4"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" on >/dev/null 2>&1
set +e
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
@@ -369,7 +374,8 @@ echo
echo "=== Fall 5: 'migrate on' ohne Migrationsimages -> Warnung auf stderr, Rueckgabewert 0 ==="
SBX="$TMPROOT/t5"
setup_sandbox "$SBX"
# images/migrate/ bleibt leer
# der gewaehlte Bildsatz ("ohne-wlan") wird geleert - setup_sandbox bestueckt beide
rm -f "$SBX/images/migrate-nocreds"/*.img
set +e
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
RC=$?
@@ -468,7 +474,7 @@ echo
echo "=== Fall 12: 'status' im Migrationsmodus zeigt Modus, Phase und zuletzt erkannte Lampe ==="
SBX="$TMPROOT/t12"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" migrate on >/dev/null 2>&1 # phase=auto in control.json, wie es der echte Ablauf waere
# status.json so, wie server.py sie beim Start geschrieben haette (mode/last_seen).
cat >"$SBX/run/status.json" <<'JSON'
@@ -513,7 +519,7 @@ echo
echo "=== Fall 15 (wichtigster Test): 'migrate on' bei laufendem Recovery-Modus laesst GENAU EIN Profil aktiv ==="
SBX="$TMPROOT/t15"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" on >/dev/null 2>&1
ACTIVE_BEFORE=$(cat "$SBX/nmstate/active")
echo "$ACTIVE_BEFORE" | grep -qx "ninlight-recovery" && pass "Vorbedingung: ninlight-recovery laeuft" || fail "Vorbedingung nicht erfuellt: $ACTIVE_BEFORE"
@@ -538,7 +544,7 @@ SBX="$TMPROOT/tC1"
BROKEN_UNIT="$TMPROOT/broken-ninlight-station.service"
grep -v '^RuntimeDirectoryPreserve=restart' "$REAL_UNIT" >"$BROKEN_UNIT"
setup_sandbox "$SBX" "$BROKEN_UNIT"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --listen 2>"$SBX/stderr.log")
RC=$?
@@ -555,7 +561,7 @@ ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
# in tools/station/ninlight-station.service tatsaechlich steht und wirkt.
SBX2="$TMPROOT/tC1-fixed"
setup_sandbox "$SBX2"
echo "dummy" >"$SBX2/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX2/images/migrate-nocreds/gu10_rgb_prod.img"
set +e
OUT2=$(run_cli "$SBX2" migrate on --listen 2>&1)
RC2=$?
@@ -567,7 +573,7 @@ echo
echo "=== Zusatzfall I1: 'status' zeigt die Phase aus control.json, nicht die veraltete Phase aus status.json ==="
SBX="$TMPROOT/tI1"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
# status.json wird nur beim Dienststart/bei einer Auslieferung geschrieben - hier
# simuliert, wie es nach dem Start aussehen wuerde (Phase "listen", wie beim Start).
@@ -604,7 +610,7 @@ echo
echo "=== Zusatzfall I3: ap_switch bricht ab und faehrt beide Profile herunter, wenn 'device show wlan0' nach dem Umschalten nicht passt ==="
SBX="$TMPROOT/tI3"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
set +e
OUT=$(FAKE_NMCLI_DEVSHOW=fremdes-profil run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
RC=$?
@@ -619,7 +625,7 @@ echo
echo "=== Zusatzfall N2 (Fixrunde 2): 'status' zeigt keine veraltete Migrationsphase mehr, nachdem in den Rettungsmodus zurueckgeschaltet wurde ==="
SBX="$TMPROOT/tN2"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # control.json bleibt mit phase=armed liegen
run_cli "$SBX" on >/dev/null 2>&1 # zurueck in den Rettungsmodus - control.json wird bewusst NICHT aufgeraeumt
@@ -638,7 +644,7 @@ echo
echo "=== Zusatzfall N2b (Fixrunde 2): dieselbe veraltete Freigabe, aber ohne aktualisierte status.json (Modus '-') - genau das woertliche Repro aus dem Review ==="
SBX="$TMPROOT/tN2b"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
run_cli "$SBX" on >/dev/null 2>&1
@@ -819,7 +825,7 @@ echo "=== Fall D4 (Befund Task 7f): 'off' fasst den Anzeigedienst nicht mehr an
# naechsten Pi-Neustart dunkel. Jetzt laesst 'off' den Dienst laufen.
SBX="$TMPROOT/tD4"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
run_cli "$SBX" migrate on >/dev/null 2>&1
: >"$SBX/cmd.log"
set +e
@@ -997,7 +1003,7 @@ echo
echo "=== Fall Z4: 'migrate on --listen --for 5' - beides zusammen ==="
SBX="$TMPROOT/tZ4"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --listen --for 5 2>&1)
RC=$?
@@ -1008,7 +1014,7 @@ set -e
# Umgekehrte Reihenfolge der Schalter muss genauso gehen.
SBX="$TMPROOT/tZ4b"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --for 5 --listen 2>&1)
RC=$?
@@ -1182,6 +1188,126 @@ echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "kein
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zustandsdatei, die einen Zeitschalter vortaeuscht" || fail "autooff.json trotz gescheitertem Timer: $(cat "$SBX/run/autooff.json")"
echo "$ERR" | grep -q "off" && pass "die Meldung sagt, dass die Station AN bleibt" || fail "die Meldung verschweigt den Zustand der Station: $ERR"
echo
echo "=== Bildsatz A: die Vorgabe ohne Umschalterdatei ist der Satz OHNE WLAN-Zugangsdaten ==="
# Die sichere Richtung: ein Fehlgriff kostet einen Onboarding-Durchgang, kein
# WLAN-Passwort in einer fremden Lampe.
SBX="$TMPROOT/bs1"
setup_sandbox "$SBX"
set +e
OUT=$(run_cli "$SBX" bildsatz 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
echo "$OUT" | grep -q "ohne-wlan" && pass "ohne Datei gilt 'ohne-wlan'" || fail "Vorgabe ist nicht 'ohne-wlan': $OUT"
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt" || fail "Verzeichnis fehlt in der Ausgabe: $OUT"
[ ! -e "$SBX/var/imageset" ] && pass "das blosse Berichten legt keine Datei an" || fail "'bildsatz' ohne Argument hat geschrieben"
echo
echo "=== Bildsatz B: umschalten schreibt die Datei atomar mit 0644 und bleibt stehen ==="
SBX="$TMPROOT/bs2"
setup_sandbox "$SBX"
set +e
OUT=$(run_cli "$SBX" bildsatz mit-wlan 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
[ "$(cat "$SBX/var/imageset" 2>/dev/null | tr -d '\n')" = "mit-wlan" ] && pass "die Datei enthaelt 'mit-wlan'" || fail "Dateiinhalt: $(cat "$SBX/var/imageset" 2>&1)"
MODE=$(file_mode "$SBX/var/imageset")
[ "$MODE" = "644" ] && pass "Rechte 0644 (der Anzeigedienst muss lesen koennen)" || fail "Rechte $MODE statt 644"
[ -z "$(find "$SBX/var" -name '.imageset-*' 2>/dev/null)" ] && pass "keine Temp-Datei liegengeblieben" || fail "Temp-Datei liegt noch da"
echo "$OUT" | grep -qi "achtung" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT"
# ... und er BLEIBT stehen: ein zweiter, unabhaengiger Aufruf sieht ihn.
OUT2=$(run_cli "$SBX" bildsatz 2>&1)
echo "$OUT2" | grep -q "mit-wlan" && pass "der Zustand bleibt ueber Aufrufe hinweg" || fail "Zustand nicht gespeichert: $OUT2"
# ... und zurueck.
run_cli "$SBX" bildsatz ohne-wlan >/dev/null 2>&1
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "ohne-wlan" ] && pass "zurueckschalten geht auch" || fail "zurueckschalten hat nicht gewirkt"
echo
echo "=== Bildsatz C: ein unbekannter Name schaltet nichts um ==="
SBX="$TMPROOT/bs3"
setup_sandbox "$SBX"
printf 'mit-wlan\n' >"$SBX/var/imageset"
set +e
ERR=$(run_cli "$SBX" bildsatz irgendwas 2>&1 >/dev/null)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 fuer einen unbekannten Bildsatz"
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "mit-wlan" ] && pass "der bestehende Zustand blieb unveraendert" || fail "der Zustand wurde veraendert: $(cat "$SBX/var/imageset")"
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
echo
echo "=== Bildsatz D: ein kaputter Dateiinhalt faellt auf die SICHERE Vorgabe zurueck ==="
# Nie auf den teureren Satz - das ist die eine Richtung, die kein Zweifelsfall nehmen darf.
SBX="$TMPROOT/bs4"
setup_sandbox "$SBX"
printf 'voellig-anderes\n' >"$SBX/var/imageset"
OUT=$(run_cli "$SBX" bildsatz 2>&1)
echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fail "kaputter Inhalt fuehrte nicht auf die Vorgabe: $OUT"
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT"
# Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die
# Station fuer umgeschaltet.
echo "$OUT" | grep -q "unbekannte Einstellung 'voellig-anderes'" \
&& pass "die unbekannte Einstellung wird beim Namen genannt" \
|| fail "die unbekannte Einstellung wird verschwiegen: $OUT"
# Gegenprobe: bei einem GUELTIGEN Eintrag darf diese Zeile nicht erscheinen.
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
OUT=$(run_cli "$SBX" bildsatz 2>&1)
echo "$OUT" | grep -q "unbekannte Einstellung" \
&& fail "meldet 'unbekannte Einstellung' fuer einen gueltigen Eintrag: $OUT" \
|| pass "kein Fehlalarm bei gueltigem Eintrag"
echo
echo "=== Bildsatz E: ein gewaehlter, aber fehlender Satz wird beim Umschalten UND im Status gemeldet ==="
SBX="$TMPROOT/bs5"
setup_sandbox "$SBX"
rm -rf "$SBX/images/migrate-wlan"
set +e
ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Umschalten gelingt trotzdem (der Zustand ist gueltig, nur die Bestueckung fehlt)" || fail "Exit-Code $RC"
echo "$ERR" | grep -qi "warnung" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR"
OUT=$(run_cli "$SBX" status 2>&1)
echo "$OUT" | grep -q "FEHLT" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT"
echo
echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ==="
SBX="$TMPROOT/bs6"
setup_sandbox "$SBX"
rm -f "$SBX/images/migrate-nocreds"/*.img
OUT=$(run_cli "$SBX" status 2>&1)
echo "$OUT" | grep -q "FEHLT" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"
echo
echo "=== Bildsatz G: 'migrate on' nennt den geltenden Bildsatz in der Einschaltmeldung ==="
# Er kann von vorletzter Woche stammen - wer einschaltet, muss ihn sehen.
SBX="$TMPROOT/bs7"
setup_sandbox "$SBX"
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
set +e
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
echo "$OUT" | grep -q "Bildsatz: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT"
echo
echo "=== Bildsatz H: die Warnung 'keine Migrationsimages' meint den GEWAEHLTEN Satz ==="
SBX="$TMPROOT/bs8"
setup_sandbox "$SBX"
rm -f "$SBX/images/migrate-wlan"/*.img # nur der andere Satz ist leer
set +e
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
set -e
echo "$ERR" | grep -q "keine Migrationsimages" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz"
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
set +e
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
set -e
echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."