diff --git a/tools/station/deploy.sh b/tools/station/deploy.sh index 38cb5bc..a1c881e 100755 --- a/tools/station/deploy.sh +++ b/tools/station/deploy.sh @@ -329,46 +329,115 @@ fi # Bedieners unwiderruflich in fremde Lampen wandert - der teure, nicht rueckholbare # Fehler. Lieber ein abgebrochener Deploy. # -# Geprueft wird VOR dem Uebertragen, es ist also noch nichts beim Pi angekommen. Nur -# dieser eine Satz: migrate-wlan/ SOLL die Zugangsdaten enthalten, das ist sein Zweck. -# Der Pruefkoerper ist bis auf die Meldung derselbe wie in tools/release.sh. -if [ -d "$STAGE/migrate-nocreds" ]; then - echo "== Bildsatz 'ohne-wlan' auf Zugangsdaten pruefen ==" +# Geprueft wird VOR dem Uebertragen, es ist also noch nichts beim Pi angekommen. +# Geprueft werden BEIDE Saetze "ohne-wlan" - der Migrationssatz migrate-nocreds/ UND der +# Rettungssatz rescue-nocreds/. Der Rettungssatz ist dabei der gefaehrlichere von beiden: +# er geht ohne Signatur an die Lampe. Frueher hing dieser ganze Block allein an +# migrate-nocreds/, der Rettungssatz lief ungeprueft durch. +# migrate-wlan/ und rescue-wlan/ bleiben ungeprueft: die SOLLEN die Zugangsdaten +# enthalten, das ist ihr Zweck. +# +# Gesucht wird im ENTPACKTEN Rumpf, nicht in der Datei, wie sie auf der Platte liegt: +# jedes Abbild (_ota.img wie die signierten _prod.img/_dev.img) ist 56 Byte Kopf, dann +# ein gzip-Strom, dann 512 Byte Trailer bzw. Signatur. Im komprimierten Strom steht der +# Klartext des Passworts nicht - eine Suche im Rohbyteinhalt meldete deshalb IMMER +# "sauber", auch fuer den Satz aus build/, der die Zugangsdaten nachweislich traegt. Die +# Pruefung hat bis hierhin also nie irgendetwas geprueft. +# +# Ein Abbild, das sich nicht entpacken laesst, ist ein FEHLER und bricht ab - nicht +# "dann eben ungeprueft durchlassen". Genau diese Nachsicht waere der Weg, auf dem das +# Passwort am Ende doch hinausgeht. +# +# Der Pruefkoerper in tools/release.sh ist verwandt, aber nicht mehr identisch: dort +# wird ein fertiges Archiv aus lauter verschiedenen Dateiarten geprueft, hier ein +# Bildsatz aus Firmware-Abbildern. +# Als if-Zweige und nicht als "[ -d … ] && CHECK_SETS+=(…)": unter "set -e" waere eine +# solche Zeile, wenn das Verzeichnis fehlt, ein fehlgeschlagenes letztes Kommando und +# risse den Deploy ab. +declare -a CHECK_SETS=() +if [ -d "$STAGE/migrate-nocreds" ]; then CHECK_SETS+=(migrate-nocreds); fi +if [ -d "$STAGE/rescue-nocreds" ]; then CHECK_SETS+=(rescue-nocreds); fi +if [ ${#CHECK_SETS[@]} -gt 0 ]; then + echo "== Bildsaetze 'ohne-wlan' auf Zugangsdaten pruefen ==" LOCAL="$ROOT/config.local.cmake" if [ -f "$LOCAL" ]; then - python3 - "$STAGE/migrate-nocreds" "$LOCAL" <<'CHECKPY' || exit 1 + python3 - "$LOCAL" "$STAGE" "${CHECK_SETS[@]}" <<'CHECKPY' || exit 1 import os import re import sys +import zlib -stage, local = sys.argv[1], sys.argv[2] +local, stage, sets = sys.argv[1], sys.argv[2], sys.argv[3:] text = open(local, encoding="utf-8", errors="replace").read() # Nur die drei Werte, die wirklich geheim sind. NINLIGHT_AP_PASSWORD und der # Vorgabetoken stehen oeffentlich im Repo (README, Bruno) - sie hier mitzupruefen # haette nur falsche Alarme erzeugt. SECRETS = ("NINLIGHT_WIFI_SSID", "NINLIGHT_WIFI_PASSWORD", "NINLIGHT_TOKEN") PUBLIC = {"", "ninlight", "aabbccddeeff0011223344", "ninlight-recovery", "change-me-please"} +# Aufbau eines Abbilds: 56 Byte Kopf (vom wm_tool erzeugt), dann der gzip-Strom mit der +# eigentlichen Firmware, dann 512 Byte ninlight-Trailer bzw. RSA-Signatur. +HEADER_LEN = 56 +TRAILER_LEN = 512 needles = [] for name in SECRETS: m = re.search(r'^\s*set\s*\(\s*%s\s+"([^"]*)"' % name, text, re.M) if m and m.group(1) not in PUBLIC: needles.append((name, m.group(1).encode())) + +def unpack(raw): + """Entpackt den gzip-Rumpf eines Abbilds. Wirft bei allem, was nicht passt.""" + if len(raw) <= HEADER_LEN + TRAILER_LEN: + raise ValueError("Datei ist kuerzer als Kopf + Trailer") + dec = zlib.decompressobj(zlib.MAX_WBITS | 16) + body = dec.decompress(raw[HEADER_LEN:-TRAILER_LEN]) + # Ohne diese Abfrage waere ein ABGESCHNITTENER gzip-Strom kein Fehler: decompressobj + # liefert stillschweigend so viel, wie es lesen konnte. Der ungelesene Rest bliebe + # ungeprueft - und genau dort koennte das Passwort stehen. + if not dec.eof: + raise ValueError("gzip-Strom ist unvollstaendig") + return body + + hits = [] -for root, _dirs, files in os.walk(stage): - for fn in sorted(files): - path = os.path.join(root, fn) - with open(path, "rb") as fh: - data = fh.read() - for name, needle in needles: - if needle in data: - hits.append("%s in %s" % (name, os.path.relpath(path, stage))) +broken = [] +images = 0 +for image_set in sets: + root_dir = os.path.join(stage, image_set) + for root, _dirs, files in os.walk(root_dir): + for fn in sorted(files): + path = os.path.join(root, fn) + rel = os.path.relpath(path, stage) + with open(path, "rb") as fh: + raw = fh.read() + # Durchsucht werden beide Sichten: der entpackte Rumpf (dort steht die + # Firmware) UND die Rohbytes (Kopf und Trailer werden nicht komprimiert, + # und Beidateien wie manifest.json gibt es nur roh). + views = [raw] + if fn.endswith(".img"): + images += 1 + try: + views.append(unpack(raw)) + except Exception as exc: + broken.append("%s (%s)" % (rel, exc)) + continue + for name, needle in needles: + if any(needle in view for view in views): + # NUR der Name des Wertes und die Datei - niemals der Wert selbst. + hits.append("%s in %s" % (name, rel)) +if broken: + sys.stderr.write( + "Fehler: Abbild im Bildsatz 'ohne-wlan' laesst sich nicht entpacken, also auch " + "nicht auf Zugangsdaten pruefen - nichts uebertragen:\n " + + "\n ".join(broken) + "\n") + sys.exit(1) if hits: sys.stderr.write( "Fehler: Zugangsdaten aus config.local.cmake im Bildsatz 'ohne-wlan' gefunden, " "nichts uebertragen:\n " + "\n ".join(hits) + "\n") sys.exit(1) -print(" sauber (%d geprueft)" % len(needles)) +print(" sauber (%d Abbilder in %d Saetzen geprueft: %s)" + % (images, len(sets), ", ".join(sets))) CHECKPY else echo " uebersprungen - $LOCAL nicht vorhanden" >&2 diff --git a/tools/station/tests/test_deploy.sh b/tools/station/tests/test_deploy.sh index ae50e11..b86eb95 100755 --- a/tools/station/tests/test_deploy.sh +++ b/tools/station/tests/test_deploy.sh @@ -217,18 +217,63 @@ if with_dev: 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 " +# Baut eine Attrappe mit dem AUFBAU EINES ECHTEN ABBILDS: 56 Byte Kopf, dann ein +# gzip-Strom mit dem Rumpf, dann ggf. 512 Byte Trailer. Genau diesen Aufbau setzt die +# Zugangsdatenpruefung in deploy.sh voraus - eine unkomprimierte Attrappe wuerde die +# Pruefung an einem Abbild vorfuehren, das es so nie gibt, und damit nichts belegen. +# $1 = Pfad, $2 = Fuellzeichen (auch die 56 Kopfbytes, damit Szenarien den Quellbaum +# am Dateianfang wiedererkennen), $3 = "creds", wenn die Zugangsdaten in den +# KOMPRIMIERTEN Rumpf eingebaut werden sollen, $4 = "ota" fuer den NINL-Trailer. +# Ergebnis ohne Trailer ist eine gueltige Eingabe fuer nl_sign.py: >= 1024 Byte, durch 4 +# teilbar, kein NINL im Signaturblock. +make_image_file() { + local path=$1 fill=${2:-A} with_creds=${3:-} kind=${4:-gz} + SSID="$SECRET_SSID" PASS="$SECRET_PASS" TOKEN="$SECRET_TOKEN" \ + python3 - "$path" "$fill" "$with_creds" "$kind" <<'PY' +import gzip +import os 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: + +path, fill, with_creds, kind = sys.argv[1], sys.argv[2].encode(), sys.argv[3], sys.argv[4] + +body = bytearray((fill * 4096)[:4096]) +if with_creds == "creds": + creds = (os.environ["SSID"] + "\0" + os.environ["PASS"] + "\0" + + os.environ["TOKEN"] + "\0").encode() + body[64:64 + len(creds)] = creds + +# mtime=0: sonst haengt die Dateigroesse an der Uhrzeit und Szenarien, die Groessen +# vergleichen, waeren launisch. +packed = gzip.compress(bytes(body), mtime=0) +data = bytearray((fill * 56)[:56] + packed) +# Auffuellen auf die Mindestlaenge und auf ein Vielfaches von 4 - der Rumpf eines echten +# Abbilds ist deutlich groesser als der komprimierte Testrumpf. Die Nullen hinter dem +# gzip-Strom stoeren das Entpacken nicht. +while len(data) < 1024 or len(data) % 4 != 0: + data.append(0) +if kind == "ota": + trailer = bytearray(b"\xff" * 512) + trailer[0:4] = b"NINL" + trailer[4] = 1 + data += trailer +with open(path, "wb") as fh: fh.write(bytes(data)) -" "$path" "$fill" +PY +} + +# Erzeugt eine gueltige rohe _gz.img-Attrappe (ohne Trailer, ohne Zugangsdaten). +make_gz_file() { + make_image_file "$1" "${2:-A}" "" gz +} + +# Erzeugt eine _ota.img-Attrappe im echten Aufbau, ohne Zugangsdaten. +make_ota_image() { + make_image_file "$1" "${2:-A}" "" ota +} + +# Dasselbe, aber mit den Zugangsdaten im komprimierten Rumpf. +make_ota_image_with_creds() { + make_image_file "$1" "${2:-A}" creds ota } # Erzeugt eine _ota.img-Attrappe mit ANDEREM Inhalt als das zugehoerige _gz.img und @@ -270,33 +315,36 @@ EOF # 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" + make_image_file "$1" "${2:-A}" creds gz } -# Sucht eine Zeichenkette BINAER in jeder Datei unterhalb eines Verzeichnisses. -# Rueckgabe 0 = gefunden. Wie contains_secrets() in test_release.sh. +# Sucht eine Zeichenkette in jeder Datei unterhalb eines Verzeichnisses - roh UND im +# ENTPACKTEN Rumpf jedes .img (56 Byte Kopf, gzip, 512 Byte Trailer/Signatur). +# Rueckgabe 0 = gefunden. Wie contains_secrets() in test_release.sh, nur dass die +# Gegenproben seit den echt aufgebauten Attrappen sonst nichts mehr faenden: im +# komprimierten Strom steht der Klartext nicht. Genau das war der Fehler in deploy.sh. tree_contains() { local dir=$1 needle=$2 NEEDLE="$needle" python3 - "$dir" <<'PY' import os import sys +import zlib 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(): + raw = fh.read() + if needle in raw: + sys.exit(0) + if not fn.endswith(".img") or len(raw) <= 56 + 512: + continue + for body in (raw[56:-512], raw[56:]): + try: + clear = zlib.decompressobj(zlib.MAX_WBITS | 16).decompress(body) + except Exception: + continue + if needle in clear: sys.exit(0) sys.exit(1) PY @@ -1559,6 +1607,160 @@ else fail "das neue Rettungsimage fehlt: $OUT" fi +echo +echo "=== Szenario 39: Zugangsdaten im RETTUNGSSATZ 'ohne-wlan' brechen den Deploy ab ===" +# Der Rettungssatz ist der gefaehrlichere der beiden Saetze "ohne-wlan": er geht ohne +# Signatur an die Lampe. Trotzdem hing die Zugangsdatenpruefung frueher allein an +# migrate-nocreds/ - rescue-nocreds/ lief ungeprueft durch. Hier ohne Schluessel, damit +# es NUR um den Rettungssatz geht: es entsteht gar kein migrate-nocreds/. +SANDBOX="$TMPROOT/s39"; STUBBIN="$TMPROOT/bin39"; LOG="$TMPROOT/s39.log"; SANDBOX_STATE="$TMPROOT/s39-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +write_local_cmake "$SANDBOX" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo" +make_ota_image "$SANDBOX/build/foo/ninlight_foo_ota.img" W +# Der undichte Rettungsabzug: die Zugangsdaten stecken im KOMPRIMIERTEN Rumpf. +make_ota_image_with_creds "$SANDBOX/build-nocreds/foo/ninlight_foo_ota.img" N +# GEGENPROBE: ohne sie waere dieses Szenario wertlos - der "Compiler" dieser Suite muss +# die Zugangsdaten wirklich einbauen. +if tree_contains "$SANDBOX/build-nocreds" "$SECRET_PASS"; then + pass "Gegenprobe: der Rettungsabzug enthaelt das WLAN-Passwort wirklich (im gzip-Rumpf)" +else + fail "Gegenprobe fehlgeschlagen - der Rettungsabzug enthaelt gar keine Zugangsdaten, Szenario 39 waere wertlos" +fi +# Und die Gegenprobe zur Gegenprobe: ROH ist nichts davon zu finden. Genau deshalb hat +# die alte Pruefung nie etwas gemeldet. +if grep -q "$SECRET_PASS" "$SANDBOX/build-nocreds/foo/ninlight_foo_ota.img" 2>/dev/null; then + fail "das Passwort steht im Klartext in der Datei - die Attrappe ist nicht komprimiert und belegt nichts" +else + pass "Gegenprobe: roh ist im Abbild nichts zu finden - nur entpackt" +fi +mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds" +echo "alter-sauberer-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(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 Rettungssatz waere ausgeliefert worden: $OUT" +echo "$OUT" | grep -q "NINLIGHT_WIFI_PASSWORD" \ + && pass "Fehlermeldung nennt den betroffenen Schluessel" \ + || fail "Fehlermeldung nennt den Schluessel nicht: $OUT" +echo "$OUT" | grep -q "ninlight_foo_ota.img" \ + && pass "Fehlermeldung nennt die betroffene Datei" \ + || fail "Fehlermeldung nennt die Datei nicht: $OUT" +if echo "$OUT" | grep -q "$SECRET_PASS"; then + fail "die Meldung gibt den WERT aus - sie darf nur den Namen nennen" +else + pass "die Meldung nennt nur den Namen, nie den Wert" +fi +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/rescue-nocreds/ninlight_foo_ota.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 40 (Abgrenzung zu 39): rescue-wlan DARF die Zugangsdaten tragen ===" +# Dieselbe Abgrenzung wie Szenario 34 fuer die Migration: der Satz 'mit-wlan' SOLL die +# Zugangsdaten enthalten. Eine Pruefung, die auch ihn abweist, waere unbenutzbar. +SANDBOX="$TMPROOT/s40"; STUBBIN="$TMPROOT/bin40"; LOG="$TMPROOT/s40.log"; SANDBOX_STATE="$TMPROOT/s40-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +write_local_cmake "$SANDBOX" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo" +make_ota_image_with_creds "$SANDBOX/build/foo/ninlight_foo_ota.img" W +make_ota_image "$SANDBOX/build-nocreds/foo/ninlight_foo_ota.img" N +: >"$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 - die Pruefung weist einen sauberen Rettungssatz ab: $OUT" +[ -f "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan/ninlight_foo_ota.img" ] \ + && pass "rescue-wlan kam trotz einkompilierter Zugangsdaten an" \ + || fail "rescue-wlan wurde faelschlich mit abgewiesen: $OUT" +[ -f "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds/ninlight_foo_ota.img" ] \ + && pass "der saubere Rettungssatz 'ohne-wlan' kam an" \ + || fail "der saubere Rettungssatz 'ohne-wlan' fehlt: $OUT" +if tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/rescue-wlan" "$SECRET_PASS"; then + pass "Gegenprobe: rescue-wlan traegt die Zugangsdaten wirklich - die Pruefung haette anschlagen KOENNEN" +else + fail "Gegenprobe fehlgeschlagen - rescue-wlan traegt gar keine Zugangsdaten, die Abgrenzung waere wertlos" +fi +if ! tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/rescue-nocreds" "$SECRET_PASS"; then + pass "und der Rettungssatz 'ohne-wlan' auf dem Pi ist frei davon" +else + fail "der Rettungssatz 'ohne-wlan' auf dem Pi enthaelt Zugangsdaten" +fi + +echo +echo "=== Szenario 41: ein Abbild, das sich nicht entpacken laesst, bricht den Deploy ab ===" +# Nachsicht an dieser Stelle ("dann eben ungeprueft durchlassen") ist der Weg, auf dem +# das Passwort am Ende doch hinausgeht: ein Abbild in einem anderen Format ist +# genau das, was niemand geprueft hat. +SANDBOX="$TMPROOT/s41"; STUBBIN="$TMPROOT/bin41"; LOG="$TMPROOT/s41.log"; SANDBOX_STATE="$TMPROOT/s41-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +write_local_cmake "$SANDBOX" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo" +make_ota_image "$SANDBOX/build/foo/ninlight_foo_ota.img" W +# Kein gzip-Rumpf: aufgebaut wie ein Abbild (56 Byte Kopf, 512 Byte Trailer), aber der +# Rumpf dazwischen ist unkomprimiert - die alte, unkomprimierte Attrappe dieser Suite. +make_ota_file "$SANDBOX/build-nocreds/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0 - ein ungepruefter Rettungssatz waere ausgeliefert worden: $OUT" +echo "$OUT" | grep -q "nicht entpacken" \ + && pass "Fehlermeldung nennt den Grund" \ + || fail "keine Fehlermeldung zum Entpacken: $OUT" +echo "$OUT" | grep -q "rescue-nocreds/ninlight_foo_ota.img" \ + && pass "Fehlermeldung nennt die betroffene Datei" \ + || fail "Fehlermeldung nennt die Datei 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 ungepruefbarem Abbild uebertragen: $(cat "$LOG")" +fi + +echo +echo "=== Szenario 42: die Meldung sagt, wie viele Abbilder in wie vielen Saetzen geprueft wurden ===" +# Die alte Meldung "sauber (2 geprueft)" zaehlte die SUCHBEGRIFFE und las sich wie zwei +# Dateien - sie haette genauso gelautet, wenn kein einziges Abbild angefasst worden waere. +SANDBOX="$TMPROOT/s42"; STUBBIN="$TMPROOT/bin42"; LOG="$TMPROOT/s42.log"; SANDBOX_STATE="$TMPROOT/s42-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 +make_ota_image "$SANDBOX/build/foo/ninlight_foo_ota.img" W +make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N +make_ota_image "$SANDBOX/build-nocreds/foo/ninlight_foo_ota.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: $OUT" +# migrate-nocreds/: foo_prod.img + foo_dev.img, rescue-nocreds/: ninlight_foo_ota.img. +echo "$OUT" | grep -q "sauber (3 Abbilder in 2 Saetzen geprueft" \ + && pass "die Meldung nennt Zahl der Abbilder UND der Saetze" \ + || fail "die Meldung sagt nicht, was geprueft wurde: $OUT" +echo "$OUT" | grep -q "migrate-nocreds, rescue-nocreds" \ + && pass "die Meldung nennt beide geprueften Saetze beim Namen" \ + || fail "die Meldung nennt die Saetze nicht: $OUT" + echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden."