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:
+139
-66
@@ -85,64 +85,70 @@ NL_SIGN="$ROOT/tools/mkimage/nl_sign.py"
|
||||
# Manifest blind ersetzen - sonst wuerden Images/Eintraege einer Variante verschwinden,
|
||||
# die dieser Lauf mangels Schluessel gar nicht neu bauen konnte (Fixrunde 1, C2/I1).
|
||||
MIGRATE_COMPLETE=0
|
||||
# Dasselbe fuer den Satz OHNE WLAN-Zugangsdaten (siehe unten): "vollstaendig" heisst hier
|
||||
# zusaetzlich, dass fuer JEDES Ziel ein Abzug aus build-nocreds/ vorlag - fehlt auch nur
|
||||
# einer, darf --delete auf dem Pi nicht laufen.
|
||||
NOCREDS_COMPLETE=0
|
||||
# Wo die Abzuege OHNE einkompilierte WLAN-Zugangsdaten liegen (tools/build_nocreds.sh,
|
||||
# dieselbe Variable wie dort und in tools/release.sh).
|
||||
NOCREDS_BUILD_REL=${NINLIGHT_NOCREDS_BUILD_REL:-build-nocreds}
|
||||
|
||||
if [ -d "$SIGN_DIR" ]; then
|
||||
HAVE_PROD=0
|
||||
HAVE_DEV=0
|
||||
if [ -f "$KEY_PROD" ]; then
|
||||
HAVE_PROD=1
|
||||
else
|
||||
echo "Hinweis: Produktionsschluessel fehlt - keine _prod.img-Migrationsimages" >&2
|
||||
fi
|
||||
if [ -f "$KEY_DEV" ]; then
|
||||
HAVE_DEV=1
|
||||
else
|
||||
echo "Hinweis: Entwicklungsschluessel fehlt - keine _dev.img-Migrationsimages" >&2
|
||||
fi
|
||||
if [ "$HAVE_PROD" -eq 1 ] || [ "$HAVE_DEV" -eq 1 ]; then
|
||||
# Erst HIER, nachdem feststeht dass ueberhaupt signiert wird, STAGE/migrate anlegen
|
||||
# und das fehlende _gz.img zum Abbruch machen (Fixrunde 1, C2+C3): ein
|
||||
# Schluesselverzeichnis ohne jeden Schluessel darf weder ein leeres migrate/
|
||||
# erzeugen (das wuerde ein vorhandenes migrate/ auf dem Pi mit --delete leerraeumen)
|
||||
# noch am Fehlen des _gz.img scheitern, wenn gar nicht signiert werden soll.
|
||||
mkdir -p "$STAGE/migrate"
|
||||
for f in "${FILES[@]}"; do
|
||||
base=$(basename "$f")
|
||||
target=${base#ninlight_}; target=${target%_ota.img}
|
||||
# Signiert wird IMMER das rohe _gz.img, niemals das fertige _ota.img: beide
|
||||
# belegen denselben 512-Byte-Block am Dateiende (Trailer bzw. Signatur), ein
|
||||
# signiertes _ota.img waere ein Image, das die alte Lampe ablehnt oder halb
|
||||
# annimmt.
|
||||
gz="$ROOT/build/$target/ninlight_${target}_gz.img"
|
||||
[ -f "$gz" ] || {
|
||||
echo "Fehler: $gz fehlt - kein Migrationsimage fuer $target moeglich" >&2; exit 1; }
|
||||
if [ "$HAVE_PROD" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_PROD" --in "$gz" --out "$STAGE/migrate/${target}_prod.img" \
|
||||
|| { echo "Fehler: Signieren von $target (prod) fehlgeschlagen" >&2; exit 1; }
|
||||
# Signiert einen kompletten Satz Migrationsimages und schreibt sein manifest.json.
|
||||
# $1 = Quellverzeichnis der Abzuege (build bzw. build-nocreds), absolut
|
||||
# $2 = Zielverzeichnis unterhalb von $STAGE (migrate-wlan bzw. migrate-nocreds)
|
||||
# $3 = "strict": ein fehlendes _gz.img ist ein Abbruch. "lax": es wird uebersprungen.
|
||||
# Gibt 0 zurueck, wenn der Satz VOLLSTAENDIG entstanden ist (kein Ziel uebersprungen),
|
||||
# sonst 1. Legt $STAGE/$2 nur an, wenn wirklich etwas darin landet - ein leeres
|
||||
# Verzeichnis wuerde auf dem Pi mit --delete einen vorhandenen Satz leerraeumen.
|
||||
sign_set() {
|
||||
local src_root=$1 stage_sub=$2 strictness=$3
|
||||
local complete=1 produced=0
|
||||
local f base target gz
|
||||
for f in "${FILES[@]}"; do
|
||||
base=$(basename "$f")
|
||||
target=${base#ninlight_}; target=${target%_ota.img}
|
||||
# Signiert wird IMMER das rohe _gz.img, niemals das fertige _ota.img: beide
|
||||
# belegen denselben 512-Byte-Block am Dateiende (Trailer bzw. Signatur), ein
|
||||
# signiertes _ota.img waere ein Image, das die alte Lampe ablehnt oder halb
|
||||
# annimmt.
|
||||
gz="$src_root/$target/ninlight_${target}_gz.img"
|
||||
if [ ! -f "$gz" ]; then
|
||||
if [ "$strictness" = "strict" ]; then
|
||||
echo "Fehler: $gz fehlt - kein Migrationsimage fuer $target moeglich" >&2; exit 1
|
||||
fi
|
||||
if [ "$HAVE_DEV" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_DEV" --in "$gz" --out "$STAGE/migrate/${target}_dev.img" \
|
||||
|| { echo "Fehler: Signieren von $target (dev) fehlgeschlagen" >&2; exit 1; }
|
||||
fi
|
||||
done
|
||||
if [ "$HAVE_PROD" -eq 1 ] && [ "$HAVE_DEV" -eq 1 ]; then
|
||||
MIGRATE_COMPLETE=1
|
||||
echo "Hinweis: $gz fehlt - $target fehlt im Bildsatz '$stage_sub'" >&2
|
||||
complete=0
|
||||
continue
|
||||
fi
|
||||
VERSION_ARGS=()
|
||||
for t in "${!NINL_VERSION[@]}"; do
|
||||
VERSION_ARGS+=("$t=${NINL_VERSION[$t]}")
|
||||
done
|
||||
# manifest.json aus den entstandenen Dateien - Groesse und sha256 werden hier neu
|
||||
# berechnet statt aus dem Signierschritt uebernommen, damit die Datei die Wahrheit
|
||||
# ueber das tatsaechlich abgelegte Image sagt. Form haelt sich an die Spec
|
||||
# (docs/superpowers/spec, Abschnitt 4) und damit an das, was server.py tatsaechlich
|
||||
# liest. Fixrunde 2, N2: "ninlight_version" steht PRO EINTRAG, nicht (mehr nur)
|
||||
# manifestweit - ein einziges Feld fuer das ganze Manifest war schon falsch, sobald
|
||||
# der Merge unten (bei nur einem Schluessel oder gezieltem Deploy) alte und neue
|
||||
# Eintraege mit unterschiedlichen Firmware-Versionen zusammenfuehrt. Das
|
||||
# manifestweite Feld bleibt nur als Bestandskompatibilitaet erhalten und wird nur
|
||||
# gesetzt, wenn wirklich alle Eintraege dieselbe Version tragen.
|
||||
python3 - "$STAGE/migrate" "${VERSION_ARGS[@]}" <<'PY'
|
||||
mkdir -p "$STAGE/$stage_sub"
|
||||
if [ "$HAVE_PROD" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_PROD" --in "$gz" --out "$STAGE/$stage_sub/${target}_prod.img" \
|
||||
|| { echo "Fehler: Signieren von $target (prod, $stage_sub) fehlgeschlagen" >&2; exit 1; }
|
||||
produced=1
|
||||
fi
|
||||
if [ "$HAVE_DEV" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_DEV" --in "$gz" --out "$STAGE/$stage_sub/${target}_dev.img" \
|
||||
|| { echo "Fehler: Signieren von $target (dev, $stage_sub) fehlgeschlagen" >&2; exit 1; }
|
||||
produced=1
|
||||
fi
|
||||
done
|
||||
[ "$produced" -eq 1 ] || return 1
|
||||
local VERSION_ARGS=()
|
||||
local t
|
||||
for t in "${!NINL_VERSION[@]}"; do
|
||||
VERSION_ARGS+=("$t=${NINL_VERSION[$t]}")
|
||||
done
|
||||
# manifest.json aus den entstandenen Dateien - Groesse und sha256 werden hier neu
|
||||
# berechnet statt aus dem Signierschritt uebernommen, damit die Datei die Wahrheit
|
||||
# ueber das tatsaechlich abgelegte Image sagt. Form haelt sich an die Spec
|
||||
# (docs/superpowers/spec, Abschnitt 4) und damit an das, was server.py tatsaechlich
|
||||
# liest. Fixrunde 2, N2: "ninlight_version" steht PRO EINTRAG, nicht (mehr nur)
|
||||
# manifestweit - ein einziges Feld fuer das ganze Manifest war schon falsch, sobald
|
||||
# der Merge unten (bei nur einem Schluessel oder gezieltem Deploy) alte und neue
|
||||
# Eintraege mit unterschiedlichen Firmware-Versionen zusammenfuehrt. Das
|
||||
# manifestweite Feld bleibt nur als Bestandskompatibilitaet erhalten und wird nur
|
||||
# gesetzt, wenn wirklich alle Eintraege dieselbe Version tragen.
|
||||
python3 - "$STAGE/$stage_sub" "${VERSION_ARGS[@]}" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -183,6 +189,51 @@ with open(os.path.join(migrate_dir, "manifest.json"), "w") as fh:
|
||||
json.dump(manifest, fh, indent=2, sort_keys=True)
|
||||
fh.write("\n")
|
||||
PY
|
||||
[ "$complete" -eq 1 ]
|
||||
}
|
||||
|
||||
HAVE_PROD=0
|
||||
HAVE_DEV=0
|
||||
if [ -d "$SIGN_DIR" ]; then
|
||||
if [ -f "$KEY_PROD" ]; then
|
||||
HAVE_PROD=1
|
||||
else
|
||||
echo "Hinweis: Produktionsschluessel fehlt - keine _prod.img-Migrationsimages" >&2
|
||||
fi
|
||||
if [ -f "$KEY_DEV" ]; then
|
||||
HAVE_DEV=1
|
||||
else
|
||||
echo "Hinweis: Entwicklungsschluessel fehlt - keine _dev.img-Migrationsimages" >&2
|
||||
fi
|
||||
if [ "$HAVE_PROD" -eq 1 ] || [ "$HAVE_DEV" -eq 1 ]; then
|
||||
BOTH_KEYS=0
|
||||
[ "$HAVE_PROD" -eq 1 ] && [ "$HAVE_DEV" -eq 1 ] && BOTH_KEYS=1
|
||||
|
||||
# ZWEI Bildsaetze, weil die WLAN-Zugangsdaten zur BAUZEIT in der Firmware stecken
|
||||
# und die Station beide vorhalten soll (der Bediener schaltet per
|
||||
# "ninlight-station bildsatz" bzw. am Taster um):
|
||||
#
|
||||
# migrate-wlan/ aus build/ - Lampe haengt sofort im eigenen WLAN
|
||||
# migrate-nocreds/ aus build-nocreds/ - Lampe startet im Onboarding-AP
|
||||
#
|
||||
# Der Satz aus build/ ist streng: dort liegen die _ota.img, die oben schon geprueft
|
||||
# wurden, ein fehlendes _gz.img daneben ist ein kaputter Baum. Der Satz aus
|
||||
# build-nocreds/ ist nachsichtig: er entsteht aus einem EIGENEN, optionalen Bau
|
||||
# (tools/build_nocreds.sh), und ein Deploy darf nicht daran scheitern, dass der noch
|
||||
# nicht gelaufen ist - dann fehlt der Satz eben, und die Station liefert in dieser
|
||||
# Stellung sichtbar nichts aus, statt heimlich den anderen Satz zu nehmen.
|
||||
echo " Bildsatz migrate-wlan (aus build/)"
|
||||
if sign_set "$ROOT/build" migrate-wlan strict; then
|
||||
[ "$BOTH_KEYS" -eq 1 ] && MIGRATE_COMPLETE=1
|
||||
fi
|
||||
echo " Bildsatz migrate-nocreds (aus $NOCREDS_BUILD_REL/)"
|
||||
if sign_set "$ROOT/$NOCREDS_BUILD_REL" migrate-nocreds lax; then
|
||||
[ "$BOTH_KEYS" -eq 1 ] && NOCREDS_COMPLETE=1
|
||||
else
|
||||
if [ ! -d "$STAGE/migrate-nocreds" ]; then
|
||||
echo "Hinweis: keine Abzuege unter $ROOT/$NOCREDS_BUILD_REL - der Bildsatz 'ohne-wlan' bleibt auf dem Pi unveraendert. Erzeugen mit tools/build_nocreds.sh." >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Hinweis: $SIGN_DIR enthaelt weder Produktions- noch Entwicklungsschluessel - keine Migrationsimages erzeugt" >&2
|
||||
fi
|
||||
@@ -216,10 +267,11 @@ rsync -a -c --delete --exclude=__pycache__ -e "$SSH_BIN" "$HERE/display/" "$HOST
|
||||
rsync -a -c --delete --exclude=__pycache__ -e "$SSH_BIN" "$HERE/button/" "$HOST:/tmp/ninlight-button/"
|
||||
# Pfade als Variablen mit Default: so kann ein Test die Installation gegen
|
||||
# ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen lassen.
|
||||
"$SSH_BIN" "$HOST" bash -s -- "$FULL_SYNC" "$MIGRATE_COMPLETE" <<'REMOTE'
|
||||
"$SSH_BIN" "$HOST" bash -s -- "$FULL_SYNC" "$MIGRATE_COMPLETE" "$NOCREDS_COMPLETE" <<'REMOTE'
|
||||
set -e
|
||||
FULL_SYNC=$1
|
||||
MIGRATE_COMPLETE=$2
|
||||
NOCREDS_COMPLETE=$3
|
||||
ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station}
|
||||
BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
|
||||
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
|
||||
@@ -249,26 +301,32 @@ if [ "$FULL_SYNC" -eq 1 ]; then
|
||||
# ausklammern, sonst wuerde --delete es leeren, wenn dieser Deploy ohne
|
||||
# Schluesselverzeichnis lief und /tmp/ninlight-images/migrate deshalb gar nicht
|
||||
# existiert.
|
||||
sudo rsync -a -c --delete --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
sudo rsync -a -c --delete --exclude=/migrate-wlan --exclude=/migrate-nocreds /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
else
|
||||
# Gezielter Deploy: nur die uebertragenen Dateien ersetzen/ergaenzen, alle
|
||||
# anderen Geraetetypen im Zielverzeichnis bleiben erhalten (kein --delete).
|
||||
echo " gezielter Deploy - andere Geraetetypen im Zielverzeichnis bleiben erhalten"
|
||||
sudo rsync -a -c --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
sudo rsync -a -c --exclude=/migrate-wlan --exclude=/migrate-nocreds /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
fi
|
||||
# $1 = Name des Bildsatzverzeichnisses, $2 = 1, wenn dieser Lauf den Satz VOLLSTAENDIG
|
||||
# beschreibt (beide Schluessel, alle Ziele) und --delete deshalb erlaubt ist.
|
||||
#
|
||||
# Migrationsimages getrennt behandeln: fehlt migrate/ in der Uebertragung (weil auf
|
||||
# dem Entwicklungsrechner kein Schluesselverzeichnis oder gar kein Schluessel
|
||||
# vorhanden war), darf ein vorhandenes migrate/ auf dem Pi NICHT geleert werden -
|
||||
# sonst zerstoert ein Deploy ohne Schluesselzugriff die Bestueckung frueherer Deploys
|
||||
# (derselbe Fehler wie frueher bei den _ota.img, siehe oben, nur diesmal beim Fehlen
|
||||
# statt beim Ersetzen).
|
||||
if [ -d /tmp/ninlight-images/migrate ]; then
|
||||
sudo install -d -m 0755 "$ROOT_DIR/images/migrate"
|
||||
if [ "$FULL_SYNC" -eq 1 ] && [ "$MIGRATE_COMPLETE" -eq 1 ]; then
|
||||
sync_migrate_set() {
|
||||
set=$1
|
||||
complete=$2
|
||||
if [ -d "/tmp/ninlight-images/$set" ]; then
|
||||
sudo install -d -m 0755 "$ROOT_DIR/images/$set"
|
||||
if [ "$FULL_SYNC" -eq 1 ] && [ "$complete" -eq 1 ]; then
|
||||
# Nur wenn dieser Lauf mit BEIDEN Schluesseln signiert hat, beschreibt
|
||||
# STAGE/migrate den vollstaendigen Soll-Zustand - erst dann darf --delete
|
||||
# ueberzaehlige Dateien wegraeumen und manifest.json blind ersetzt werden.
|
||||
sudo rsync -a -c --delete /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
||||
sudo rsync -a -c --delete "/tmp/ninlight-images/$set/" "$ROOT_DIR/images/$set/"
|
||||
else
|
||||
# Gezielter Deploy ODER ein Voll-Deploy mit nur einem der beiden Schluessel:
|
||||
# in beiden Faellen kennt dieser Lauf nur einen Teil dessen, was auf dem Pi liegen
|
||||
@@ -278,9 +336,9 @@ if [ -d /tmp/ninlight-images/migrate ]; then
|
||||
# sondern mit dem, was schon da ist, zusammengefuehrt (Fixrunde 1, I1 - sonst
|
||||
# verschwinden die Eintraege aller Ziele/Varianten, die dieser Lauf nicht neu
|
||||
# gebaut hat, obwohl ihre Images unveraendert liegen bleiben).
|
||||
sudo rsync -a -c --exclude=/manifest.json /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
||||
if [ -f /tmp/ninlight-images/migrate/manifest.json ]; then
|
||||
sudo python3 - "$ROOT_DIR/images/migrate/manifest.json" /tmp/ninlight-images/migrate/manifest.json <<'MERGE'
|
||||
sudo rsync -a -c --exclude=/manifest.json "/tmp/ninlight-images/$set/" "$ROOT_DIR/images/$set/"
|
||||
if [ -f "/tmp/ninlight-images/$set/manifest.json" ]; then
|
||||
sudo python3 - "$ROOT_DIR/images/$set/manifest.json" "/tmp/ninlight-images/$set/manifest.json" <<'MERGE'
|
||||
import json
|
||||
import sys
|
||||
|
||||
@@ -340,7 +398,22 @@ else
|
||||
# sich von hier aus unterscheiden - beide Faelle liefern ein leeres migrate/ von
|
||||
# diesem Rechner. Die lokale Meldung (deploy.sh, "Hinweis: ...") ist bereits genau;
|
||||
# diese hier behauptet absichtlich keinen bestimmten Grund (Fixrunde 2, Kleinigkeit).
|
||||
echo " keine Migrationsimages von diesem Rechner - migrate/ auf dem Pi bleibt unangetastet"
|
||||
echo " keine Migrationsimages fuer $set von diesem Rechner - $set/ auf dem Pi bleibt unangetastet"
|
||||
fi
|
||||
}
|
||||
|
||||
# Beide Bildsaetze. Sie sind voneinander unabhaengig: liegt nur einer von diesem Rechner
|
||||
# vor (z.B. weil tools/build_nocreds.sh noch nicht gelaufen ist), bleibt der andere auf
|
||||
# dem Pi unveraendert - der Umschalter zeigt dann eben auf einen Satz, der fehlt, und die
|
||||
# Station liefert in dieser Stellung sichtbar nichts aus.
|
||||
sync_migrate_set migrate-wlan "$MIGRATE_COMPLETE"
|
||||
sync_migrate_set migrate-nocreds "$NOCREDS_COMPLETE"
|
||||
# Altbestand: bis zum Umschalter hiess das einzige Verzeichnis "migrate" und enthielt die
|
||||
# Abzuege MIT WLAN-Zugangsdaten. Es wird von niemandem mehr gelesen; liegen bleiben darf
|
||||
# es trotzdem nicht, sonst glaubt der naechste Mensch am Pi, es sei der gueltige Satz.
|
||||
if [ -d "$ROOT_DIR/images/migrate" ]; then
|
||||
sudo rm -rf "$ROOT_DIR/images/migrate"
|
||||
echo " Altbestand entfernt: images/migrate (heisst jetzt images/migrate-wlan)"
|
||||
fi
|
||||
sudo install -m 0755 /tmp/server.py "$ROOT_DIR/server.py"
|
||||
sudo install -m 0644 /tmp/products.py "$ROOT_DIR/products.py"
|
||||
|
||||
Reference in New Issue
Block a user