fix(station): Manifest-Merge robust gegen Fehlform, Version pro Eintrag
Fixrunde 2 zu "Migrationsimages beim Deploy signieren":
- N1 (Important): das Merge-Skript im SSH-Heredoc von deploy.sh fing beim
Bestandsmanifest nur Lesefehler und kaputtes JSON ab (OSError/ValueError).
War die Datei gueltiges JSON mit falscher Form ("[]", "null", ein String,
oder ein images-Eintrag, der selbst kein Objekt ist), krachte .get() mit
AttributeError - und das VOR der Installation von
server.py/products.py/CLI/Unit und vor dem Neustart, liess einen Pi also
mit neuen Images, aber altem Stationscode zurueck. Jetzt wird alte wie
neue Manifest-Struktur konsequent auf isinstance(dict)/isinstance(list)
geprueft, jede Fehlform faellt auf ein leeres Manifest zurueck statt
abzustuerzen. Neues Szenario 21 deckt alle vier Faelle aus der
Review-Tabelle ab (kaputt, leer, falsch geformtes JSON, kaputter Eintrag).
- N2 (Important): das zusammengefuehrte Manifest hat "ninlight_version"
bisher blind vom neuen Manifest uebernommen, obwohl uebernommene
Alt-Eintraege (deren Images unveraendert liegen bleiben, siehe Fixrunde 1
I1) eine andere Firmware-Version tragen koennen - eine konkrete falsche
Version beim Protokollieren ist schlechter als das vorherige "unbekannt".
Die Version wandert jetzt PRO EINTRAG ins Manifest (deploy.sh erzeugt sie
dort, der Merge behaelt sie); das manifestweite Feld bleibt nur als
Bestandskompatibilitaet und wird nur gesetzt, wenn wirklich alle Eintraege
uebereinstimmen. server.py liest beim Ausliefern jetzt die Version DES
AUSGELIEFERTEN EINTRAGS (Station.image_version(target, variant) ersetzt
manifest_version()) und faellt nur auf das manifestweite Feld zurueck,
wenn der Eintrag selbst keine eigene traegt. test_server.py bekommt einen
neuen Test fuer den Vorrang des Eintrags vor dem manifestweiten Feld,
Szenario 14 in test_deploy.sh prueft jetzt zusaetzlich unterschiedliche
Versionen zwischen zusammengefuehrten Eintraegen und dass das
manifestweite Feld dann fehlt statt falsch zu sein. Die Spec
(docs/superpowers/specs/.../design.md, Abschnitt 4, lokal/nicht
versioniert) ist nachgezogen. Die dadurch ueberfluessig gewordene
"sorted(versions)[0]"-Stelle aus Fixrunde 1 ist entfallen.
- Kleinigkeit: die Remote-Meldung, wenn migrate/ von diesem Rechner leer
ist, behauptet nicht mehr "kein Schluesselverzeichnis" (das stimmt im
Fall "Verzeichnis vorhanden, aber leer" nicht) - sie nennt jetzt keinen
bestimmten Grund mehr, nur die Konsequenz.
- Kleinigkeit: --exclude=manifest.json ist jetzt --exclude=/manifest.json,
konsistent mit dem daneben verankerten --exclude=/migrate.
Alle drei Aenderungen (N1, N2, beide Kleinigkeiten) sind vor dem Fix per
Rot-Probe an einer Kopie von deploy.sh verifiziert: mit den urspruenglichen
Fehlern schlagen Szenario 21 (N1) bzw. Szenario 14 (N2) zuverlaessig fehl.
Bewusst nicht angefasst (Review-Vorgabe): der fehlende HOME-Testfall, und
dass der Merge kein os.path.exists() prueft.
This commit is contained in:
@@ -504,10 +504,11 @@ else
|
||||
fi
|
||||
set +e
|
||||
# Form haelt sich an die Spec (docs/superpowers/spec Abschnitt 4) und an das, was
|
||||
# server.py tatsaechlich liest (Fixrunde 1, C1): "ninlight_version" auf oberster
|
||||
# Ebene, nicht pro Eintrag - ein reines json.load haette das nicht gemerkt, weil es
|
||||
# nur die Feldnamen INNERHALB der Eintraege prueft. Die naechste Pruefung (Szenario
|
||||
# 20) schickt dieselbe Datei zusaetzlich durch den echten Server-Leser.
|
||||
# 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:
|
||||
@@ -522,13 +523,13 @@ ok = (
|
||||
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('ninlight_version' not in e 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 und target/variant/file/bytes/sha256 je Eintrag"
|
||||
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
|
||||
@@ -608,12 +609,17 @@ 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"
|
||||
# Manifest eines frueheren Deploys, das bar beschrieben hat - genau das darf ein
|
||||
# gezielter Deploy von foo nicht einfach wegschreiben (Fixrunde 1, I1).
|
||||
# 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/manifest.json" <<'JSON'
|
||||
{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.0.1",
|
||||
{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.9.0",
|
||||
"images": [
|
||||
{"target": "bar", "variant": "prod", "file": "bar_prod.img", "bytes": 20, "sha256": "x"},
|
||||
{"target": "bar", "variant": "dev", "file": "bar_dev.img", "bytes": 19, "sha256": "y"}
|
||||
{"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"
|
||||
@@ -639,15 +645,33 @@ MANIFEST_CHECK=$(python3 -c "
|
||||
import json
|
||||
with open('$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json') as fh:
|
||||
data = json.load(fh)
|
||||
keys = {(e.get('target'), e.get('variant')) for e in data.get('images', [])}
|
||||
entries = data.get('images', [])
|
||||
keys = {(e.get('target'), e.get('variant')) for e in entries}
|
||||
expected = {('foo', 'prod'), ('foo', 'dev'), ('bar', 'prod'), ('bar', 'dev')}
|
||||
print('OK' if keys == expected else 'FAIL: ' + json.dumps(sorted(keys)))
|
||||
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 (foo neu, bar zusammengefuehrt)"
|
||||
pass "manifest.json beschreibt nach dem gezielten Deploy weiterhin BEIDE Geraetetypen mit je eigener Version, ohne falsches manifestweites Feld"
|
||||
else
|
||||
fail "manifest.json verliert Eintraege beim gezielten Deploy: $MANIFEST_CHECK"
|
||||
fail "manifest.json-Inhalt nach dem gezielten Deploy unerwartet: $MANIFEST_CHECK"
|
||||
fi
|
||||
|
||||
echo
|
||||
@@ -829,8 +853,10 @@ echo "=== Szenario 20 (Fixrunde 1, C1): manifest.json wird vom ECHTEN Server-Les
|
||||
# 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.manifest_version() - genau die Funktion, die im echten Betrieb entscheidet,
|
||||
# welche Version protokolliert und angezeigt wird.
|
||||
# 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"
|
||||
@@ -865,16 +891,63 @@ st = module.Station(
|
||||
mode='migration',
|
||||
control_path=Path('$TMPROOT/s20-control.json'),
|
||||
)
|
||||
print(st.manifest_version())
|
||||
print(st.image_version('foo', 'dev'))
|
||||
" 2>&1)
|
||||
RC2=$?
|
||||
set -e
|
||||
if [ "$RC2" -eq 0 ] && [ "$VERSION" = "1.0.0" ]; then
|
||||
pass "Station.manifest_version() (der echte Server-Code) liest die Version korrekt, nicht 'unbekannt'"
|
||||
pass "Station.image_version() (der echte Server-Code) liest die Version korrekt, nicht 'unbekannt'"
|
||||
else
|
||||
fail "Station.manifest_version() liefert '$VERSION' statt '1.0.0' (RC=$RC2) - Manifest-Form stimmt nicht mit dem Server ueberein"
|
||||
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"
|
||||
cp "$TMPROOT/bad-manifests/$label.json" "$SANDBOX_STATE/opt/ninlight-station/images/migrate/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/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
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "Alle Tests bestanden."
|
||||
|
||||
Reference in New Issue
Block a user