237 lines
8.7 KiB
Bash
Executable File
237 lines
8.7 KiB
Bash
Executable File
#!/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 <<EOF
|
|
Home Assistant restore test
|
|
Mode: WhatIf
|
|
RestoreRoot: $RESTORE_ROOT
|
|
HA backup source: newest *.tar under $HA_BACKUP_DIR
|
|
Mosquitto source: $MOSQUITTO_APPDATA
|
|
Fachrepo source: $FACHREPO_SOURCE
|
|
Test endpoints: HA http://127.0.0.1:18123, MQTT 127.0.0.1:11883
|
|
Scope: HA backup extract + isolated HA boot + API token smoke + MQTT auth/retained smoke
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
require_cmd docker
|
|
require_cmd tar
|
|
require_cmd curl
|
|
require_path "$COMPOSE_FILE"
|
|
require_path "$HA_BACKUP_DIR"
|
|
require_path "$MOSQUITTO_APPDATA/config/passwordfile"
|
|
require_path "$MOSQUITTO_APPDATA/config/aclfile"
|
|
require_path "$MOSQUITTO_APPDATA/data"
|
|
require_path "$MOSQUITTO_REPO_CONF"
|
|
require_path "$FACHREPO_SOURCE"
|
|
require_path "$HA_TOKEN_FILE"
|
|
|
|
RESTORE_SUCCESS=0
|
|
cleanup() {
|
|
RESTORE_ROOT="$RESTORE_ROOT" cleanup_compose "$COMPOSE_FILE"
|
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
|
preserve_on_failure "homeassistant" "$RESTORE_ROOT"
|
|
return
|
|
fi
|
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
|
rm -rf "$RESTORE_ROOT"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
latest_backup="$(find "$HA_BACKUP_DIR" -maxdepth 1 -type f -name '*.tar' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print substr($0, index($0,$2))}')"
|
|
if [ -z "$latest_backup" ] || [ ! -f "$latest_backup" ]; then
|
|
echo "No HA native backup tar found under $HA_BACKUP_DIR" >&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" <<EOF
|
|
# Home Assistant Restore Test Report - $(date +%F)
|
|
|
|
- Service: \`homeassistant\` + \`smarthome-mosquitto\`
|
|
- HA backup source: \`$latest_backup\`
|
|
- Restore root: \`$RESTORE_ROOT\`
|
|
- Test containers:
|
|
- \`restoretest-homeassistant\`
|
|
- \`restoretest-ha-mosquitto\`
|
|
- Test endpoints:
|
|
- HA: \`http://127.0.0.1:18123\`
|
|
- MQTT: \`127.0.0.1:11883\`
|
|
- Result: \`SUCCESS\`
|
|
|
|
## Checks
|
|
|
|
- HA-native backup tar readable: \`ok\`
|
|
- HA inner archive restored: \`ok\`
|
|
- HA backup size bytes: \`$backup_size\`
|
|
- Restored HA file count: \`$ha_file_count\`
|
|
- Restored HA bytes: \`$ha_bytes\`
|
|
- Restored Mosquitto appdata bytes: \`$mosquitto_data_bytes\`
|
|
- Fachrepo clone clean: \`ok\`
|
|
- Fachrepo HEAD: \`$fachrepo_head\`
|
|
- HA HTTP status: \`$ha_http_status\`
|
|
- HA API token smoke: \`$ha_api_status\`
|
|
- HA check_config: \`ok\`
|
|
- MQTT publish/subscribe with restored credentials: \`ok\`
|
|
- MQTT retained topic after broker restart: \`ok\`
|
|
|
|
## Notes
|
|
|
|
- Productive \`homeassistant\` and \`smarthome-mosquitto\` containers were not used.
|
|
- Test ran without Traefik and without the productive domain.
|
|
- Test ports were bound to localhost only.
|
|
- Token and MQTT password values were used for smoke tests but not printed.
|
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
|
EOF
|
|
|
|
RESTORE_SUCCESS=1
|
|
echo "Home Assistant restore test ok -> $REPORT_FILE"
|