Files
ninlight-w600/tools/station/tests/test_deploy.sh
Michael Sauer c482194503 fix(station): deploy.sh prueft Bildsatz 'ohne-wlan' auf Zugangsdaten
release.sh prueft sein fertiges Archiv binaer gegen die Werte aus
config.local.cmake und bricht ab, wenn es etwas findet. deploy.sh signierte
dagegen ungeprueft, was in build-nocreds/ lag - und genau dieser Satz ist die
sichere Vorgabe, die im Automatikmodus an fremde Lampen in Funkreichweite geht.

Der Weg dorthin ist Bedienfehler, nicht Programmfehler: ein von Hand
konfiguriertes build-nocreds/<ziel>, ein gesetztes
NINLIGHT_NOCREDS_BUILD_REL=build. Das Ergebnis waere, dass das Panel
"ohne WLAN" anzeigt, waehrend das WLAN-Passwort des Bedieners unwiderruflich
in fremde Lampen wandert.

Derselbe Pruefkoerper wie in release.sh, angewandt auf $STAGE/migrate-nocreds
und VOR dem Uebertragen - bei einem Treffer geht kein Byte an den Pi. Szenario
33 stellt den undichten Abzug nach (mit Gegenprobe, dass der Abzug die
Zugangsdaten wirklich traegt), Szenario 34 grenzt ab, dass migrate-wlan sie
weiterhin tragen darf.

Mutationsproben: Pruefblock entfernt = 6 Zusagen rot (die Zugangsdaten landen
beim Ziel); Pruefung auf den ganzen STAGE ausgeweitet = 4 Zusagen rot.
2026-09-15 04:05:03 +02:00

1429 lines
66 KiB
Bash
Executable File

#!/usr/bin/env bash
# Stub-Tests fuer deploy.sh - ohne Pi ausfuehrbar.
#
# ssh/rsync werden im PATH durch Stubs ersetzt: ssh fuehrt das per Heredoc
# uebergebene Remote-Skript lokal aus (statt es an einen echten Pi zu
# schicken), rsync uebersetzt "host:pfad"-Ziele auf den blossen lokalen Pfad
# und ruft dann den echten rsync auf. So laeuft die komplette Logik von
# deploy.sh - inklusive des Installationsschritts, der normalerweise per SSH
# auf dem Pi passiert - tatsaechlich, aber vollstaendig gegen ein Sandbox-
# Verzeichnis (NINLIGHT_STATION_ROOT/…) statt gegen echte Systempfade.
#
# Aufruf: tools/station/tests/test_deploy.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
STATION_DIR=$(cd "$HERE/.." && pwd)
MKIMAGE_DIR=$(cd "$STATION_DIR/../mkimage" && pwd)
REAL_RSYNC=$(command -v rsync)
# Sicherheitsnetz (Fixrunde 1, C4): NINLIGHT_SIGN_DIR ist genau die Variable, mit der
# man auf ~/workspace/fw-sign zeigt - und auf diesem Entwicklungsrechner liegen dort
# ECHTE Signierschluessel. Ist sie im aufrufenden Profil oder fuer einen frueheren
# manuellen Deploy exportiert, darf sie unter keinen Umstaenden in diese Suite
# durchsickern. Deshalb: hier explizit loeschen, und run_deploy() unten liest sie NIE
# wieder - jedes Szenario, das echte Testschluessel braucht, uebergibt sie ueber die
# separate Variable TEST_SIGN_DIR (die deploy.sh selbst gar nicht kennt und die daher
# auch nicht zufaellig aus der Umgebung vorbelegt sein kann).
unset NINLIGHT_SIGN_DIR
unset TEST_SIGN_DIR
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
# --- Sandbox-Repo aufbauen: tools/station/deploy.sh + tools/mkimage/nl_mkimage.py + build/ ---
setup_sandbox() {
local sandbox=$1
rm -rf "$sandbox"
mkdir -p "$sandbox/tools/station" "$sandbox/tools/mkimage" "$sandbox/build"
cp "$STATION_DIR/deploy.sh" "$sandbox/tools/station/deploy.sh"
chmod +x "$sandbox/tools/station/deploy.sh"
# Das ECHTE Signierwerkzeug wird gebraucht (nicht gestubbt): die Migrations-Szenarien
# pruefen echtes Signieren/die echten Formatchecks (Mindestlaenge, kein Trailer, 4096
# Bit). Nur die Schluessel selbst sind Wegwerf-Testschluessel, nie echte.
cp "$MKIMAGE_DIR/nl_sign.py" "$sandbox/tools/mkimage/nl_sign.py"
# Inhalt irrelevant, die Dateien muessen nur existieren (werden "uebertragen").
echo "# dummy" >"$sandbox/tools/station/server.py"
echo "# dummy" >"$sandbox/tools/station/products.py"
echo "# dummy" >"$sandbox/tools/station/ninlight-station"
echo "# dummy" >"$sandbox/tools/station/ninlight-station.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-display.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-button.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-poweroff.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-reboot.service"
# display/ wird ECHT kopiert, nicht durch Attrappen ersetzt: die Zusage aus Task 6b
# ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis
# HERKUNFT.md mit auf den Pi gehen. Mit selbst erfundenen Dateinamen im Sandbox-Repo
# wuerde der Test genau das nicht mehr zeigen - er pruefte nur noch sich selbst.
cp -R "$STATION_DIR/display" "$sandbox/tools/station/display"
rm -rf "$sandbox/tools/station/display/__pycache__"
# Ein __pycache__ mit .pyc dieses Rechners (andere Python-Version als der Pi) - es
# darf nicht mit uebertragen werden.
mkdir -p "$sandbox/tools/station/display/__pycache__"
echo "pyc-muell" >"$sandbox/tools/station/display/__pycache__/render.cpython-999.pyc"
# button/ ebenso echt kopiert, gleiches Argument wie bei display/ oben.
cp -R "$STATION_DIR/button" "$sandbox/tools/station/button"
rm -rf "$sandbox/tools/station/button/__pycache__"
mkdir -p "$sandbox/tools/station/button/__pycache__"
echo "pyc-muell" >"$sandbox/tools/station/button/__pycache__/button.cpython-999.pyc"
# Fake nl_mkimage.py --verify: verhaelt sich wie das echte Tool (eine Zeile
# "<geraet> <version> <bytes>" auf stdout bei Erfolg, Exit 0; eine Zeile auf
# stderr bei Fehlschlag, Exit 1), aber steuerbar per Umgebungsvariable
# MKIMAGE_FAIL_SUBSTR: enthaelt der zu pruefende Dateiname diesen Teilstring,
# schlaegt die Pruefung fehl.
cat >"$sandbox/tools/mkimage/nl_mkimage.py" <<'PY'
import os
import sys
args = sys.argv[1:]
f = args[args.index("--verify") + 1]
target = args[args.index("--expect-device") + 1]
fail_substr = os.environ.get("MKIMAGE_FAIL_SUBSTR", "")
if fail_substr and fail_substr in f:
print(f"Fehler: {f} besteht die Trailer-Pruefung nicht (Test-Stub)", file=sys.stderr)
sys.exit(1)
size = os.path.getsize(f)
print(f"{target} 1.0.0 {size}")
PY
}
# --- Stub-Kommandos fuer ssh/rsync im PATH ---
setup_stubbin() {
local bindir=$1 log=$2
mkdir -p "$bindir"
cat >"$bindir/ssh" <<EOF
#!/usr/bin/env bash
echo "STUB ssh \$*" >>"$log"
host=\$1; shift
"\$@"
EOF
cat >"$bindir/rsync" <<EOF
#!/usr/bin/env bash
echo "STUB rsync \$*" >>"$log"
args=()
for a in "\$@"; do
case "\$a" in
-*) args+=("\$a") ;;
*:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;;
esac
done
exec "$REAL_RSYNC" "\${args[@]}"
EOF
cat >"$bindir/scp" <<EOF
#!/usr/bin/env bash
echo "STUB scp \$* - deploy.sh sollte scp nicht aufrufen" >>"$log"
exit 1
EOF
cat >"$bindir/sudo" <<EOF
#!/usr/bin/env bash
echo "STUB sudo \$*" >>"$log"
exec "\$@"
EOF
# Der Stub unterscheidet nach Unit: deploy.sh behandelt den Anzeigedienst
# ausdruecklich anders als den Hauptdienst (ein fehlgeschlagener Neustart der
# Anzeige ist KEIN Deploy-Fehler). Mit einem Schalter fuer beide Units zusammen
# liesse sich genau dieser Unterschied nicht pruefen.
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
case "\$*" in
*ninlight-display*)
case "\$1" in
is-active) exit "\${FAKE_DISPLAY_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_DISPLAY_RESTART_EXIT:-0}" ;;
*) exit 0 ;;
esac
;;
*ninlight-button*)
case "\$1" in
is-active) exit "\${FAKE_BUTTON_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_BUTTON_RESTART_EXIT:-0}" ;;
*) exit 0 ;;
esac
;;
esac
case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;;
*) exit 0 ;;
esac
EOF
chmod +x "$bindir"/ssh "$bindir"/rsync "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl
}
run_deploy() {
# Fuehrt deploy.sh mit stubbtem PATH aus; Ausgabe landet in $OUT, Exit-Code in $?.
local sandbox=$1; shift
# Auf dem echten Pi existieren /usr/local/bin und /etc/systemd/system schon;
# in der Sandbox muessen sie fuer "install" eigens angelegt werden.
mkdir -p "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system"
# NINLIGHT_SIGN_DIR wird hier HART auf einen lokalen Wert gesetzt, niemals ueber
# "${NINLIGHT_SIGN_DIR:-default}" aus der geerbten Umgebung uebernommen (das war der
# C4-Fehler aus Fixrunde 1: ein Fallback greift nur, wenn die Variable NICHT gesetzt
# ist - war sie es, z.B. weil ein Profil sie dauerhaft auf ~/workspace/fw-sign zeigen
# laesst, haette der Fallback sie unveraendert durchgereicht). Die einzige Variable,
# die ein Szenario mit echten Testschluesseln setzen darf, ist TEST_SIGN_DIR - eine
# Variable, die deploy.sh selbst gar nicht kennt und die deshalb nie zufaellig von
# aussen vorbelegt sein kann wie NINLIGHT_SIGN_DIR es auf einem Entwicklungsrechner
# ist. Ohne TEST_SIGN_DIR zeigt sie auf einen Pfad, der in der Sandbox garantiert
# nicht existiert.
local sign_dir="${TEST_SIGN_DIR:-$sandbox/no-such-sign-dir}"
PATH="$STUBBIN:$PATH" \
NINLIGHT_SSH_BIN="$STUBBIN/ssh" \
NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \
NINLIGHT_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" \
NINLIGHT_BUTTON_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-button.service" \
NINLIGHT_POWEROFF_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-poweroff.service" \
NINLIGHT_REBOOT_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-reboot.service" \
NINLIGHT_SIGN_DIR="$sign_dir" \
"$sandbox/tools/station/deploy.sh" "$@"
}
# --- Hilfsmittel fuer die Migrations-Szenarien ---
# Erzeugt Wegwerf-Testschluessel (nl_sign.generate_test_key(), nie echte Schluessel,
# nie geschrieben ausser in dieses Sandbox-Verzeichnis) unter $keydir. with_prod/
# with_dev steuern, welche der beiden Dateien entsteht.
make_test_keys() {
local keydir=$1 with_prod=$2 with_dev=$3
mkdir -p "$keydir/QCX_Device_CA"
PYTHONPATH="$MKIMAGE_DIR" python3 - "$keydir" "$with_prod" "$with_dev" <<'PY'
import sys
import nl_sign
keydir, with_prod, with_dev = sys.argv[1], sys.argv[2] == "1", sys.argv[3] == "1"
if with_prod:
k = nl_sign.generate_test_key()
with open(keydir + "/QCX_Device_CA/qcx_firmware_signing_key.pem", "wb") as fh:
fh.write(k.private_pem)
if with_dev:
k = nl_sign.generate_test_key()
with open(keydir + "/fw_sign.pem", "wb") as fh:
fh.write(k.private_pem)
PY
}
# Erzeugt eine gueltige rohe _gz.img-Attrappe: >= 1024 Byte, durch 4 teilbar, ohne
# NINL-Trailer - genau das, was nl_sign.py als Eingabe akzeptiert.
make_gz_file() {
local path=$1 fill=${2:-A}
python3 -c "
import sys
path, fill = sys.argv[1], sys.argv[2].encode()
data = bytearray((fill * 2048)[:2048])
data[-512:-508] = b'\\x00\\x00\\x00\\x00' # nie zufaellig wie 'NINL' aussehen
with open(path, 'wb') as fh:
fh.write(bytes(data))
" "$path" "$fill"
}
# Erzeugt eine _ota.img-Attrappe mit ANDEREM Inhalt als das zugehoerige _gz.img und
# einem echten NINL-Trailer - damit ein versehentliches Signieren des _ota.img statt
# des _gz.img (Fall 7) zweifelsfrei auffliegt: weder Inhalt noch Groesse passen dann.
make_ota_file() {
local path=$1
python3 -c "
import sys
path = sys.argv[1]
body = b'B' * 2048
trailer = bytearray(b'\\xff' * 512)
trailer[0:4] = b'NINL'
with open(path, 'wb') as fh:
fh.write(body + bytes(trailer))
" "$path"
}
# Wegwerf-Zugangsdaten fuer die Pruefung des Bildsatzes "ohne-wlan" - niemals die
# echten aus dem Repo. Dieselbe Rolle wie SECRET_* in tools/tests/test_release.sh.
SECRET_SSID="TestWLAN-9c2e-geheim"
SECRET_PASS="testpasswort-9c2e"
SECRET_TOKEN="0badc0de9c2e0badc0de9c2e"
# Legt config.local.cmake im Sandbox-Repo an - genau die Datei, gegen die deploy.sh den
# Bildsatz "ohne-wlan" prueft.
write_local_cmake() {
local sandbox=$1
cat >"$sandbox/config.local.cmake" <<EOF
set(NINLIGHT_WIFI_SSID "$SECRET_SSID")
set(NINLIGHT_WIFI_PASSWORD "$SECRET_PASS")
set(NINLIGHT_TOKEN "$SECRET_TOKEN")
EOF
}
# Wie make_gz_file, aber mit einkompilierten WLAN-Zugangsdaten - das, was ein Abzug aus
# einem falsch konfigurierten build-nocreds/ (oder aus NINLIGHT_NOCREDS_BUILD_REL=build)
# wirklich enthielte. Der "Compiler" dieser Suite, analog zum Docker-Stub in
# test_release.sh: nur weil er die Werte tatsaechlich einbaut, sagt ein bestandener
# Pruefdurchlauf ueberhaupt etwas.
make_gz_file_with_creds() {
local path=$1 fill=${2:-A}
SSID="$SECRET_SSID" PASS="$SECRET_PASS" TOKEN="$SECRET_TOKEN" python3 -c "
import os, sys
path, fill = sys.argv[1], sys.argv[2].encode()
creds = (os.environ['SSID'] + '\\0' + os.environ['PASS'] + '\\0'
+ os.environ['TOKEN'] + '\\0').encode()
data = bytearray((fill * 2048)[:2048])
data[64:64 + len(creds)] = creds
data[-512:-508] = b'\\x00\\x00\\x00\\x00' # nie zufaellig wie 'NINL' aussehen
with open(path, 'wb') as fh:
fh.write(bytes(data))
" "$path" "$fill"
}
# Sucht eine Zeichenkette BINAER in jeder Datei unterhalb eines Verzeichnisses.
# Rueckgabe 0 = gefunden. Wie contains_secrets() in test_release.sh.
tree_contains() {
local dir=$1 needle=$2
NEEDLE="$needle" python3 - "$dir" <<'PY'
import os
import sys
needle = os.environ["NEEDLE"].encode()
for root, _dirs, files in os.walk(sys.argv[1]):
for fn in files:
with open(os.path.join(root, fn), "rb") as fh:
if needle in fh.read():
sys.exit(0)
sys.exit(1)
PY
}
echo "=== Szenario 1: Image besteht die Trailer-Pruefung nicht -> nichts uebertragen ==="
SANDBOX="$TMPROOT/s1"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"; SANDBOX_STATE="$TMPROOT/s1-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "dummy" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(MKIMAGE_FAIL_SUBSTR=foo run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if echo "$OUT" | grep -q "besteht die Trailer-Pruefung nicht"; then
pass "Fehlermeldung zur Trailer-Pruefung vorhanden"
else
fail "Fehlermeldung zur Trailer-Pruefung fehlt"
fi
if ! grep -q "STUB ssh\|STUB rsync\|STUB scp" "$LOG"; then
pass "kein ssh/rsync/scp aufgerufen - nichts wurde uebertragen"
else
fail "ssh/rsync/scp wurde trotz fehlgeschlagener Pruefung aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 2: keine Images unter build/ gefunden ==="
SANDBOX="$TMPROOT/s2"; STUBBIN="$TMPROOT/bin2"; LOG="$TMPROOT/s2.log"; SANDBOX_STATE="$TMPROOT/s2-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if echo "$OUT" | grep -q "keine Images unter"; then
pass "Fehlermeldung zu fehlenden Images vorhanden"
else
fail "Fehlermeldung zu fehlenden Images fehlt"
fi
if ! grep -q "STUB" "$LOG" 2>/dev/null; then
pass "kein Transfer-Kommando aufgerufen"
else
fail "ein Transfer-Kommando wurde trotz fehlender Images aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 3: gezielter Deploy eines Geraetetyps laesst andere Images auf dem Ziel unangetastet (Critical 2) ==="
SANDBOX="$TMPROOT/s3"; STUBBIN="$TMPROOT/bin3"; LOG="$TMPROOT/s3.log"; SANDBOX_STATE="$TMPROOT/s3-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
echo "bar-image" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
# Zielverzeichnis simuliert einen vorherigen Voll-Deploy: Images fuer foo UND bar liegen schon da.
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "foo-image-alt" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img"
echo "bar-image" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "andere Geraetetypen im Zielverzeichnis bleiben erhalten"; then
pass "Ausgabe kuendigt gezielten Deploy ohne --delete an"
else
fail "Ausgabe erwaehnt den gezielten Deploy nicht"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img" ]; then
pass "Image des NICHT ausgewaehlten Geraetetyps (bar) blieb erhalten"
else
fail "Image des nicht ausgewaehlten Geraetetyps (bar) wurde geloescht - Critical 2 nicht behoben"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img")" = "foo-image-neu" ]; then
pass "Image des ausgewaehlten Geraetetyps (foo) wurde aktualisiert"
else
fail "Image des ausgewaehlten Geraetetyps (foo) wurde nicht aktualisiert"
fi
echo
echo "=== Szenario 4: Voll-Deploy (kein target) entfernt veraltete Images - zur Abgrenzung von Szenario 3 ==="
SANDBOX="$TMPROOT/s4"; STUBBIN="$TMPROOT/bin4"; LOG="$TMPROOT/s4.log"; SANDBOX_STATE="$TMPROOT/s4-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then
pass "veraltetes Image wurde beim Voll-Deploy entfernt (--delete wirkt weiterhin, wenn gewollt)"
else
fail "veraltetes Image wurde beim Voll-Deploy NICHT entfernt"
fi
echo
echo "=== Szenario 5: Dienst laeuft, Neustart nach dem Deploy schlaegt fehl -> Exit != 0 (Minor) ==="
SANDBOX="$TMPROOT/s5"; STUBBIN="$TMPROOT/bin5"; LOG="$TMPROOT/s5.log"; SANDBOX_STATE="$TMPROOT/s5-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
# Ueber run_deploy() statt eines eigenen Aufrufs (Fixrunde 1: der vorherige direkte
# Aufruf hier setzte NINLIGHT_SIGN_DIR zwar auch hart, ging aber am gemeinsamen Muster
# vorbei - ein Praezedenzfall, den ein spaeterer Umbau leicht wieder falsch abschreiben
# koennte). FAKE_SYSTEMCTL_* werden als gewoehnliche vorangestellte Umgebungsvariablen
# durchgereicht, run_deploy() ist eine Funktion und sieht sie genauso wie jeder andere
# Befehl.
OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0, wenn der Neustart fehlschlaegt ($RC)"; else fail "Exit-Code war 0, obwohl der Neustart fehlschlug"; fi
if echo "$OUT" | grep -q "Neustart nach dem Deploy ist aber fehlgeschlagen"; then
pass "Fehlermeldung unterscheidet Neustart-Fehlschlag von 'Dienst laeuft nicht'"
else
fail "Fehlermeldung fehlt oder ist mehrdeutig"
fi
if echo "$OUT" | grep -q "Dienst laeuft nicht"; then
fail "faelschlich 'Dienst laeuft nicht' gemeldet, obwohl der Dienst lief"
else
pass "'Dienst laeuft nicht' wird nicht faelschlich gemeldet"
fi
echo
echo "=== Szenario 6: deploy.sh/setup_pi.sh nutzen ohne Override /usr/bin/ssh (bzw. scp) ==="
# Die anderen Szenarien laufen mit NINLIGHT_SSH_BIN auf einen Stub umgebogen - das prueft die
# Logik, aber nicht mehr, WELCHEN ssh das Skript ohne Override tatsaechlich waehlt. Der Nutzer
# verlangt ausdruecklich /usr/bin/ssh und /usr/bin/scp statt einer Variante im PATH; das hier
# ist die Absicherung dagegen, dass der Default beim naechsten Umbau stillschweigend zurueck
# auf ein PATH-"ssh" rutscht.
if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/deploy.sh"; then
pass "deploy.sh: Default fuer SSH_BIN ist /usr/bin/ssh"
else
fail "deploy.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh"
fi
if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/setup_pi.sh"; then
pass "setup_pi.sh: Default fuer SSH_BIN ist /usr/bin/ssh"
else
fail "setup_pi.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh"
fi
if grep -q 'SCP_BIN=\${NINLIGHT_SCP_BIN:-/usr/bin/scp}' "$STATION_DIR/setup_pi.sh"; then
pass "setup_pi.sh: Default fuer SCP_BIN ist /usr/bin/scp"
else
fail "setup_pi.sh: Default fuer SCP_BIN ist nicht mehr /usr/bin/scp"
fi
echo
echo "=== Szenario 7: Voll-Deploy mit zwei Build-Verzeichnissen fuer denselben Dateinamen -> Abbruch (B1) ==="
SANDBOX="$TMPROOT/s7"; STUBBIN="$TMPROOT/bin7"; LOG="$TMPROOT/s7.log"; SANDBOX_STATE="$TMPROOT/s7-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/evk" "$SANDBOX/build/evk-onb"
echo "evk-mit-wlan" >"$SANDBOX/build/evk/ninlight_evk_ota.img"
echo "evk-ohne-wlan" >"$SANDBOX/build/evk-onb/ninlight_evk_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0, obwohl zwei Fundorte denselben Dateinamen liefern"; fi
if echo "$OUT" | grep -q "ninlight_evk_ota.img mehrfach gefunden"; then
pass "Fehlermeldung nennt den doppelten Dateinamen"
else
fail "Fehlermeldung zum doppelten Dateinamen fehlt: $OUT"
fi
if echo "$OUT" | grep -q "build/evk/ninlight_evk_ota.img" && echo "$OUT" | grep -q "build/evk-onb/ninlight_evk_ota.img"; then
pass "Fehlermeldung nennt beide Fundorte"
else
fail "Fehlermeldung nennt nicht beide Fundorte: $OUT"
fi
if ! grep -q "STUB" "$LOG" 2>/dev/null; then
pass "kein Transfer-Kommando aufgerufen - nichts wurde stillschweigend uebertragen"
else
fail "ein Transfer-Kommando wurde trotz doppeltem Dateinamen aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 8: Voll-Deploy zeigt vor dem Loeschen, welche Images verschwinden (B2) ==="
SANDBOX="$TMPROOT/s8"; STUBBIN="$TMPROOT/bin8"; LOG="$TMPROOT/s8.log"; SANDBOX_STATE="$TMPROOT/s8-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img"
echo "auch-veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_e14_cw_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "folgende Images werden entfernt"; then
pass "Ausgabe kuendigt die zu entfernenden Images an, bevor geloescht wird"
else
fail "Ausgabe nennt die zu entfernenden Images nicht: $OUT"
fi
if echo "$OUT" | grep -q "ninlight_veraltet_ota.img" && echo "$OUT" | grep -q "ninlight_e14_cw_ota.img"; then
pass "beide zu entfernenden Dateien werden namentlich genannt"
else
fail "nicht alle zu entfernenden Dateien werden genannt: $OUT"
fi
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then
pass "veraltetes Image wurde nach der Ankuendigung tatsaechlich entfernt"
else
fail "veraltetes Image wurde trotz Ankuendigung nicht entfernt"
fi
echo
echo "=== Szenario 9: Voll-Deploy ohne verwaiste Images meldet das statt zu schweigen (B2) ==="
SANDBOX="$TMPROOT/s9"; STUBBIN="$TMPROOT/bin9"; LOG="$TMPROOT/s9.log"; SANDBOX_STATE="$TMPROOT/s9-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "keine Images werden entfernt"; then
pass "Ausgabe meldet explizit, dass nichts entfernt wird"
else
fail "Ausgabe meldet nicht, dass nichts entfernt wird: $OUT"
fi
echo
echo "=== Vorbereitung: Wegwerf-Testschluessel fuer die Migrations-Szenarien ==="
# Nur ueber nl_sign.generate_test_key() erzeugt, nie ausserhalb dieses Testlaufs
# verwendet und nie in ein Repository- oder Log-File jenseits dieser TMPROOT-Sandbox
# geschrieben.
KEYDIR_FULL="$TMPROOT/keys-full"
make_test_keys "$KEYDIR_FULL" 1 1
KEYDIR_DEV_ONLY="$TMPROOT/keys-dev-only"
make_test_keys "$KEYDIR_DEV_ONLY" 0 1
pass "Testschluessel erzeugt"
echo
echo "=== Szenario 10: Mit vorhandenem Schluesselverzeichnis entstehen zwei Migrationsimages je Ziel (Fall 1) ==="
SANDBOX="$TMPROOT/s10"; STUBBIN="$TMPROOT/bin10"; LOG="$TMPROOT/s10.log"; SANDBOX_STATE="$TMPROOT/s10-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
GZ_SIZE=$(wc -c <"$SANDBOX/build/foo/ninlight_foo_gz.img" | tr -d ' ')
PROD_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
if [ -f "$PROD_IMG" ] && [ -f "$DEV_IMG" ]; then
pass "beide Migrationsimages (prod, dev) wurden angelegt"
else
fail "mindestens ein Migrationsimage fehlt: $OUT"
fi
for img in "$PROD_IMG" "$DEV_IMG"; do
[ -f "$img" ] || continue
size=$(wc -c <"$img" | tr -d ' ')
expected=$((GZ_SIZE + 512))
if [ "$size" -eq "$expected" ]; then
pass "$(basename "$img") ist genau 512 Byte groesser als das _gz.img ($size Byte)"
else
fail "$(basename "$img") hat $size Byte, erwartet $expected"
fi
done
echo
echo "=== Szenario 11: manifest.json listet jedes ausgelieferte Migrationsimage (Fall 2) ==="
SANDBOX="$TMPROOT/s11"; STUBBIN="$TMPROOT/bin11"; LOG="$TMPROOT/s11.log"; SANDBOX_STATE="$TMPROOT/s11-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then
pass "manifest.json wurde angelegt"
else
fail "manifest.json fehlt oder Exit-Code $RC: $OUT"
fi
set +e
# Form haelt sich an die Spec (docs/superpowers/spec Abschnitt 4) und an das, was
# server.py tatsaechlich liest. Seit Fixrunde 2 (N2) traegt "ninlight_version" PRO
# EINTRAG die Version - hier stimmen beide Eintraege ueberein, deshalb steht das
# manifestweite Feld zusaetzlich als Bequemlichkeit oben. Ein reines json.load haette
# eine falsche Verschachtelung nicht gemerkt (Fixrunde 1, C1); die naechste Pruefung
# (Szenario 20) schickt dieselbe Datei zusaetzlich durch den echten Server-Leser.
CHECK=$(python3 -c "
import json
with open('$MANIFEST') as fh:
data = json.load(fh)
entries = data.get('images', [])
by_variant = {e.get('variant'): e for e in entries if e.get('target') == 'foo'}
ok = (
isinstance(data.get('built'), str) and len(data['built']) > 0
and data.get('ninlight_version') == '1.0.0'
and len(entries) == 2
and set(by_variant) == {'prod', 'dev'}
and all(e.get('file') in ('foo_prod.img', 'foo_dev.img') for e in entries)
and all(isinstance(e.get('bytes'), int) and e['bytes'] > 0 for e in entries)
and all(isinstance(e.get('sha256'), str) and len(e['sha256']) == 64 for e in entries)
and all(e.get('ninlight_version') == '1.0.0' for e in entries)
)
print('OK' if ok else 'FAIL: ' + json.dumps(data))
" 2>&1)
set -e
if [ "$CHECK" = "OK" ]; then
pass "manifest.json traegt built/ninlight_version oben (Bequemlichkeit) und target/variant/file/bytes/sha256/ninlight_version je Eintrag"
else
fail "manifest.json-Inhalt unerwartet: $CHECK"
fi
echo
echo "=== Szenario 12: Ohne NINLIGHT_SIGN_DIR entfallen die Migrationsimages, das Rettungsimage kommt trotzdem an (Fall 3) ==="
SANDBOX="$TMPROOT/s12"; STUBBIN="$TMPROOT/bin12"; LOG="$TMPROOT/s12.log"; SANDBOX_STATE="$TMPROOT/s12-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; then
pass "Rettungsimage (_ota.img) wurde trotzdem ausgeliefert"
else
fail "Rettungsimage fehlt: $OUT"
fi
if [ ! -d "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" ] \
|| [ -z "$(ls -A "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" 2>/dev/null)" ]; then
pass "keine Migrationsimages entstanden"
else
fail "Migrationsimages entstanden, obwohl kein Schluesselverzeichnis vorhanden war"
fi
if echo "$OUT" | grep -q "Schluesselverzeichnis"; then
pass "Meldung ueber fehlendes Schluesselverzeichnis vorhanden"
else
fail "keine Meldung ueber das fehlende Schluesselverzeichnis: $OUT"
fi
echo
echo "=== Szenario 13: Schluesselverzeichnis ohne Produktionsschluessel - nur dev-Images entstehen (Fall 4) ==="
SANDBOX="$TMPROOT/s13"; STUBBIN="$TMPROOT/bin13"; LOG="$TMPROOT/s13.log"; SANDBOX_STATE="$TMPROOT/s13-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
pass "dev-Migrationsimage wurde angelegt"
else
fail "dev-Migrationsimage fehlt: $OUT"
fi
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" ]; then
pass "prod-Migrationsimage entfiel wie erwartet"
else
fail "prod-Migrationsimage wurde trotz fehlendem Produktionsschluessel angelegt"
fi
if echo "$OUT" | grep -q "Produktionsschluessel fehlt"; then
pass "Meldung ueber fehlenden Produktionsschluessel vorhanden"
else
fail "keine Meldung ueber den fehlenden Produktionsschluessel: $OUT"
fi
echo
echo "=== Szenario 14: gezielter Deploy erzeugt nur die Migrationsimages des angegebenen Ziels (Fall 5, 4a-Fehler fuer migrate/) ==="
SANDBOX="$TMPROOT/s14"; STUBBIN="$TMPROOT/bin14"; LOG="$TMPROOT/s14.log"; SANDBOX_STATE="$TMPROOT/s14-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A
make_gz_file "$SANDBOX/build/bar/ninlight_bar_gz.img" C
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
echo "bar-ota" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
echo "vorhandenes-bar-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_prod.img"
echo "vorhandenes-bar-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_dev.img"
# Manifest eines frueheren Deploys, das bar beschrieben hat - genau das darf ein
# gezielter Deploy von foo nicht einfach wegschreiben (Fixrunde 1, I1). bar traegt
# absichtlich eine ANDERE Firmware-Version als das, was dieser Lauf fuer foo signiert
# (nl_mkimage-Stub liefert immer "1.0.0") - das Manifest nach dem Merge muss beide
# Versionen unveraendert nebeneinander stehen lassen (Fixrunde 2, N2).
cat >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json" <<'JSON'
{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.9.0",
"images": [
{"target": "bar", "variant": "prod", "file": "bar_prod.img", "bytes": 20, "sha256": "x",
"ninlight_version": "0.9.0"},
{"target": "bar", "variant": "dev", "file": "bar_dev.img", "bytes": 19, "sha256": "y",
"ninlight_version": "0.9.0"}
]}
JSON
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" ] \
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
pass "Migrationsimages fuer das angegebene Ziel (foo) wurden angelegt"
else
fail "Migrationsimages fuer foo fehlen: $OUT"
fi
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_prod.img" 2>/dev/null)" = "vorhandenes-bar-prod" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_dev.img" 2>/dev/null)" = "vorhandenes-bar-dev" ]; then
pass "Migrationsimages des NICHT ausgewaehlten Geraetetyps (bar) blieben unangetastet"
else
fail "Migrationsimages von bar wurden veraendert oder geloescht - 4a-Fehler fuer migrate/"
fi
set +e
MANIFEST_CHECK=$(python3 -c "
import json
with open('$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json') as fh:
data = json.load(fh)
entries = data.get('images', [])
keys = {(e.get('target'), e.get('variant')) for e in entries}
expected = {('foo', 'prod'), ('foo', 'dev'), ('bar', 'prod'), ('bar', 'dev')}
by_key = {(e.get('target'), e.get('variant')): e for e in entries}
ok = (
keys == expected
# foo wurde in DIESEM Lauf signiert -> Stub-Version 1.0.0 (Fixrunde 2, N2: Version
# steht pro Eintrag).
and by_key[('foo', 'prod')].get('ninlight_version') == '1.0.0'
and by_key[('foo', 'dev')].get('ninlight_version') == '1.0.0'
# bar wurde NICHT neu signiert -> seine alte Version (0.9.0) bleibt unangetastet,
# auch wenn sie von foos Version abweicht.
and by_key[('bar', 'prod')].get('ninlight_version') == '0.9.0'
and by_key[('bar', 'dev')].get('ninlight_version') == '0.9.0'
# Da foo (1.0.0) und bar (0.9.0) nach dem Merge NICHT uebereinstimmen, darf das
# manifestweite Feld nicht mehr die Version dieses Laufs behaupten - es muss ganz
# fehlen (Fixrunde 2, N2: das war der eigentliche Fehler - vorher stand hier
# weiterhin 1.0.0, obwohl bars Bilder unveraendert auf 0.9.0 blieben).
and 'ninlight_version' not in data
)
print('OK' if ok else 'FAIL: ' + json.dumps(data))
" 2>&1)
set -e
if [ "$MANIFEST_CHECK" = "OK" ]; then
pass "manifest.json beschreibt nach dem gezielten Deploy weiterhin BEIDE Geraetetypen mit je eigener Version, ohne falsches manifestweites Feld"
else
fail "manifest.json-Inhalt nach dem gezielten Deploy unerwartet: $MANIFEST_CHECK"
fi
echo
echo "=== Szenario 15: Kein privater Schluessel und kein Schluesselpfad-Inhalt in der Ausgabe (Fall 6) ==="
SANDBOX="$TMPROOT/s15"; STUBBIN="$TMPROOT/bin15"; LOG="$TMPROOT/s15.log"; SANDBOX_STATE="$TMPROOT/s15-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q -- "-----BEGIN"; then
fail "PEM-Header (Schluesselinhalt) taucht in der Ausgabe auf"
else
pass "kein PEM-Header in der Ausgabe"
fi
# ALLE Zeilen BEIDER privater Schluessel pruefen (nicht nur eine einzelne Zeile des
# Produktionsschluessels) und dabei auch das ssh/rsync/sudo-Log durchsuchen, nicht nur
# die Kommandozeilenausgabe von deploy.sh selbst (Fixrunde 1: die vorherige Version
# haette einen Leak ueber eine andere Zeile oder ueber $LOG glatt uebersehen).
LEAK=""
while IFS= read -r line; do
[ -n "$line" ] || continue
if echo "$OUT" | grep -qF -- "$line"; then
LEAK="Ausgabe enthaelt: $line"
break
fi
if grep -qF -- "$line" "$LOG"; then
LEAK="Log enthaelt: $line"
break
fi
done < <(cat "$KEYDIR_FULL/QCX_Device_CA/qcx_firmware_signing_key.pem" "$KEYDIR_FULL/fw_sign.pem")
if [ -z "$LEAK" ]; then
pass "keine Zeile eines privaten Schluessels taucht in Ausgabe oder Log auf"
else
fail "Schluesselinhalt gefunden - $LEAK"
fi
echo
echo "=== Szenario 16: Signiert wird das _gz.img, niemals das _ota.img (Fall 7, gefaehrlichster Fehler) ==="
SANDBOX="$TMPROOT/s16"; STUBBIN="$TMPROOT/bin16"; LOG="$TMPROOT/s16.log"; SANDBOX_STATE="$TMPROOT/s16-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A
make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
if [ -f "$DEV_IMG" ]; then
set +e
python3 -c "
import sys
with open(sys.argv[1], 'rb') as fh:
signed = fh.read()
with open(sys.argv[2], 'rb') as fh:
gz = fh.read()
sys.exit(0 if signed[:-512] == gz else 1)
" "$DEV_IMG" "$SANDBOX/build/foo/ninlight_foo_gz.img"
PAYLOAD_MATCH=$?
set -e
if [ "$PAYLOAD_MATCH" -eq 0 ]; then
pass "die signierte Nutzlast ist byteidentisch mit dem _gz.img"
else
fail "die signierte Nutzlast entspricht NICHT dem _gz.img - das _ota.img wurde signiert!"
fi
else
fail "dev-Migrationsimage wurde nicht angelegt: $OUT"
fi
echo
echo "=== Szenario 17: Ohne Schluesselverzeichnis bleibt ein vorhandenes migrate/ beim Voll-Deploy unangetastet ==="
SANDBOX="$TMPROOT/s17"; STUBBIN="$TMPROOT/bin17"; LOG="$TMPROOT/s17.log"; SANDBOX_STATE="$TMPROOT/s17-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ]; then
pass "vorhandenes migrate/ blieb beim Voll-Deploy ohne Schluesselverzeichnis unangetastet"
else
fail "migrate/ wurde geleert, obwohl kein Schluesselverzeichnis vorhanden war - zerstoert die Bestueckung"
fi
if echo "$OUT" | grep -q "migrate-wlan/ auf dem Pi bleibt unangetastet"; then
pass "Ausgabe kuendigt an, dass migrate/ unangetastet bleibt"
else
fail "Ausgabe erwaehnt nicht, dass migrate/ unangetastet bleibt: $OUT"
fi
echo
echo "=== Szenario 18 (Fixrunde 1, C2): Schluesselverzeichnis vorhanden, aber leer - migrate/ bleibt unangetastet, Rettungsimage kommt trotzdem ==="
# Der eigentliche C2-Repro: anders als Szenario 12 (Verzeichnis fehlt komplett) EXISTIERT
# $SIGN_DIR hier, enthaelt aber keinen der beiden Schluessel. Vorher fuehrte das trotzdem
# zu einem (leeren) $STAGE/migrate mit Manifest, das dann per --delete ein vorhandenes
# migrate/ auf dem Pi geleert hat.
SIGN_EMPTY="$TMPROOT/keys-empty"
mkdir -p "$SIGN_EMPTY"
SANDBOX="$TMPROOT/s18"; STUBBIN="$TMPROOT/bin18"; LOG="$TMPROOT/s18.log"; SANDBOX_STATE="$TMPROOT/s18-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
echo '{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.0.1", "images": []}' \
>"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$SIGN_EMPTY" run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; then
pass "Rettungsimage (_ota.img) wurde trotzdem ausgeliefert"
else
fail "Rettungsimage fehlt: $OUT"
fi
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ] \
&& grep -q '"images": \[\]' "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"; then
pass "vorhandenes migrate/ (Bilder UND manifest.json) blieb unangetastet - Verzeichnis war da, aber leer"
else
fail "migrate/ wurde trotz vorhandenem, aber leerem Schluesselverzeichnis veraendert/geleert - C2 nicht behoben"
fi
if echo "$OUT" | grep -q "enthaelt weder Produktions- noch Entwicklungsschluessel"; then
pass "Meldung nennt das leere Schluesselverzeichnis"
else
fail "keine Meldung ueber das leere Schluesselverzeichnis: $OUT"
fi
echo
echo "=== Szenario 19 (Fixrunde 1, C2): Voll-Deploy mit nur dev-Schluessel raeumt keine vorhandenen _prod.img weg ==="
SANDBOX="$TMPROOT/s19"; STUBBIN="$TMPROOT/bin19"; LOG="$TMPROOT/s19.log"; SANDBOX_STATE="$TMPROOT/s19-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
echo "echtes-altes-prod-image" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test 2>&1) # Voll-Deploy, kein Ziel-Argument
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
pass "neues dev-Migrationsimage wurde signiert und ausgeliefert"
else
fail "dev-Migrationsimage fehlt: $OUT"
fi
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "echtes-altes-prod-image" ]; then
pass "das vorhandene _prod.img blieb unangetastet, obwohl dieser Voll-Deploy nur den dev-Schluessel hatte"
else
fail "das vorhandene _prod.img wurde beim Voll-Deploy mit nur dev-Schluessel entfernt - C2 (Teilfall) nicht behoben"
fi
echo
echo "=== Szenario 20 (Fixrunde 1, C1): manifest.json wird vom ECHTEN Server-Leser korrekt interpretiert ==="
# json.load allein (wie in Szenario 11) haette die falsche Verschachtelung von
# ninlight_version nicht gemerkt, weil es nur die Feldnamen innerhalb der Eintraege
# prueft. Hier laeuft dieselbe Datei stattdessen durch server.py's eigene
# Station.image_version(target, variant) - genau die Funktion, die im echten Betrieb
# entscheidet, welche Version protokolliert und angezeigt wird (seit Fixrunde 2, N2:
# pro ausgeliefertem Eintrag, nicht mehr nur manifestweit - deshalb jetzt mit
# target/variant statt ohne Argumente).
SANDBOX="$TMPROOT/s20"; STUBBIN="$TMPROOT/bin20"; LOG="$TMPROOT/s20.log"; SANDBOX_STATE="$TMPROOT/s20-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then
pass "manifest.json wurde angelegt"
else
fail "manifest.json fehlt oder Exit-Code $RC: $OUT"
fi
printf 'mit-wlan\n' >"$TMPROOT/s20-imageset"
set +e
VERSION=$(python3 -c "
import importlib.util
from pathlib import Path
station_dir = Path('$STATION_DIR')
spec = importlib.util.spec_from_file_location('ninlight_station_server_c1', station_dir / 'server.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
st = module.Station(
image_dir=Path('$MANIFEST').resolve().parent.parent,
state_path=Path('$TMPROOT/s20-status.json'),
stall_after=None,
mode='migration',
control_path=Path('$TMPROOT/s20-control.json'),
# Das Manifest dieses Szenarios gehoert zum Satz MIT WLAN-Zugangsdaten - der
# Umschalter muss also darauf stehen, sonst liest der Server im anderen Satz nach.
imageset_path=Path('$TMPROOT/s20-imageset'),
)
print(st.image_version('foo', 'dev'))
" 2>&1)
RC2=$?
set -e
if [ "$RC2" -eq 0 ] && [ "$VERSION" = "1.0.0" ]; then
pass "Station.image_version() (der echte Server-Code) liest die Version korrekt, nicht 'unbekannt'"
else
fail "Station.image_version() liefert '$VERSION' statt '1.0.0' (RC=$RC2) - Manifest-Form stimmt nicht mit dem Server ueberein"
fi
echo
echo "=== Szenario 21 (Fixrunde 2, N1): kaputtes/falsch geformtes Bestandsmanifest darf den Deploy nicht mitten drin abbrechen ==="
# Der Merge steht in deploy.sh VOR der Installation von server.py/products.py/CLI/Unit
# und vor dem Neustart - ein Abbruch hier liesse den Pi mit neuen Images, aber altem
# Stationscode zurueck. "truncated" und "empty" liefen schon vorher durch (json.load
# wirft ValueError, das war immer abgefangen); "array"/"null"/"string"/"null-entry"
# sind gueltiges JSON mit falscher FORM und haben vorher mit AttributeError abgebrochen
# ("... has no attribute 'get'"), weil nur auf Lese-, nicht auf Parse-Fehler geprueft
# wurde. Alle vier Zeilen aus der Review-Tabelle.
mkdir -p "$TMPROOT/bad-manifests"
printf '%s' '{ kaputt' >"$TMPROOT/bad-manifests/truncated.json"
: >"$TMPROOT/bad-manifests/empty.json"
printf '%s' '[]' >"$TMPROOT/bad-manifests/array.json"
printf '%s' 'null' >"$TMPROOT/bad-manifests/null.json"
printf '%s' '"text"' >"$TMPROOT/bad-manifests/string.json"
printf '%s' '{"images": [null]}' >"$TMPROOT/bad-manifests/null-entry.json"
for label in truncated empty array null string null-entry; do
SANDBOX="$TMPROOT/s21-$label"; STUBBIN="$TMPROOT/bin21-$label"
LOG="$TMPROOT/s21-$label.log"; SANDBOX_STATE="$TMPROOT/s21-$label-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
cp "$TMPROOT/bad-manifests/$label.json" "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
: >"$LOG"
set +e
# Nur EIN Schluessel (dev) -> MIGRATE_COMPLETE=0 -> der Merge-Zweig laeuft, nicht der
# --delete-Zweig, der das kaputte Bestandsmanifest ohnehin ueberschreiben wuerde.
OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then
pass "Bestandsmanifest '$label': Exit-Code 0, Deploy bricht nicht mitten drin ab"
else
fail "Bestandsmanifest '$label': Exit-Code $RC - Deploy abgebrochen. Ausgabe: $OUT"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] \
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
pass "Bestandsmanifest '$label': Deploy lief vollstaendig durch (server.py installiert, foo_dev.img da)"
else
fail "Bestandsmanifest '$label': Deploy blieb auf halbem Weg stehen (server.py oder foo_dev.img fehlt)"
fi
done
echo
echo "=== Szenario 22 (Task 6b, Punkt 1): deploy.sh liefert Anzeigeverzeichnis und Anzeige-Unit mit aus ==="
SANDBOX="$TMPROOT/s22"; STUBBIN="$TMPROOT/bin22"; LOG="$TMPROOT/s22.log"; SANDBOX_STATE="$TMPROOT/s22-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
# Bestand auf dem "Pi": eine Datei, die es im Repo nicht mehr gibt. display/ ist reiner
# Code aus dem Repo - was dort verschwindet, muss auch hier verschwinden, sonst laedt
# ein zurueckgebliebenes altes Modul weiter mit.
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/display"
echo "alt" >"$SANDBOX_STATE/opt/ninlight-station/display/veraltet.py"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
DISP="$SANDBOX_STATE/opt/ninlight-station/display"
for F in display.py render.py epdconfig.py epd2in13_V2.py epd2in13_V3.py HERKUNFT.md; do
[ -f "$DISP/$F" ] && pass "display/$F ausgeliefert" || fail "display/$F fehlt unter $DISP"
done
[ -f "$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" ] && pass "ninlight-display.service ausgeliefert" || fail "Anzeige-Unit fehlt - sie waere weiter toter Text im Repo"
[ ! -f "$DISP/veraltet.py" ] && pass "im Repo geloeschte Datei verschwindet auch auf dem Pi (--delete)" || fail "veraltet.py blieb im Zielverzeichnis liegen"
[ ! -d "$DISP/__pycache__" ] && pass "kein __pycache__ dieses Rechners auf dem Pi gelandet" || fail "__pycache__ wurde mit uebertragen: $(ls "$DISP/__pycache__")"
# Die Anzeige lief nicht (Stub-Default) - dann wird sie auch nicht neu gestartet, aber
# der Deploy sagt es.
echo "$OUT" | grep -q "Anzeigedienst laeuft nicht" && pass "Deploy meldet, dass der Anzeigedienst nicht laeuft" || fail "Ausgabe erwaehnt den Anzeigedienst nicht: $OUT"
# Die fluechtige Ablage wird wieder aufgeraeumt (sie liegt, wie bei server.py & Co.,
# im echten /tmp - deploy.sh's REMOTE-Block kennt dafuer keine Sandbox-Variable).
[ ! -d /tmp/ninlight-display ] && pass "die fluechtige Ablage /tmp/ninlight-display wurde aufgeraeumt" || fail "/tmp/ninlight-display blieb liegen"
echo
echo "=== Szenario 23: laeuft die Anzeige, wird sie nach dem Deploy neu gestartet ==="
# Ohne Neustart liefe sie mit dem gerade ersetzten Code weiter.
SANDBOX="$TMPROOT/s23"; STUBBIN="$TMPROOT/bin23"; LOG="$TMPROOT/s23.log"; SANDBOX_STATE="$TMPROOT/s23-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "systemctl restart ninlight-display" "$LOG" && pass "Anzeigedienst wurde neu gestartet" || fail "kein 'systemctl restart ninlight-display' im Log: $(cat "$LOG")"
echo "$OUT" | grep -q "Anzeigedienst neu gestartet" && pass "Ausgabe meldet den Neustart" || fail "Ausgabe meldet den Neustart nicht: $OUT"
echo
echo "=== Szenario 24: ein fehlgeschlagener Neustart der ANZEIGE ist kein Deploy-Fehler ==="
# Die Anzeige ist nirgends Voraussetzung fuer eine Migration. Ein Deploy, der wegen des
# Panels rot wird, verleitet nur dazu, einen echten Fehler zu uebersehen. Beim
# HAUPTdienst ist es umgekehrt - das prueft das Szenario darunter zur Abgrenzung.
SANDBOX="$TMPROOT/s24"; STUBBIN="$TMPROOT/bin24"; LOG="$TMPROOT/s24.log"; SANDBOX_STATE="$TMPROOT/s24-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 FAKE_DISPLAY_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>"$TMPROOT/s24.err")
RC=$?
set -e
ERR=$(cat "$TMPROOT/s24.err")
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Anzeige-Neustart" || fail "Exit-Code $RC - der Deploy wurde wegen der Anzeige rot: $ERR"
echo "$ERR" | grep -q "Warnung" && pass "Warnung (keine Fehlermeldung) auf stderr" || fail "keine Warnung auf stderr: $ERR"
[ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] && pass "der Deploy selbst lief vollstaendig durch" || fail "server.py fehlt - der Deploy brach ab"
echo
echo "=== Szenario 25 (Abgrenzung): ein fehlgeschlagener Neustart des HAUPTdienstes ist sehr wohl ein Deploy-Fehler ==="
SANDBOX="$TMPROOT/s25"; STUBBIN="$TMPROOT/bin25"; LOG="$TMPROOT/s25.log"; SANDBOX_STATE="$TMPROOT/s25-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 beim Hauptdienst" || fail "Exit-Code 0 - ein toter Hauptdienst ginge unbemerkt durch: $OUT"
echo
echo "=== Szenario 26: deploy.sh liefert das Tasterverzeichnis und die Taster-Unit mit aus ==="
SANDBOX="$TMPROOT/s26"; STUBBIN="$TMPROOT/bin26"; LOG="$TMPROOT/s26.log"; SANDBOX_STATE="$TMPROOT/s26-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
# Bestand auf dem "Pi": eine Datei, die es im Repo nicht mehr gibt - muss beim Deploy
# verschwinden (dasselbe Argument wie bei display/ in Szenario 22).
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/button"
echo "alt" >"$SANDBOX_STATE/opt/ninlight-station/button/veraltet.py"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
BTN="$SANDBOX_STATE/opt/ninlight-station/button"
[ -f "$BTN/button.py" ] && pass "button/button.py ausgeliefert" || fail "button/button.py fehlt unter $BTN"
[ -f "$SANDBOX_STATE/etc/systemd/system/ninlight-button.service" ] && pass "ninlight-button.service ausgeliefert" || fail "Taster-Unit fehlt - sie waere weiter toter Text im Repo"
[ ! -f "$BTN/veraltet.py" ] && pass "im Repo geloeschte Datei verschwindet auch auf dem Pi (--delete)" || fail "veraltet.py blieb im Zielverzeichnis liegen"
[ ! -d "$BTN/__pycache__" ] && pass "kein __pycache__ dieses Rechners auf dem Pi gelandet" || fail "__pycache__ wurde mit uebertragen: $(ls "$BTN/__pycache__")"
echo "$OUT" | grep -q "Tasterdienst laeuft nicht" && pass "Deploy meldet, dass der Tasterdienst nicht laeuft" || fail "Ausgabe erwaehnt den Tasterdienst nicht: $OUT"
[ ! -d /tmp/ninlight-button ] && pass "die fluechtige Ablage /tmp/ninlight-button wurde aufgeraeumt" || fail "/tmp/ninlight-button blieb liegen"
echo
echo "=== Szenario 27: ein fehlgeschlagener Neustart des TASTERdienstes ist kein Deploy-Fehler ==="
SANDBOX="$TMPROOT/s27"; STUBBIN="$TMPROOT/bin27"; LOG="$TMPROOT/s27.log"; SANDBOX_STATE="$TMPROOT/s27-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_BUTTON_ACTIVE_EXIT=0 FAKE_BUTTON_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>"$TMPROOT/s27.err")
RC=$?
set -e
ERR=$(cat "$TMPROOT/s27.err")
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Taster-Neustart" || fail "Exit-Code $RC - der Deploy wurde wegen des Tasters rot: $ERR"
echo "$ERR" | grep -q "Warnung" && pass "Warnung (keine Fehlermeldung) auf stderr" || fail "keine Warnung auf stderr: $ERR"
[ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] && pass "der Deploy selbst lief vollstaendig durch" || fail "server.py fehlt - der Deploy brach ab"
echo
echo "=== Szenario 28: beide Bildsaetze werden ausgeliefert, und zwar aus verschiedenen Bauten ==="
# Der Umschalter auf der Station ist nur so gut wie die Bestueckung: liegen beide Saetze
# nicht getrennt auf dem Pi, kann er nichts umschalten. Geprueft wird am INHALT, nicht am
# Verzeichnisnamen - die Fuellzeichen der beiden _gz.img sind verschieden, ein
# vertauschter Quellbaum faellt also auf.
SANDBOX="$TMPROOT/s28"; STUBBIN="$TMPROOT/bin28"; LOG="$TMPROOT/s28.log"; SANDBOX_STATE="$TMPROOT/s28-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" W
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
WLAN_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
NOCR_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img"
if [ -f "$WLAN_IMG" ] && [ -f "$NOCR_IMG" ]; then
pass "beide Bildsaetze liegen getrennt auf dem Pi"
else
fail "mindestens ein Bildsatz fehlt: $OUT"
fi
if [ -f "$WLAN_IMG" ] && [ -f "$NOCR_IMG" ]; then
# Die ersten Bytes sind das Fuellzeichen des jeweiligen _gz.img.
W_HEAD=$(head -c 8 "$WLAN_IMG")
N_HEAD=$(head -c 8 "$NOCR_IMG")
[ "$W_HEAD" = "WWWWWWWW" ] && pass "migrate-wlan/ kommt wirklich aus build/" || fail "migrate-wlan/ enthaelt '$W_HEAD' statt der build/-Bytes"
[ "$N_HEAD" = "NNNNNNNN" ] && pass "migrate-nocreds/ kommt wirklich aus build-nocreds/" || fail "migrate-nocreds/ enthaelt '$N_HEAD' statt der build-nocreds/-Bytes"
fi
for m in migrate-wlan migrate-nocreds; do
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/$m/manifest.json" ]; then
pass "$m/manifest.json wurde angelegt"
else
fail "$m/manifest.json fehlt"
fi
done
echo
echo "=== Szenario 29: ohne build-nocreds/ bleibt der Satz 'ohne-wlan' auf dem Pi unangetastet ==="
# tools/build_nocreds.sh ist ein eigener, optionaler Bau. Ein Deploy darf nicht daran
# scheitern, dass er nicht gelaufen ist - und er darf die Bestueckung frueherer Deploys
# nicht wegraeumen (derselbe Fehler wie frueher bei migrate/, siehe Szenario 17).
SANDBOX="$TMPROOT/s29"; STUBBIN="$TMPROOT/bin29"; LOG="$TMPROOT/s29.log"; SANDBOX_STATE="$TMPROOT/s29-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds"
echo "alter-nocreds-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlendem build-nocreds/" || fail "Exit-Code $RC: $OUT"
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" 2>/dev/null)" = "alter-nocreds-abzug" ]; then
pass "vorhandener Satz 'ohne-wlan' blieb unangetastet"
else
fail "der Satz 'ohne-wlan' wurde geleert oder ueberschrieben, obwohl build-nocreds/ fehlte"
fi
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ] \
&& pass "der Satz 'mit-wlan' wurde trotzdem ausgeliefert" \
|| fail "der Satz 'mit-wlan' fehlt: $OUT"
echo "$OUT" | grep -q "build_nocreds.sh" \
&& pass "der Hinweis nennt, womit der fehlende Satz entsteht" \
|| fail "kein Hinweis auf tools/build_nocreds.sh: $OUT"
echo
echo "=== Szenario 30: ein unvollstaendiger Satz 'ohne-wlan' raeumt nicht mit --delete auf ==="
# build-nocreds/ hat nur eines von zwei Zielen - dieser Lauf beschreibt den Sollzustand
# also NICHT vollstaendig, und --delete wuerde das zweite Ziel des vorigen Deploys
# wegraeumen.
SANDBOX="$TMPROOT/s30"; STUBBIN="$TMPROOT/bin30"; LOG="$TMPROOT/s30.log"; SANDBOX_STATE="$TMPROOT/s30-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar" "$SANDBOX/build-nocreds/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
make_gz_file "$SANDBOX/build/bar/ninlight_bar_gz.img"
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
echo "bar-ota" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds"
echo "alter-bar-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/bar_dev.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/bar_dev.img" 2>/dev/null)" = "alter-bar-abzug" ]; then
pass "das nicht neu gebaute Ziel blieb im Satz 'ohne-wlan' erhalten"
else
fail "das nicht neu gebaute Ziel wurde aus dem Satz 'ohne-wlan' entfernt - --delete lief trotz unvollstaendigem Satz"
fi
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" ] \
&& pass "das gebaute Ziel kam dazu" || fail "das gebaute Ziel fehlt im Satz 'ohne-wlan'"
echo
echo "=== Szenario 31: das alte, namenlose migrate/ wird beim Deploy entfernt ==="
# Bis zum Umschalter hiess das einzige Verzeichnis "migrate" und enthielt die Abzuege MIT
# Zugangsdaten. Es wird von niemandem mehr gelesen - bliebe es liegen, haelt der naechste
# Mensch am Pi es fuer den gueltigen Satz.
SANDBOX="$TMPROOT/s31"; STUBBIN="$TMPROOT/bin31"; LOG="$TMPROOT/s31.log"; SANDBOX_STATE="$TMPROOT/s31-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
echo "altbestand" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
[ ! -e "$SANDBOX_STATE/opt/ninlight-station/images/migrate" ] \
&& pass "der Altbestand images/migrate wurde entfernt" \
|| fail "images/migrate liegt noch da"
echo
echo "=== Szenario 32: die beiden Abschlussbild-Units werden mit ausgeliefert ==="
# Ohne sie bliebe auf dem E-Paper nach dem Herunterfahren fuer immer das letzte Bild
# stehen - bei der Abnahme war das die Rueckfrage des Tastermenues.
SANDBOX="$TMPROOT/s32"; STUBBIN="$TMPROOT/bin32"; LOG="$TMPROOT/s32.log"; SANDBOX_STATE="$TMPROOT/s32-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
for u in ninlight-poweroff ninlight-reboot; do
[ -f "$SANDBOX_STATE/etc/systemd/system/$u.service" ] \
&& pass "$u.service installiert" || fail "$u.service fehlt - sie waere toter Text im Repo"
done
# Ein Deploy darf keine Unit EINSCHALTEN, die vorher aus war - das macht setup_pi.sh.
grep -Fq "systemctl enable ninlight-poweroff" "$LOG" \
&& fail "der Deploy aktiviert die Abschlussbild-Unit" \
|| pass "der Deploy aktiviert sie nicht (das ist setup_pi.sh's Aufgabe)"
# Und die Ablage ist hinterher leer.
[ ! -e /tmp/ninlight-poweroff.service ] && [ ! -e /tmp/ninlight-reboot.service ] \
&& pass "die Ablage wurde aufgeraeumt" || fail "Unit-Dateien blieben unter /tmp liegen"
echo
echo "=== Szenario 33 (Abschlussreview, I3): Zugangsdaten im Bildsatz 'ohne-wlan' brechen den Deploy ab ==="
# Der Satz "ohne-wlan" ist die SICHERE VORGABE - im Automatikmodus geht er an jede alte
# Klyqa-Lampe in Funkreichweite, auch an fremde, und das Flashen ist nicht umkehrbar.
# release.sh prueft sein Archiv binaer gegen config.local.cmake; deploy.sh signierte
# bisher ungeprueft, was in build-nocreds/ lag. Ein von Hand konfiguriertes
# build-nocreds/<ziel> genuegt, damit das Panel "ohne WLAN" anzeigt, waehrend das
# WLAN-Passwort des Bedieners unwiderruflich in fremde Lampen wandert.
SANDBOX="$TMPROOT/s33"; STUBBIN="$TMPROOT/bin33"; LOG="$TMPROOT/s33.log"; SANDBOX_STATE="$TMPROOT/s33-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
write_local_cmake "$SANDBOX"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo"
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" W
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
# Der undichte Abzug: enthaelt SSID, Passwort und Token.
make_gz_file_with_creds "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
# GEGENPROBE (ohne sie waere dieses Szenario wertlos): der "Compiler" dieser Suite muss
# die Zugangsdaten wirklich einbauen. Genau diese Gegenprobe gibt es in test_release.sh
# fuer den Docker-Stub - dort hat sie belegt, dass Fall 5 nicht nur deshalb gruen ist,
# weil nie jemand etwas einbaut.
if tree_contains "$SANDBOX/build-nocreds" "$SECRET_PASS"; then
pass "Gegenprobe: der Abzug in build-nocreds/ enthaelt das WLAN-Passwort wirklich"
else
fail "Gegenprobe fehlgeschlagen - der Abzug enthaelt gar keine Zugangsdaten, Szenario 33 waere wertlos"
fi
# Der Pi hat schon einen (sauberen) Bestand aus einem frueheren Deploy - er darf durch
# den Abbruch weder ersetzt noch geloescht werden.
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds"
echo "alter-sauberer-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img"
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0 - der undichte Bildsatz waere ausgeliefert worden: $OUT"
echo "$OUT" | grep -q "Zugangsdaten" \
&& pass "Fehlermeldung nennt die gefundenen Zugangsdaten" \
|| fail "keine Fehlermeldung zu Zugangsdaten: $OUT"
echo "$OUT" | grep -q "NINLIGHT_WIFI_PASSWORD" \
&& pass "Fehlermeldung nennt den betroffenen Schluessel" \
|| fail "Fehlermeldung nennt den Schluessel nicht: $OUT"
if ! grep -q "STUB ssh\|STUB rsync\|STUB scp" "$LOG"; then
pass "kein ssh/rsync/scp aufgerufen - nichts wurde uebertragen"
else
fail "es wurde trotz gefundener Zugangsdaten uebertragen: $(cat "$LOG")"
fi
[ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img")" = "alter-sauberer-abzug" ] \
&& pass "der Bestand auf dem Pi blieb unangetastet" \
|| fail "der Bestand auf dem Pi wurde veraendert"
if ! tree_contains "$SANDBOX_STATE" "$SECRET_PASS"; then
pass "kein Zugangsdatum hat es in die Sandbox-Zielablage geschafft"
else
fail "Zugangsdaten sind trotz Abbruch beim Ziel gelandet"
fi
echo
echo "=== Szenario 34 (Abgrenzung zu 33): ein sauberer Satz 'ohne-wlan' geht raus, migrate-wlan DARF die Zugangsdaten tragen ==="
# Die Pruefung muss genau einen Satz betreffen. migrate-wlan/ enthaelt die Zugangsdaten
# absichtlich - das ist sein ganzer Zweck ("Lampe haengt sofort im eigenen WLAN"). Eine
# Pruefung, die auch ihn abweist, waere unbenutzbar und wuerde nach kurzer Zeit
# abgeschaltet.
SANDBOX="$TMPROOT/s34"; STUBBIN="$TMPROOT/bin34"; LOG="$TMPROOT/s34.log"; SANDBOX_STATE="$TMPROOT/s34-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
write_local_cmake "$SANDBOX"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo"
# build/ MIT Zugangsdaten (wie im echten Betrieb), build-nocreds/ ohne.
make_gz_file_with_creds "$SANDBOX/build/foo/ninlight_foo_gz.img" W
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
: >"$LOG"
set +e
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - die Pruefung weist einen sauberen Satz ab: $OUT"
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" ] \
&& pass "der saubere Satz 'ohne-wlan' kam an" || fail "der saubere Satz 'ohne-wlan' fehlt: $OUT"
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ] \
&& pass "migrate-wlan kam trotz einkompilierter Zugangsdaten an" \
|| fail "migrate-wlan wurde faelschlich mit abgewiesen: $OUT"
if tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" "$SECRET_PASS"; then
pass "Gegenprobe: migrate-wlan traegt die Zugangsdaten wirklich - die Pruefung haette anschlagen KOENNEN"
else
fail "Gegenprobe fehlgeschlagen - migrate-wlan traegt gar keine Zugangsdaten, die Abgrenzung waere wertlos"
fi
if ! tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds" "$SECRET_PASS"; then
pass "und der Satz 'ohne-wlan' auf dem Pi ist frei davon"
else
fail "der Satz 'ohne-wlan' auf dem Pi enthaelt Zugangsdaten"
fi
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"