Files
ninlight-w600/tools/station/tests/test_deploy.sh
T
Michael Sauer 59c8c150bd feat(station): Migrationsimages beim Deploy signieren
deploy.sh signiert pro Ziel das rohe _gz.img (nie das _ota.img) mit den
Produktions- und Entwicklungsschluesseln aus $NINLIGHT_SIGN_DIR und legt
die entstandenen _prod.img/_dev.img samt manifest.json unter migrate/ ab.
Fehlt das Schluesselverzeichnis oder einzelne Schluessel, entfaellt nur
die jeweilige Migrationsvariante mit einer Meldung auf stderr - die
Rettungsimages werden unveraendert weiter ausgeliefert.

migrate/ wird beim rsync-Abgleich auf dem Pi bewusst getrennt von den
_ota.img behandelt: fehlt es lokal (kein Schluesselverzeichnis auf dem
Entwicklungsrechner), bleibt ein vorhandenes migrate/ auf dem Pi
unangetastet statt durch --delete geleert zu werden - derselbe Fehler,
der frueher schon einmal fremde Geraetetypen weggeraeumt hat, diesmal auf
die Migrationsimages angewendet.

test_deploy.sh bekommt acht neue Szenarien fuer die sieben im Task-Brief
verlangten Faelle plus den zusaetzlichen Schutz vor dem geleerten
migrate/. Alle Szenarien erzeugen ihre eigenen Wegwerf-Testschluessel
ueber nl_sign.generate_test_key() und setzen NINLIGHT_SIGN_DIR bei jedem
Aufruf explizit (auch im "ohne Schluessel"-Fall) - der Default
$HOME/workspace/fw-sign zeigt auf diesem Rechner auf echte Schluessel,
und kein Testlauf darf sie je lesen.
2026-09-13 19:47:51 +02:00

704 lines
28 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)
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"
# 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
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
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 IMMER explizit setzen: deploy.sh faellt sonst auf
# $HOME/workspace/fw-sign zurueck, und auf Entwicklungsrechnern liegen dort die
# ECHTEN Signierschluessel. Ein Test darf die niemals auch nur lesen. Szenarien,
# die ein Schluesselverzeichnis brauchen, ueberschreiben die Variable gezielt VOR
# dem Aufruf von run_deploy (siehe unten); dieser Default zeigt auf einen Pfad,
# der in der Sandbox garantiert nicht existiert.
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_SIGN_DIR="${NINLIGHT_SIGN_DIR:-$sandbox/no-such-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"
}
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
OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 \
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_SIGN_DIR="$SANDBOX/no-such-sign-dir" \
"$SANDBOX/tools/station/deploy.sh" 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=$(NINLIGHT_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/foo_prod.img"
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/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=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate/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
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 = (
len(entries) == 2
and set(by_variant) == {'prod', 'dev'}
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)
)
print('OK' if ok else 'FAIL: ' + json.dumps(entries))
" 2>&1)
set -e
if [ "$CHECK" = "OK" ]; then
pass "manifest.json listet target/variant/bytes/sha256 fuer beide Images"
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" ] \
|| [ -z "$(ls -A "$SANDBOX_STATE/opt/ninlight-station/images/migrate" 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=$(NINLIGHT_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/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/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"
echo "vorhandenes-bar-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_prod.img"
echo "vorhandenes-bar-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_dev.img"
: >"$LOG"
set +e
OUT=$(NINLIGHT_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/foo_prod.img" ] \
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/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/bar_prod.img" 2>/dev/null)" = "vorhandenes-bar-prod" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/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
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=$(NINLIGHT_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
# Eine Zeile aus der Mitte des privaten Produktionsschluessels darf unter keinen
# Umstaenden in der Ausgabe erscheinen.
KEY_LINE=$(sed -n '3p' "$KEYDIR_FULL/QCX_Device_CA/qcx_firmware_signing_key.pem")
if [ -n "$KEY_LINE" ] && echo "$OUT" | grep -qF "$KEY_LINE"; then
fail "eine Zeile des privaten Produktionsschluessels taucht in der Ausgabe auf"
else
pass "keine Schluesselzeile in der Ausgabe"
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=$(NINLIGHT_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/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"
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img"
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/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/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/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/ 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
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"