Fixrunde 2 zu Task 4:
- N1 (wichtig): file_mode() in test_cli.sh/test_setup_pi.sh probierte
"stat -f '%Lp'" vor "stat -c '%a'" - auf GNU-coreutils (dem Pi) deutet
"-f" dort "Dateisystemstatus" und liefert Muell statt der Rechte-Zahl,
faellt aber trotzdem in den "||"-Zweig durch, der zusaetzlich "644"
anhaengt. Reihenfolge umgedreht und mit einem per gstat erzwungenen
GNU-Testlauf zusaetzlich abgesichert.
- N2 (wichtig): status zeigte nach einem Wechsel zurueck in den
Rettungsmodus weiterhin die letzte Migrationsphase aus einer liegen
gebliebenen control.json (moeglich seit RuntimeDirectoryPreserve=
restart aus Fixrunde 1). Phase wird jetzt nur noch angezeigt, wenn
Modus tatsaechlich "migration" ist.
- N3 (klein): die Schluesselerkennung in setup_pi.sh war rein
zeilenverankert ("^KEY=") und haette einen eingerueckten Bestandswert
als fehlend gewertet - beim naechsten Sourcen der Konfiguration haette
die zusaetzlich angehaengte Vorlagenzeile das eigene Passwort
ueberschrieben. Muster auf fuehrenden Leerraum/export gehaertet.
- N4 (klein): test_setup_pi.sh liess setup_pi.sh ueber den scp-Stub echte
Dateien im /tmp des Entwicklungsrechners anlegen und wieder loeschen.
Neue Variable NINLIGHT_STAGE_DIR biegt das auf eine Sandbox um.
Alle vier Punkte mit Regressionstests, N2 und N3 zusaetzlich per
Mutationsprobe (Fix zurueckgenommen, Test wird rot) verifiziert.
171 lines
7.8 KiB
Bash
Executable File
171 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Richtet einen Raspberry Pi als ninlight-Station ein. Idempotent: mehrfaches
|
|
# Ausfuehren aendert nichts und schadet nicht - auch nicht auf einem Pi, auf dem
|
|
# schon eine aeltere Version dieses Skripts gelaufen ist (siehe "Konfiguration"
|
|
# unten: fehlende Schluessel werden ergaenzt, vorhandene bleiben unangetastet).
|
|
#
|
|
# Aufruf: tools/station/setup_pi.sh <pi-host>
|
|
set -euo pipefail
|
|
HOST=${1:?Aufruf: tools/station/setup_pi.sh <pi-host>}
|
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
# /usr/bin/ssh und /usr/bin/scp statt Varianten im PATH, die sich anders verhalten koennen.
|
|
# Ueberschreibbar (wie NINLIGHT_STATION_ROOT unten und in deploy.sh), damit ein Test gegen
|
|
# Stubs statt einen echten Pi laufen kann.
|
|
SSH_BIN=${NINLIGHT_SSH_BIN:-/usr/bin/ssh}
|
|
SCP_BIN=${NINLIGHT_SCP_BIN:-/usr/bin/scp}
|
|
# Wo die per scp uebertragenen Dateien auf dem Zielsystem zwischenlanden, bevor
|
|
# sie an ihren endgueltigen Platz installiert werden. Ueberschreibbar wie die
|
|
# anderen NINLIGHT_*-Pfade: auf dem Pi bleibt es bei /tmp, ein Test kann hier in
|
|
# eine Sandbox statt in das echte /tmp des Entwicklungsrechners umleiten
|
|
# (Fixrunde 2, N4) - sowohl fuer die lokale scp-Zielangabe unten als auch fuer
|
|
# die REMOTE-Bloecke, die dieselbe Variable (mit demselben Default) referenzieren.
|
|
STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp}
|
|
|
|
echo "== Voraussetzungen auf $HOST pruefen =="
|
|
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
|
|
set -e
|
|
systemctl is-active --quiet NetworkManager || { echo "NetworkManager laeuft nicht - dieser Plan setzt ihn voraus" >&2; exit 1; }
|
|
nmcli -t -f DEVICE,STATE device | grep -q "^wlan0:" || { echo "wlan0 wird nicht von NetworkManager verwaltet" >&2; exit 1; }
|
|
python3 -c "import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)" || { echo "Python 3.11+ noetig" >&2; exit 1; }
|
|
echo " ok"
|
|
REMOTE
|
|
|
|
echo "== Konfiguration =="
|
|
"$SCP_BIN" -q "$HERE/station.conf.example" "$HOST:$STAGE_DIR/ninlight-station.conf.example"
|
|
# CONF ueberschreibbar wie ROOT_DIR/BIN_PATH/UNIT_PATH unten: auf dem Pi bleibt es
|
|
# beim Default, ein Test kann hier gegen eine Sandbox-Datei statt gegen echte
|
|
# Systempfade laufen.
|
|
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
|
|
set -e
|
|
CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf}
|
|
TEMPLATE=${NINLIGHT_STAGE_DIR:-/tmp}/ninlight-station.conf.example
|
|
if [ -f "$CONF" ]; then
|
|
# Eine bestehende Konfiguration bleibt bei ihren Werten - nur Schluessel, die
|
|
# eine neuere Version des CLI zusaetzlich verlangt (z.B. die Migrations- und
|
|
# Anzeige-Schluessel aus Task 4), werden aus der Vorlage ergaenzt. Ohne das
|
|
# wuerde das Ausrollen eines neuen ninlight-station auf einen bereits
|
|
# eingerichteten Pi jeden Unterbefehl - "off" eingeschlossen - mit "Schluessel
|
|
# ... fehlt" blockieren, und ein bereits laufender Zugangspunkt bliebe an, bis
|
|
# jemand die 0600-Datei von Hand ergaenzt (Fixrunde 1, I4).
|
|
ADDED=""
|
|
for KEY in SSID PSK IMAGE_DIR HOME_CONNECTION AP_CONNECTION \
|
|
MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
|
|
# Nicht nur "^KEY=": eine vorhandene Zuweisung mit fuehrendem Leerraum oder
|
|
# einem "export" davor (beides gueltiges ". datei"-Shell) wuerde sonst als
|
|
# fehlend gelten, die Vorlagenzeile wird angehaengt, und da das CLI die
|
|
# Datei per ". $CONF" sourct, gewinnt die LETZTE Zuweisung - ein eigenes
|
|
# Passwort wuerde so still durch den oeffentlichen Vorlagenwert ersetzt
|
|
# (Fixrunde 2, N3: genau das "vorhandene Werte nicht ueberschreiben" aus I4,
|
|
# nur eine Formatierungsvariante weiter).
|
|
if ! sudo grep -Eq "^[[:space:]]*(export[[:space:]]+)?${KEY}=" "$CONF"; then
|
|
LINE=$(grep -E "^${KEY}=" "$TEMPLATE" || true)
|
|
if [ -n "$LINE" ]; then
|
|
printf '%s\n' "$LINE" | sudo tee -a "$CONF" >/dev/null
|
|
ADDED="$ADDED $KEY"
|
|
fi
|
|
fi
|
|
done
|
|
if [ -n "$ADDED" ]; then
|
|
echo " $CONF: fehlende Schluessel ergaenzt:$ADDED (vorhandene Werte unveraendert)"
|
|
else
|
|
echo " $CONF existiert bereits, alle Schluessel vorhanden"
|
|
fi
|
|
else
|
|
sudo install -m 0600 "$TEMPLATE" "$CONF"
|
|
echo " $CONF aus der Vorlage angelegt"
|
|
fi
|
|
rm -f "$TEMPLATE"
|
|
REMOTE
|
|
|
|
echo "== Server, CLI und Unit installieren =="
|
|
"$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:$STAGE_DIR/"
|
|
# Pfade als Variablen mit Default (wie in deploy.sh): so kann ein Test die
|
|
# Installation gegen ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen
|
|
# lassen.
|
|
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
|
|
set -e
|
|
STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp}
|
|
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" "$ROOT_DIR/images"
|
|
sudo install -m 0755 "$STAGE_DIR/server.py" "$ROOT_DIR/server.py"
|
|
sudo install -m 0644 "$STAGE_DIR/products.py" "$ROOT_DIR/products.py"
|
|
sudo install -m 0755 "$STAGE_DIR/ninlight-station" "$BIN_PATH"
|
|
sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH"
|
|
rm -f "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" "$STAGE_DIR/ninlight-station.service"
|
|
sudo systemctl daemon-reload
|
|
echo " installiert"
|
|
REMOTE
|
|
|
|
echo "== NetworkManager-Profil =="
|
|
# Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf:
|
|
# - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht
|
|
# jeder lesen koennen). Der SSH-Benutzer ist aber nicht root, direktes
|
|
# ". /etc/ninlight-station.conf" wuerde also mit "Permission denied"
|
|
# abbrechen. "sudo cat" + eval liest sie dennoch ein, ohne die Rechte der
|
|
# Datei aufzuweichen.
|
|
# - "nmcli ... wifi-sec.psk $PSK" wuerde das Passwort als Kommandozeilen-
|
|
# argument an einen Prozess uebergeben - fuer jeden lokalen Benutzer per
|
|
# "ps"/"/proc/<pid>/cmdline" sichtbar. Deshalb wird das Verbindungsprofil
|
|
# stattdessen direkt als NetworkManager-Keyfile (0600 root:root)
|
|
# geschrieben; das Passwort landet nur in einer Root-Datei, nie in einer
|
|
# Prozessliste.
|
|
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
|
|
set -e
|
|
CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf}
|
|
NM_DIR=${NINLIGHT_NM_DIR:-/etc/NetworkManager/system-connections}
|
|
eval "$(sudo cat "$CONF")"
|
|
CONN_FILE="$NM_DIR/${AP_CONNECTION}.nmconnection"
|
|
if sudo test -f "$CONN_FILE"; then
|
|
UUID=$(sudo sed -n 's/^uuid=//p' "$CONN_FILE" | head -n1)
|
|
ACTION="aktualisiert"
|
|
else
|
|
# /proc/sys/kernel/random/uuid gibt es nur unter Linux (dem Pi) - der Fallback
|
|
# auf uuidgen ist es, was einen Testlauf gegen den lokalen Stub (macOS) ueberhaupt
|
|
# ermoeglicht, aendert aber nichts am Verhalten auf dem Pi selbst.
|
|
UUID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen | tr '[:upper:]' '[:lower:]')
|
|
ACTION="angelegt"
|
|
fi
|
|
TMP=$(mktemp)
|
|
trap 'rm -f "$TMP"' EXIT
|
|
{
|
|
printf '%s\n' '[connection]'
|
|
printf 'id=%s\n' "$AP_CONNECTION"
|
|
printf 'uuid=%s\n' "$UUID"
|
|
printf '%s\n' 'type=wifi'
|
|
printf '%s\n' 'interface-name=wlan0'
|
|
printf '%s\n' 'autoconnect=false'
|
|
printf '\n%s\n' '[wifi]'
|
|
printf '%s\n' 'mode=ap'
|
|
printf 'ssid=%s\n' "$SSID"
|
|
printf '%s\n' 'band=bg'
|
|
printf '%s\n' 'channel=6'
|
|
printf '\n%s\n' '[wifi-security]'
|
|
printf '%s\n' 'key-mgmt=wpa-psk'
|
|
printf '%s\n' 'proto=rsn'
|
|
printf '%s\n' 'pairwise=ccmp;'
|
|
printf '%s\n' 'group=ccmp;'
|
|
printf 'psk=%s\n' "$PSK"
|
|
printf '\n%s\n' '[ipv4]'
|
|
printf '%s\n' 'method=shared'
|
|
printf '%s\n' 'address1=192.168.8.1/24'
|
|
printf '\n%s\n' '[ipv6]'
|
|
printf '%s\n' 'method=disabled'
|
|
} >"$TMP"
|
|
sudo install -o root -g root -m 0600 "$TMP" "$CONN_FILE"
|
|
rm -f "$TMP"
|
|
sudo nmcli connection load "$CONN_FILE" >/dev/null
|
|
echo " Profil $AP_CONNECTION $ACTION"
|
|
REMOTE
|
|
|
|
cat <<'EOF'
|
|
|
|
Fertig. Die Station ist absichtlich noch AUS.
|
|
|
|
Naechste Schritte:
|
|
tools/station/deploy.sh <pi-host> Images ausliefern
|
|
ssh <pi-host> ninlight-station on Station einschalten
|
|
ssh <pi-host> ninlight-station off wieder ausschalten (nicht vergessen)
|
|
EOF
|