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.
This commit is contained in:
+106
-2
@@ -51,6 +51,7 @@ echo "== Images pruefen =="
|
||||
STAGE=$(mktemp -d)
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
printf '%-26s %-10s %s\n' GERAET VERSION BYTES
|
||||
declare -A NINL_VERSION=()
|
||||
for f in "${FILES[@]}"; do
|
||||
base=$(basename "$f")
|
||||
target=${base#ninlight_}; target=${target%_ota.img}
|
||||
@@ -61,9 +62,93 @@ for f in "${FILES[@]}"; do
|
||||
# shellcheck disable=SC2086
|
||||
set -- $out
|
||||
printf '%-26s %-10s %s\n' "$1" "$2" "$3"
|
||||
NINL_VERSION[$target]="$2"
|
||||
cp "$f" "$STAGE/$base"
|
||||
done
|
||||
|
||||
echo "== Migrationsimages signieren =="
|
||||
# Die Signierschluessel gehoeren nie ins Repo und nie auf den Pi - sie werden hier nur
|
||||
# gelesen, unter $NINLIGHT_SIGN_DIR (Vorgabe ~/workspace/fw-sign auf DIESEM Rechner).
|
||||
# Fehlt das Verzeichnis oder ein einzelner Schluessel, entfaellt die jeweilige
|
||||
# Migrationsvariante mit einer Meldung - die Rettungsimages (_ota.img) werden trotzdem
|
||||
# ausgeliefert (Exit-Code bleibt 0).
|
||||
SIGN_DIR=${NINLIGHT_SIGN_DIR:-$HOME/workspace/fw-sign}
|
||||
KEY_PROD="$SIGN_DIR/QCX_Device_CA/qcx_firmware_signing_key.pem"
|
||||
KEY_DEV="$SIGN_DIR/fw_sign.pem"
|
||||
NL_SIGN="$ROOT/tools/mkimage/nl_sign.py"
|
||||
|
||||
if [ -d "$SIGN_DIR" ]; then
|
||||
mkdir -p "$STAGE/migrate"
|
||||
HAVE_PROD=0
|
||||
HAVE_DEV=0
|
||||
if [ -f "$KEY_PROD" ]; then
|
||||
HAVE_PROD=1
|
||||
else
|
||||
echo "Hinweis: Produktionsschluessel fehlt - keine _prod.img-Migrationsimages" >&2
|
||||
fi
|
||||
if [ -f "$KEY_DEV" ]; then
|
||||
HAVE_DEV=1
|
||||
else
|
||||
echo "Hinweis: Entwicklungsschluessel fehlt - keine _dev.img-Migrationsimages" >&2
|
||||
fi
|
||||
for f in "${FILES[@]}"; do
|
||||
base=$(basename "$f")
|
||||
target=${base#ninlight_}; target=${target%_ota.img}
|
||||
# Signiert wird IMMER das rohe _gz.img, niemals das fertige _ota.img: beide belegen
|
||||
# denselben 512-Byte-Block am Dateiende (Trailer bzw. Signatur), ein signiertes
|
||||
# _ota.img waere ein Image, das die alte Lampe ablehnt oder halb annimmt.
|
||||
gz="$ROOT/build/$target/ninlight_${target}_gz.img"
|
||||
[ -f "$gz" ] || {
|
||||
echo "Fehler: $gz fehlt - kein Migrationsimage fuer $target moeglich" >&2; exit 1; }
|
||||
if [ "$HAVE_PROD" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_PROD" --in "$gz" --out "$STAGE/migrate/${target}_prod.img" \
|
||||
|| { echo "Fehler: Signieren von $target (prod) fehlgeschlagen" >&2; exit 1; }
|
||||
fi
|
||||
if [ "$HAVE_DEV" -eq 1 ]; then
|
||||
python3 "$NL_SIGN" sign --key "$KEY_DEV" --in "$gz" --out "$STAGE/migrate/${target}_dev.img" \
|
||||
|| { echo "Fehler: Signieren von $target (dev) fehlgeschlagen" >&2; exit 1; }
|
||||
fi
|
||||
done
|
||||
VERSION_ARGS=()
|
||||
for t in "${!NINL_VERSION[@]}"; do
|
||||
VERSION_ARGS+=("$t=${NINL_VERSION[$t]}")
|
||||
done
|
||||
# manifest.json aus den entstandenen Dateien - Groesse und sha256 werden hier neu
|
||||
# berechnet statt aus dem Signierschritt uebernommen, damit die Datei die Wahrheit
|
||||
# ueber das tatsaechlich abgelegte Image sagt.
|
||||
python3 - "$STAGE/migrate" "${VERSION_ARGS[@]}" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
migrate_dir = sys.argv[1]
|
||||
version_map = dict(a.split("=", 1) for a in sys.argv[2:])
|
||||
entries = []
|
||||
for name in sorted(os.listdir(migrate_dir)):
|
||||
if name.endswith("_prod.img"):
|
||||
target, variant = name[: -len("_prod.img")], "prod"
|
||||
elif name.endswith("_dev.img"):
|
||||
target, variant = name[: -len("_dev.img")], "dev"
|
||||
else:
|
||||
continue
|
||||
with open(os.path.join(migrate_dir, name), "rb") as fh:
|
||||
data = fh.read()
|
||||
entries.append({
|
||||
"target": target,
|
||||
"variant": variant,
|
||||
"bytes": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
"ninlight_version": version_map.get(target, ""),
|
||||
})
|
||||
with open(os.path.join(migrate_dir, "manifest.json"), "w") as fh:
|
||||
json.dump({"images": entries}, fh, indent=2, sort_keys=True)
|
||||
fh.write("\n")
|
||||
PY
|
||||
else
|
||||
echo "Hinweis: Schluesselverzeichnis $SIGN_DIR nicht gefunden - keine Migrationsimages erzeugt" >&2
|
||||
fi
|
||||
|
||||
echo "== uebertragen =="
|
||||
# -c (Inhaltsvergleich per Checksumme statt Groesse+Zeitstempel): ein neues
|
||||
# Image kann zufaellig dieselbe Byte-Groesse wie das alte haben, und bei
|
||||
@@ -105,12 +190,31 @@ if [ "$FULL_SYNC" -eq 1 ]; then
|
||||
else
|
||||
echo " voller Abgleich - keine Images werden entfernt"
|
||||
fi
|
||||
sudo rsync -a -c --delete /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
# migrate/ wird gleich in einem eigenen Schritt behandelt (siehe unten) - hier
|
||||
# ausklammern, sonst wuerde --delete es leeren, wenn dieser Deploy ohne
|
||||
# Schluesselverzeichnis lief und /tmp/ninlight-images/migrate deshalb gar nicht
|
||||
# existiert.
|
||||
sudo rsync -a -c --delete --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
else
|
||||
# Gezielter Deploy: nur die uebertragenen Dateien ersetzen/ergaenzen, alle
|
||||
# anderen Geraetetypen im Zielverzeichnis bleiben erhalten (kein --delete).
|
||||
echo " gezielter Deploy - andere Geraetetypen im Zielverzeichnis bleiben erhalten"
|
||||
sudo rsync -a -c /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
sudo rsync -a -c --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
||||
fi
|
||||
# Migrationsimages getrennt behandeln: fehlt migrate/ in der Uebertragung (weil auf
|
||||
# dem Entwicklungsrechner kein Schluesselverzeichnis vorhanden war), darf ein
|
||||
# vorhandenes migrate/ auf dem Pi NICHT geleert werden - sonst zerstoert ein Deploy
|
||||
# ohne Schluesselzugriff die Bestueckung frueherer Deploys (derselbe Fehler wie
|
||||
# frueher bei den _ota.img, siehe oben, nur diesmal beim Fehlen statt beim Ersetzen).
|
||||
if [ -d /tmp/ninlight-images/migrate ]; then
|
||||
sudo install -d -m 0755 "$ROOT_DIR/images/migrate"
|
||||
if [ "$FULL_SYNC" -eq 1 ]; then
|
||||
sudo rsync -a -c --delete /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
||||
else
|
||||
sudo rsync -a -c /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
||||
fi
|
||||
else
|
||||
echo " kein Schluesselverzeichnis auf dem Entwicklungsrechner - migrate/ auf dem Pi bleibt unangetastet"
|
||||
fi
|
||||
sudo install -m 0755 /tmp/server.py "$ROOT_DIR/server.py"
|
||||
sudo install -m 0644 /tmp/products.py "$ROOT_DIR/products.py"
|
||||
|
||||
Reference in New Issue
Block a user