#!/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 set -euo pipefail HOST=${1:?Aufruf: tools/station/setup_pi.sh } 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} 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:/tmp/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=/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 if ! sudo grep -Eq "^${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:/tmp/" # 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 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 /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 -f /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/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//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 Images ausliefern ssh ninlight-station on Station einschalten ssh ninlight-station off wieder ausschalten (nicht vergessen) EOF