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.
351 lines
16 KiB
Bash
Executable File
351 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Liefert Firmware-Images und den Stationscode an den Pi aus.
|
|
#
|
|
# Aufruf: tools/station/deploy.sh <pi-host> [target ...]
|
|
# Ohne target-Argumente werden alle in build/*/ gefundenen _ota.img uebertragen.
|
|
set -euo pipefail
|
|
HOST=${1:?Aufruf: tools/station/deploy.sh <pi-host> [target ...]}
|
|
shift || true
|
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
ROOT=$(cd "$HERE/../.." && pwd)
|
|
MKIMAGE="$ROOT/tools/mkimage/nl_mkimage.py"
|
|
# /usr/bin/ssh statt einem ssh im PATH, das sich anders verhaelt (siehe A3-Notiz im
|
|
# Abnahmebericht). Ueberschreibbar, damit tests/test_deploy.sh gegen einen Stub statt
|
|
# einen echten Pi laufen kann - dasselbe Prinzip wie bei NINLIGHT_STATION_ROOT unten.
|
|
SSH_BIN=${NINLIGHT_SSH_BIN:-/usr/bin/ssh}
|
|
|
|
declare -a FILES=()
|
|
if [ $# -gt 0 ]; then
|
|
# Gezielter Deploy: nur die genannten Geraetetypen werden ersetzt, alle
|
|
# anderen Images im Zielverzeichnis bleiben unangetastet (siehe FULL_SYNC
|
|
# unten).
|
|
FULL_SYNC=0
|
|
for t in "$@"; do
|
|
f="$ROOT/build/$t/ninlight_${t}_ota.img"
|
|
[ -f "$f" ] || { echo "Fehler: $f fehlt - erst bauen" >&2; exit 1; }
|
|
FILES+=("$f")
|
|
done
|
|
else
|
|
FULL_SYNC=1
|
|
# Zwei Build-Verzeichnisse koennen denselben Image-Dateinamen liefern (z.B.
|
|
# build/evk/ und ein Nebenschauplatz wie build/evk-onb/, das ohne WLAN-
|
|
# Zugangsdaten gebaut wurde). Landen beide flach im selben STAGE/$base,
|
|
# entscheidet allein die Sortierreihenfolge, welches gewinnt - und die
|
|
# Trailer-Pruefung merkt nichts, weil in beiden derselbe Geraetetyp steht.
|
|
# Ein zweiter Fundort fuer denselben Dateinamen ist deshalb ein Abbruch,
|
|
# kein Zufall.
|
|
declare -A SEEN_AT=()
|
|
while IFS= read -r f; do
|
|
base=$(basename "$f")
|
|
if [ -n "${SEEN_AT[$base]:-}" ]; then
|
|
echo "Fehler: $base mehrfach gefunden - ${SEEN_AT[$base]} und $f. Voller Deploy abgebrochen, welches Bild echt ist, ist nicht entscheidbar. Nicht benoetigtes Build-Verzeichnis loeschen oder gezielt deployen." >&2
|
|
exit 1
|
|
fi
|
|
SEEN_AT[$base]=$f
|
|
FILES+=("$f")
|
|
done < <(find "$ROOT/build" -maxdepth 2 -name 'ninlight_*_ota.img' | sort)
|
|
[ ${#FILES[@]} -gt 0 ] || { echo "Fehler: keine Images unter $ROOT/build gefunden" >&2; exit 1; }
|
|
fi
|
|
|
|
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}
|
|
# Ein Image, das die Pruefung nicht besteht, wuerde jede Lampe ablehnen -
|
|
# das faellt sonst erst am Geraet auf.
|
|
out=$(python3 "$MKIMAGE" --verify "$f" --expect-device "$target") || {
|
|
echo "Fehler: $base besteht die Trailer-Pruefung nicht, nichts uebertragen" >&2; exit 1; }
|
|
# 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).
|
|
# ${HOME:-} statt $HOME: unter `set -u` (z.B. via cron oder env -i) waere ein
|
|
# unbound-variable-Abbruch hier voellig unnoetig, denn selbst ohne Migrationsimages
|
|
# sollen die Rettungsimages ausgeliefert werden (Fixrunde 1, Kleinigkeit 1).
|
|
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"
|
|
# MIGRATE_COMPLETE=1 bedeutet: dieser Lauf hat mit BEIDEN Schluesseln signiert, das
|
|
# lokale STAGE/migrate beschreibt also den vollstaendigen Satz an Migrationsvarianten
|
|
# fuer FILES. Nur dann darf der Abgleich auf dem Pi mit --delete arbeiten bzw. das
|
|
# Manifest blind ersetzen - sonst wuerden Images/Eintraege einer Variante verschwinden,
|
|
# die dieser Lauf mangels Schluessel gar nicht neu bauen konnte (Fixrunde 1, C2/I1).
|
|
MIGRATE_COMPLETE=0
|
|
|
|
if [ -d "$SIGN_DIR" ]; then
|
|
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
|
|
if [ "$HAVE_PROD" -eq 1 ] || [ "$HAVE_DEV" -eq 1 ]; then
|
|
# Erst HIER, nachdem feststeht dass ueberhaupt signiert wird, STAGE/migrate anlegen
|
|
# und das fehlende _gz.img zum Abbruch machen (Fixrunde 1, C2+C3): ein
|
|
# Schluesselverzeichnis ohne jeden Schluessel darf weder ein leeres migrate/
|
|
# erzeugen (das wuerde ein vorhandenes migrate/ auf dem Pi mit --delete leerraeumen)
|
|
# noch am Fehlen des _gz.img scheitern, wenn gar nicht signiert werden soll.
|
|
mkdir -p "$STAGE/migrate"
|
|
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
|
|
if [ "$HAVE_PROD" -eq 1 ] && [ "$HAVE_DEV" -eq 1 ]; then
|
|
MIGRATE_COMPLETE=1
|
|
fi
|
|
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. Form haelt sich an die Spec
|
|
# (docs/superpowers/spec, Abschnitt 4) und damit an das, was server.py tatsaechlich
|
|
# liest. Fixrunde 2, N2: "ninlight_version" steht PRO EINTRAG, nicht (mehr nur)
|
|
# manifestweit - ein einziges Feld fuer das ganze Manifest war schon falsch, sobald
|
|
# der Merge unten (bei nur einem Schluessel oder gezieltem Deploy) alte und neue
|
|
# Eintraege mit unterschiedlichen Firmware-Versionen zusammenfuehrt. Das
|
|
# manifestweite Feld bleibt nur als Bestandskompatibilitaet erhalten und wird nur
|
|
# gesetzt, wenn wirklich alle Eintraege dieselbe Version tragen.
|
|
python3 - "$STAGE/migrate" "${VERSION_ARGS[@]}" <<'PY'
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
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,
|
|
"file": name,
|
|
"bytes": len(data),
|
|
"sha256": hashlib.sha256(data).hexdigest(),
|
|
"ninlight_version": version_map.get(target, ""),
|
|
})
|
|
|
|
manifest = {
|
|
"built": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"images": entries,
|
|
}
|
|
versions = {e["ninlight_version"] for e in entries}
|
|
if len(versions) == 1:
|
|
(version,) = versions
|
|
if version:
|
|
manifest["ninlight_version"] = version
|
|
with open(os.path.join(migrate_dir, "manifest.json"), "w") as fh:
|
|
json.dump(manifest, fh, indent=2, sort_keys=True)
|
|
fh.write("\n")
|
|
PY
|
|
else
|
|
echo "Hinweis: $SIGN_DIR enthaelt weder Produktions- noch Entwicklungsschluessel - keine Migrationsimages erzeugt" >&2
|
|
fi
|
|
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
|
|
# schnell aufeinanderfolgenden Deploys reicht rsyncs Sekundenaufloesung fuer
|
|
# den Zeitstempel nicht, um das als Aenderung zu erkennen. Ohne -c wuerde
|
|
# rsync ein solches Update dann stillschweigend ueberspringen.
|
|
#
|
|
# /tmp/ninlight-images auf dem Pi ist nur eine fluechtige Ablage fuer diesen
|
|
# Lauf - hier darf --delete unbedingt ran, das betrifft nicht das live
|
|
# ausgelieferte Verzeichnis.
|
|
# -e: rsync soll fuer den Transport zum Pi denselben ssh wie oben benutzen.
|
|
rsync -a -c --delete -e "$SSH_BIN" "$STAGE/" "$HOST:/tmp/ninlight-images/"
|
|
rsync -a -e "$SSH_BIN" "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:/tmp/"
|
|
# Pfade als Variablen mit Default: so kann ein Test die Installation gegen
|
|
# ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen lassen.
|
|
"$SSH_BIN" "$HOST" bash -s -- "$FULL_SYNC" "$MIGRATE_COMPLETE" <<'REMOTE'
|
|
set -e
|
|
FULL_SYNC=$1
|
|
MIGRATE_COMPLETE=$2
|
|
ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station}
|
|
BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
|
|
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
|
|
|
|
sudo install -d -m 0755 "$ROOT_DIR/images"
|
|
if [ "$FULL_SYNC" -eq 1 ]; then
|
|
# Vollabgleich: alle Images ausserhalb der aktuellen Auslieferung entfernen.
|
|
# Vorher zeigen, WELCHE das sind - sonst faellt ein geloeschtes Image erst
|
|
# auf, wenn jemand mit einer toten Lampe vor der Station steht. Ermittelt
|
|
# per einfachem Namensvergleich (portabel, kein Verlass auf rsyncs
|
|
# "deleting"-Ausgabe, die ohne -v gar nicht erscheint).
|
|
OLD_LIST=$(find "$ROOT_DIR/images" -maxdepth 1 -name 'ninlight_*_ota.img' -exec basename {} \; 2>/dev/null | sort)
|
|
NEW_LIST=$(find /tmp/ninlight-images -maxdepth 1 -name 'ninlight_*_ota.img' -exec basename {} \; | sort)
|
|
REMOVED=""
|
|
if [ -n "$OLD_LIST" ]; then
|
|
REMOVED=$(comm -23 <(printf '%s\n' "$OLD_LIST") <(printf '%s\n' "$NEW_LIST"))
|
|
fi
|
|
if [ -n "$REMOVED" ]; then
|
|
echo " voller Abgleich - folgende Images werden entfernt:"
|
|
echo "$REMOVED" | sed 's/^/ /'
|
|
else
|
|
echo " voller Abgleich - keine Images werden entfernt"
|
|
fi
|
|
# 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 --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/"
|
|
fi
|
|
# Migrationsimages getrennt behandeln: fehlt migrate/ in der Uebertragung (weil auf
|
|
# dem Entwicklungsrechner kein Schluesselverzeichnis oder gar kein Schluessel
|
|
# 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 ] && [ "$MIGRATE_COMPLETE" -eq 1 ]; then
|
|
# Nur wenn dieser Lauf mit BEIDEN Schluesseln signiert hat, beschreibt
|
|
# STAGE/migrate den vollstaendigen Soll-Zustand - erst dann darf --delete
|
|
# ueberzaehlige Dateien wegraeumen und manifest.json blind ersetzt werden.
|
|
sudo rsync -a -c --delete /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
|
else
|
|
# Gezielter Deploy ODER ein Voll-Deploy mit nur einem der beiden Schluessel:
|
|
# in beiden Faellen kennt dieser Lauf nur einen Teil dessen, was auf dem Pi liegen
|
|
# soll. rsync ohne --delete laesst alles Nicht-Uebertragene unangetastet (Fixrunde
|
|
# 1, C2 - sonst raeumt ein Voll-Deploy mit nur dem dev-Schluessel saemtliche
|
|
# _prod.img weg). manifest.json wird deshalb auch nicht einfach ueberschrieben,
|
|
# sondern mit dem, was schon da ist, zusammengefuehrt (Fixrunde 1, I1 - sonst
|
|
# verschwinden die Eintraege aller Ziele/Varianten, die dieser Lauf nicht neu
|
|
# gebaut hat, obwohl ihre Images unveraendert liegen bleiben).
|
|
sudo rsync -a -c --exclude=/manifest.json /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/"
|
|
if [ -f /tmp/ninlight-images/migrate/manifest.json ]; then
|
|
sudo python3 - "$ROOT_DIR/images/migrate/manifest.json" /tmp/ninlight-images/migrate/manifest.json <<'MERGE'
|
|
import json
|
|
import sys
|
|
|
|
manifest_path, new_manifest_path = sys.argv[1], sys.argv[2]
|
|
with open(new_manifest_path) as fh:
|
|
new_manifest = json.load(fh)
|
|
if not isinstance(new_manifest, dict):
|
|
new_manifest = {}
|
|
try:
|
|
with open(manifest_path) as fh:
|
|
old_manifest = json.load(fh)
|
|
except (OSError, ValueError):
|
|
old_manifest = {}
|
|
# Fixrunde 2, N1: ein BESTEHENDES manifest.json kann gueltiges JSON sein und trotzdem
|
|
# die falsche Form haben (z.B. "[]", "null", ein String, oder ein Eintrag, der selbst
|
|
# kein Objekt ist) - das darf niemals mitten im Deploy mit AttributeError abbrechen,
|
|
# denn dieser Merge steht 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.
|
|
if not isinstance(old_manifest, dict):
|
|
old_manifest = {}
|
|
old_images = old_manifest.get("images")
|
|
if not isinstance(old_images, list):
|
|
old_images = []
|
|
by_key = {(e.get("target"), e.get("variant")): e for e in old_images if isinstance(e, dict)}
|
|
new_images = new_manifest.get("images")
|
|
if not isinstance(new_images, list):
|
|
new_images = []
|
|
for e in new_images:
|
|
if isinstance(e, dict):
|
|
by_key[(e.get("target"), e.get("variant"))] = e
|
|
merged_images = sorted(by_key.values(), key=lambda e: (e.get("target") or "", e.get("variant") or ""))
|
|
merged = {
|
|
"built": new_manifest.get("built"),
|
|
"images": merged_images,
|
|
}
|
|
# Fixrunde 2, N2: das manifestweite "ninlight_version" darf nach dem Merge nicht mehr
|
|
# einfach vom neuen Manifest uebernommen werden - die uebernommenen Alt-Eintraege
|
|
# koennen eine andere Version tragen (das war die alte Version dieses Bugs: ein
|
|
# Voll-Deploy mit nur dem dev-Schluessel hat die neue Firmware-Version ueber ein
|
|
# unveraendert liegen gebliebenes altes _prod.img geschrieben). Jeder Eintrag traegt
|
|
# seine eigene Version; das Top-Level-Feld ist nur noch eine Bequemlichkeit fuer den
|
|
# Fall, dass wirklich alle Eintraege uebereinstimmen.
|
|
versions = {e.get("ninlight_version") for e in merged_images}
|
|
if len(versions) == 1:
|
|
(version,) = versions
|
|
if isinstance(version, str) and version:
|
|
merged["ninlight_version"] = version
|
|
with open(manifest_path, "w") as fh:
|
|
json.dump(merged, fh, indent=2, sort_keys=True)
|
|
fh.write("\n")
|
|
MERGE
|
|
fi
|
|
fi
|
|
else
|
|
# Weder "kein Schluesselverzeichnis" noch "Verzeichnis vorhanden, aber leer" laesst
|
|
# sich von hier aus unterscheiden - beide Faelle liefern ein leeres migrate/ von
|
|
# diesem Rechner. Die lokale Meldung (deploy.sh, "Hinweis: ...") ist bereits genau;
|
|
# diese hier behauptet absichtlich keinen bestimmten Grund (Fixrunde 2, Kleinigkeit).
|
|
echo " keine Migrationsimages von diesem Rechner - 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"
|
|
sudo install -m 0755 /tmp/ninlight-station "$BIN_PATH"
|
|
sudo install -m 0644 /tmp/ninlight-station.service "$UNIT_PATH"
|
|
rm -rf /tmp/ninlight-images /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/ninlight-station.service
|
|
sudo systemctl daemon-reload
|
|
if systemctl is-active --quiet ninlight-station; then
|
|
if sudo systemctl restart ninlight-station; then
|
|
echo " Dienst neu gestartet"
|
|
else
|
|
echo " Fehler: Dienst lief, Neustart nach dem Deploy ist aber fehlgeschlagen" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo " Dienst laeuft nicht (Station ist aus)"
|
|
fi
|
|
REMOTE
|
|
|
|
echo "fertig."
|