Files
ninlight-w600/tools/station/tests/test_deploy.sh
T
Michael Sauer eec9fa5a21 feat(station): Rettungsabbilder folgen dem Bildsatz-Umschalter beim Deploy
deploy.sh legt Rettungsabzuege jetzt in images/rescue-wlan/ (aus build/) und
images/rescue-nocreds/ (aus build-nocreds/) ab, statt sie flach in images/
abzulegen - dasselbe Muster wie migrate-wlan/migrate-nocreds, inklusive
derselben --delete-Vorsicht (nur bei vollstaendigem Satz). Alter flacher
Bestand wird bei jedem Deploy aufgeraeumt, auch bei einem gezielten.

README beschreibt den Umschalter jetzt fuer beide Betriebsarten.
2026-09-23 21:22:06 +02:00

1569 lines
72 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 MIT dieser Fassung von
# deploy.sh: Rettungsabbilder fuer foo UND bar liegen schon in images/rescue-wlan/ (dem
# "mit-wlan"-Bildsatz aus build/, siehe server.py:RESCUE_SET_DIRS).
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan"
echo "foo-image-alt" >"$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img"
echo "bar-image" >"$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/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/rescue-wlan/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/rescue-wlan/ninlight_foo_ota.img" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/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/rescue-wlan/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/rescue-wlan/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
echo "=== Szenario 35: der Bildsatz-Umschalter gilt jetzt auch fuer die Rettung - beide Rettungssaetze werden ausgeliefert ==="
# Analog zu Szenario 28 (Migration), aber fuer die Rettungsabbilder: rescue-wlan/ kommt
# aus build/ (dasselbe _ota.img, das ohnehin schon fuer die Trailer-Pruefung gebraucht
# wird), rescue-nocreds/ aus build-nocreds/ - ein eigener, unsignierter Fund.
SANDBOX="$TMPROOT/s35"; STUBBIN="$TMPROOT/bin35"; LOG="$TMPROOT/s35.log"; SANDBOX_STATE="$TMPROOT/s35-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo"
make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img"
python3 -c "
import sys
path = sys.argv[1]
body = b'N' * 2048
trailer = bytearray(b'\xff' * 512)
trailer[0:4] = b'NINL'
with open(path, 'wb') as fh:
fh.write(body + bytes(trailer))
" "$SANDBOX/build-nocreds/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"
RESCUE_WLAN="$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img"
RESCUE_NOCREDS="$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/ninlight_foo_ota.img"
if [ -f "$RESCUE_WLAN" ] && [ -f "$RESCUE_NOCREDS" ]; then
pass "beide Rettungssaetze liegen getrennt auf dem Pi"
else
fail "mindestens ein Rettungssatz fehlt: $OUT"
fi
if head -c 1 "$RESCUE_WLAN" | grep -q "B"; then
pass "rescue-wlan/ kommt wirklich aus build/"
else
fail "rescue-wlan/ hat nicht den erwarteten Inhalt aus build/"
fi
if head -c 1 "$RESCUE_NOCREDS" | grep -q "N"; then
pass "rescue-nocreds/ kommt wirklich aus build-nocreds/"
else
fail "rescue-nocreds/ hat nicht den erwarteten Inhalt aus build-nocreds/"
fi
echo
echo "=== Szenario 36: ohne build-nocreds/ bleibt der Rettungssatz 'ohne-wlan' auf dem Pi unangetastet ==="
SANDBOX="$TMPROOT/s36"; STUBBIN="$TMPROOT/bin36"; LOG="$TMPROOT/s36.log"; SANDBOX_STATE="$TMPROOT/s36-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds"
echo "alter-rescue-nocreds-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/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 trotz fehlendem build-nocreds/" || fail "Exit-Code $RC: $OUT"
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/ninlight_foo_ota.img" 2>/dev/null)" \
= "alter-rescue-nocreds-abzug" ]; then
pass "vorhandener Rettungssatz 'ohne-wlan' blieb unangetastet"
else
fail "der Rettungssatz 'ohne-wlan' wurde geleert oder ueberschrieben, obwohl build-nocreds/ fehlte"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img" ]; then
pass "der Rettungssatz 'mit-wlan' wurde trotzdem ausgeliefert"
else
fail "der Rettungssatz 'mit-wlan' fehlt: $OUT"
fi
if echo "$OUT" | grep -q "build-nocreds"; then
pass "der Hinweis nennt, womit der fehlende Rettungssatz entsteht"
else
fail "kein Hinweis auf build-nocreds/ in der Ausgabe: $OUT"
fi
echo
echo "=== Szenario 37: gezielter Deploy ohne build-nocreds/<ziel> bricht nicht ab, sondern laesst 'ohne-wlan' fuer dieses Ziel unangetastet ==="
SANDBOX="$TMPROOT/s37"; STUBBIN="$TMPROOT/bin37"; LOG="$TMPROOT/s37.log"; SANDBOX_STATE="$TMPROOT/s37-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlendem build-nocreds/foo" || fail "Exit-Code $RC: $OUT"
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img" ]; then
pass "der Rettungssatz 'mit-wlan' fuer foo wurde trotzdem ausgeliefert"
else
fail "der Rettungssatz 'mit-wlan' fuer foo fehlt: $OUT"
fi
if [ ! -e "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/ninlight_foo_ota.img" ]; then
pass "kein Rettungsimage 'ohne-wlan' fuer foo entstanden - build-nocreds/foo fehlte"
else
fail "es entstand ein Rettungsimage 'ohne-wlan' fuer foo, obwohl build-nocreds/foo fehlte"
fi
if echo "$OUT" | grep -q "bleibt im Rettungs-Bildsatz 'ohne-wlan' unveraendert"; then
pass "Hinweis auf das fehlende build-nocreds/foo vorhanden"
else
fail "kein Hinweis auf das fehlende build-nocreds/foo: $OUT"
fi
echo
echo "=== Szenario 38: Altbestand - vor diesem Umschalter flach in images/ liegende Rettungsabbilder werden auch bei einem gezielten Deploy aufgeraeumt ==="
# Anders als bei den Migrationsabbildern (die ein eigenes, benanntes Verzeichnis erst
# seit einem frueheren Umschalter haben) gab es fuer die Rettung nie eine Uebergangsphase
# mit --delete: der flache Bestand wird von server.py seit diesem Umschalter nicht mehr
# gelesen (siehe rescue_dir() dort) und ist deshalb toter Ballast, sobald IRGENDEIN
# Deploy mit dieser Fassung laeuft - auch ein gezielter, der images/ sonst nicht mit
# --delete abgleicht.
SANDBOX="$TMPROOT/s38"; STUBBIN="$TMPROOT/bin38"; LOG="$TMPROOT/s38.log"; SANDBOX_STATE="$TMPROOT/s38-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "alter-flacher-bestand" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img"
echo "alter-flacher-bestand-bar" >"$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
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
if [ ! -e "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ] \
&& [ ! -e "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img" ]; then
pass "der flache Altbestand wurde auch bei einem gezielten Deploy entfernt"
else
fail "flacher Altbestand liegt noch da - er wuerde von niemandem mehr gelesen, aber am Pi einen gueltigen Satz vortaeuschen"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img" ]; then
pass "das neue Rettungsimage liegt an der richtigen Stelle (images/rescue-wlan/)"
else
fail "das neue Rettungsimage fehlt: $OUT"
fi
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"