#!/bin/bash set -euo pipefail # Home Assistant + Mosquitto Restore Smoke Test # # Scope: # - Restore aus dem neuesten HA-nativen Backup-Artefakt # - Kopie der Mosquitto-Appdata in ein isoliertes Restore-Lab # - Kopie des Fachrepo-Clones zur Lesbarkeits-/Git-Status-Pruefung # - Start isolierter Testcontainer auf localhost-Ports, ohne Traefik/Public Route # - HA HTTP/API-Smoke und MQTT Publish/Subscribe + retained Topic nach Restart SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" . "$SCRIPT_DIR/common.sh" WHATIF=0 KEEP_DATA=0 for arg in "$@"; do case "$arg" in --what-if) WHATIF=1 ;; --keep-data) KEEP_DATA=1 ;; *) echo "Unknown argument: $arg" >&2; exit 1 ;; esac done RESTORE_ROOT="/mnt/user/backups/restore-lab/homeassistant" REPORT_ROOT="/mnt/user/backups/restore-reports" REPORT_FILE="$REPORT_ROOT/homeassistant-$(date +%F).md" COMPOSE_FILE="$SCRIPT_DIR/homeassistant-compose.test.yml" HA_BACKUP_DIR="/mnt/user/appdata/homeassistant/backups" MOSQUITTO_APPDATA="/mnt/user/appdata/mosquitto" MOSQUITTO_REPO_CONF="/mnt/user/services/homelab-infra/smart-home/mosquitto/config/mosquitto.conf" FACHREPO_SOURCE="/mnt/user/services/smart-home-kalli" HA_TOKEN_FILE="/mnt/user/appdata/secrets/ha_token_codex" if [ "$WHATIF" -eq 1 ]; then cat <&2 exit 1 fi rm -rf "$RESTORE_ROOT" mkdir -p \ "$RESTORE_ROOT/ha-backup" \ "$RESTORE_ROOT/homeassistant/config" \ "$RESTORE_ROOT/mosquitto/config" \ "$RESTORE_ROOT/mosquitto/appdata/config" \ "$RESTORE_ROOT/mosquitto/appdata/data" \ "$RESTORE_ROOT/mosquitto/appdata/log" \ "$RESTORE_ROOT/fachrepo" tar -xf "$latest_backup" -C "$RESTORE_ROOT/ha-backup" require_path "$RESTORE_ROOT/ha-backup/backup.json" require_path "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz" tar -xzf "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz" -C "$RESTORE_ROOT/homeassistant/config" --strip-components=1 data cp "$MOSQUITTO_REPO_CONF" "$RESTORE_ROOT/mosquitto/config/mosquitto.conf" cp -a "$MOSQUITTO_APPDATA/config/." "$RESTORE_ROOT/mosquitto/appdata/config/" cp -a "$MOSQUITTO_APPDATA/data/." "$RESTORE_ROOT/mosquitto/appdata/data/" if [ -d "$MOSQUITTO_APPDATA/log" ]; then cp -a "$MOSQUITTO_APPDATA/log/." "$RESTORE_ROOT/mosquitto/appdata/log/" || true fi cp -a "$FACHREPO_SOURCE/." "$RESTORE_ROOT/fachrepo/" ha_config="$RESTORE_ROOT/homeassistant/config" require_path "$ha_config/configuration.yaml" require_path "$ha_config/secrets.yaml" require_path "$ha_config/trusted_proxies.yaml" require_path "$ha_config/.storage/onboarding" require_path "$ha_config/.storage/auth" fachrepo_head="$(git -C "$RESTORE_ROOT/fachrepo" log -1 --oneline)" fachrepo_status="$(git -C "$RESTORE_ROOT/fachrepo" status --short)" if [ -n "$fachrepo_status" ]; then echo "Restored fachrepo clone is not clean:" >&2 echo "$fachrepo_status" >&2 exit 1 fi backup_size="$(stat -c '%s' "$latest_backup")" ha_file_count="$(find "$ha_config" -type f | wc -l | tr -d ' ')" ha_bytes="$(du -sb "$ha_config" | awk '{print $1}')" mosquitto_data_bytes="$(du -sb "$RESTORE_ROOT/mosquitto/appdata" | awk '{print $1}')" RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" up -d >/dev/null mqtt_user="$(sed -n 's/^mqtt_username:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")" mqtt_pass="$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")" if [ -z "$mqtt_user" ] || [ -z "$mqtt_pass" ]; then echo "Missing mqtt_username or mqtt_password in restored HA secrets.yaml" >&2 exit 1 fi mqtt_topic="restoretest/homeassistant/smoke" mqtt_payload="ok-$(date +%s)" mqtt_out="$RESTORE_ROOT/mqtt-sub.out" rm -f "$mqtt_out" docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" \ restoretest-ha-mosquitto sh -lc \ 'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' \ > "$mqtt_out" & sub_pid=$! sleep 1 docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" -e MQTT_PAYLOAD="$mqtt_payload" \ restoretest-ha-mosquitto sh -lc \ 'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD"' wait "$sub_pid" mqtt_result="$(cat "$mqtt_out")" if [ "$mqtt_result" != "$mqtt_payload" ]; then echo "MQTT publish/subscribe smoke failed" >&2 exit 1 fi retained_topic="restoretest/homeassistant/retained" retained_payload="retained-$(date +%s)" docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" -e MQTT_PAYLOAD="$retained_payload" \ restoretest-ha-mosquitto sh -lc \ 'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD" -r' docker restart restoretest-ha-mosquitto >/dev/null sleep 3 retained_result="$(docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" \ restoretest-ha-mosquitto sh -lc \ 'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' | tr -d '\r')" if [ "$retained_result" != "$retained_payload" ]; then echo "MQTT retained smoke failed" >&2 exit 1 fi ha_http_status="" ha_body="$RESTORE_ROOT/ha-http-body.html" for _ in $(seq 1 180); do ha_http_status="$(curl -sS -o "$ha_body" -w '%{http_code}' http://127.0.0.1:18123/ || true)" if [ "$ha_http_status" = "200" ] && grep -qi "Home Assistant" "$ha_body"; then break fi sleep 1 done if [ "$ha_http_status" != "200" ] || ! grep -qi "Home Assistant" "$ha_body"; then echo "HA HTTP smoke failed, status=$ha_http_status" >&2 docker logs --tail 120 restoretest-homeassistant >&2 || true exit 1 fi ha_api_status="$(curl -sS -o "$RESTORE_ROOT/ha-api.json" -w '%{http_code}' \ -H "Authorization: Bearer $(cat "$HA_TOKEN_FILE")" \ -H 'Content-Type: application/json' \ http://127.0.0.1:18123/api/ || true)" if [ "$ha_api_status" != "200" ]; then echo "HA API token smoke failed, status=$ha_api_status" >&2 exit 1 fi RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" exec -T restoretest-homeassistant \ python -m homeassistant --script check_config --config /config >/tmp/restoretest-ha-check-config.out write_report "$REPORT_FILE" < $REPORT_FILE"