server.py bedient jetzt zwei Modi ueber denselben Dienst: --mode/--mode-file/--control legen fest, ob ninlight_<target>_ota.img (Rettung) oder qcx_firmware_<pid>_..._ota_light.img (Migration alter Klyqa-Lampen, via products.parse_request) ausgeliefert wird. Der Modus kommt wahlweise aus einer Datei statt einem Startparameter, damit Task 4 ihn spaeter ohne Dienst-Neustart umschalten kann. Die Steuerdatei control.json (Phase auto/listen/armed) wird bei jeder Anfrage frisch gelesen und atomar mit 0644 geschrieben - eine fehlende oder kaputte Steuerdatei blockiert eine Migration nie, sondern faellt auf "auto" zurueck. Nach einer erfolgreichen Auslieferung in Phase "armed" faellt die Freigabe erst nach dem vollstaendigen Senden auf "listen" zurueck, nie davor, damit ein abgebrochener Transfer die Freigabe fuer den naechsten Bootversuch stehen laesst. status.json bekommt die Migrationsfelder aus Spec 5.2 (mode, phase, armed_target, last_seen). deploy.sh liefert products.py mit aus, sonst startet der Dienst nach dem naechsten Deploy mit ModuleNotFoundError; test_deploy.sh zieht die Sandbox-Dummy-Datei entsprechend nach. Neue Tests in test_server.py verwenden eigene server/migration_server-Fixtures samt Testklient (get/status/set_control/read_control/delete_control/write_raw_control), ohne die bestehende station-Fixture und ihre Tests anzufassen.
134 lines
6.0 KiB
Bash
Executable File
134 lines
6.0 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
|
|
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"
|
|
cp "$f" "$STAGE/$base"
|
|
done
|
|
|
|
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" <<'REMOTE'
|
|
set -e
|
|
FULL_SYNC=$1
|
|
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
|
|
sudo rsync -a -c --delete /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/"
|
|
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."
|