From bbaa0900a4409210d1e17036f7d93efa293249d0 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:51:14 +0200 Subject: [PATCH] Fix Scrutiny 1.67 startup and InfluxDB migration (#2880) * scrutiny: back up InfluxDB before 2.9 upgrade * scrutiny: prepare InfluxDB 2.9 migration safely * scrutiny: restore upstream s6 supervision * scrutiny-fa: restore upstream s6 supervision * scrutiny: validate backup contents before migration * scrutiny: keep backup validation bounded * scrutiny: document startup and migration fix * scrutiny-fa: document startup and migration fix * scrutiny: bump version to v1.67.0-3 * scrutiny-fa: bump version to v1.67.0-3 * scrutiny-fa: include migration helper in build context * scrutiny-fa: copy migration helper explicitly * scrutiny-fa: document build-context fix * scrutiny-fa: bump version to v1.67.0-4 * scrutiny-fa: materialize Home Assistant rootfs overlay * scrutiny-fa: use materialized rootfs overlay * scrutiny-fa: bump version with changelog * scrutiny: verify complete InfluxDB backup state * scrutiny-fa: verify complete InfluxDB backup state * scrutiny: support custom day intervals * scrutiny-fa: support custom day intervals * scrutiny: support absolute TLS paths * scrutiny-fa: support absolute TLS paths * scrutiny: harden collector configuration migration * scrutiny-fa: harden collector configuration migration * scrutiny: preserve legacy migration backups * scrutiny-fa: preserve legacy migration backups * scrutiny: restore initialization-only s6 hook * scrutiny-fa: restore initialization-only s6 hook * scrutiny: trigger reviewed release build * scrutiny: finalize reviewed release * scrutiny: finalize combined release metadata * scrutiny: mark review-complete release * scrutiny: normalize release metadata * scrutiny: settle release metadata * scrutiny: finalize metadata formatting * scrutiny: synchronize reviewed release metadata * scrutiny: run isolated reviewed build * scrutiny-fa: run isolated reviewed build * scrutiny: verify backup contents before migration * scrutiny-fa: verify backup contents before migration * scrutiny: test content-verified atomic backups * scrutiny-fa: test content-verified atomic backups * scrutiny: validate content-level backup integrity * scrutiny: remove blocking FIFO test fixture * scrutiny-fa: remove blocking FIFO test fixture * scrutiny: run final content-integrity build * scrutiny-fa: run final content-integrity build * scrutiny: correct nginx readiness comment * scrutiny-fa: correct nginx readiness comment * scrutiny: skip collector symlinks during migration * scrutiny-fa: skip collector symlinks during migration --- scrutiny/CHANGELOG.md | 12 + scrutiny/Dockerfile | 84 +++++-- scrutiny/config.yaml | 2 +- .../etc/cont-init.d/01-configuration.sh | 14 ++ .../etc/cont-init.d/32-nginx_ingress.sh | 4 +- scrutiny/rootfs/etc/cont-init.d/90-run.sh | 24 +- scrutiny/rootfs/etc/services.d/nginx/run | 2 +- .../local/bin/scrutiny-ha-influxdb-preflight | 228 ++++++++++++++++++ scrutiny_fa/CHANGELOG.md | 15 ++ scrutiny_fa/Dockerfile | 91 +++++-- scrutiny_fa/config.yaml | 2 +- .../etc/cont-init.d/01-configuration.sh | 129 ++++++++++ .../etc/cont-init.d/32-nginx_ingress.sh | 36 +++ .../rootfs_overlay/etc/cont-init.d/90-run.sh | 56 +++++ .../etc/nginx/includes/mime.types | 96 ++++++++ .../etc/nginx/includes/proxy_params.conf | 15 ++ .../etc/nginx/includes/resolver.conf | 1 + .../etc/nginx/includes/server_params.conf | 5 + .../etc/nginx/includes/ssl_params.conf | 9 + .../etc/nginx/includes/upstream.conf | 3 + .../rootfs_overlay/etc/nginx/nginx.conf | 56 +++++ .../etc/nginx/servers/ingress.conf | 28 +++ .../etc/services.d/nginx/finish | 9 + .../rootfs_overlay/etc/services.d/nginx/run | 11 + scrutiny_fa/scrutiny-ha-influxdb-preflight | 228 ++++++++++++++++++ 25 files changed, 1116 insertions(+), 44 deletions(-) create mode 100644 scrutiny/rootfs/usr/local/bin/scrutiny-ha-influxdb-preflight create mode 100755 scrutiny_fa/rootfs_overlay/etc/cont-init.d/01-configuration.sh create mode 100755 scrutiny_fa/rootfs_overlay/etc/cont-init.d/32-nginx_ingress.sh create mode 100755 scrutiny_fa/rootfs_overlay/etc/cont-init.d/90-run.sh create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/mime.types create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/proxy_params.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/resolver.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/server_params.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/ssl_params.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/includes/upstream.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/nginx.conf create mode 100644 scrutiny_fa/rootfs_overlay/etc/nginx/servers/ingress.conf create mode 100755 scrutiny_fa/rootfs_overlay/etc/services.d/nginx/finish create mode 100755 scrutiny_fa/rootfs_overlay/etc/services.d/nginx/run create mode 100644 scrutiny_fa/scrutiny-ha-influxdb-preflight diff --git a/scrutiny/CHANGELOG.md b/scrutiny/CHANGELOG.md index 20b134b6e4..8725d8005c 100644 --- a/scrutiny/CHANGELOG.md +++ b/scrutiny/CHANGELOG.md @@ -1,4 +1,16 @@ +## v1.67.0-8 (2026-07-18) +- Keep content-level backup validation and atomic replacement tests while removing a blocking synthetic FIFO test fixture from cross-architecture builds. + +## v1.67.0-7 (2026-07-18) +- Verify every backup file checksum and symbolic-link target, record the content-validation method in the migration marker, and preserve the previous archive until a validated replacement is atomically ready. + +## v1.67.0-6 (2026-07-18) +- Address all review findings: restore the initialization-only s6 stage-two hook, verify complete InfluxDB backup manifests and checksums, preserve legacy migration backups, and harden custom scheduling, ingress path handling, and collector configuration migration. + +## v1.67.0-3 (2026-07-18) +- Restore upstream s6 supervision for all Scrutiny services and automatically create a verified offline InfluxDB backup in `/share/scrutiny` before the 2.9 migration. + ## v1.67.0-2 (2026-07-18) - Fix startup with the Home Assistant entrypoint by removing the upstream collector's unsupported s6 service wait. The collector still waits for the Scrutiny API health endpoint before running. diff --git a/scrutiny/Dockerfile b/scrutiny/Dockerfile index 1f53a04583..14a44fb428 100644 --- a/scrutiny/Dockerfile +++ b/scrutiny/Dockerfile @@ -25,7 +25,9 @@ FROM ${BUILD_FROM} # Set S6 wait time ENV S6_CMD_WAIT_FOR_SERVICES=1 \ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ - S6_SERVICES_GRACETIME=0 + S6_SERVICES_GRACETIME=0 \ + S6_STAGE2_HOOK="/ha_entrypoint.sh" \ + SCRUTINY_HA_ADDON_SLUG="scrutiny" ################## # 3 Install apps # @@ -33,13 +35,9 @@ ENV S6_CMD_WAIT_FOR_SERVICES=1 \ # Add rootfs COPY rootfs/ / -RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; - -# The Home Assistant entrypoint starts services directly instead of running s6. -# Scrutiny's collector already waits for the API health endpoint, so remove its s6-only wait. -RUN if grep -q 's6-svwait -u /run/service/scrutiny' /etc/services.d/collector-once/run; then \ - sed -i '\|s6-svwait -u /run/service/scrutiny|d' /etc/services.d/collector-once/run; \ - fi +RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \ + chmod 0755 /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + if [ -d /command ]; then ln -sf /command/* /usr/bin/; fi # Uses /bin for compatibility purposes # hadolint ignore=DL4005 @@ -57,6 +55,7 @@ RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_auto ENV PACKAGES="jq \ curl \ cifs-utils \ + gzip \ nginx" # Automatic apps & bashio @@ -67,17 +66,74 @@ RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps. # 4 Entrypoint # ################ -# Add entrypoint -ENV S6_STAGE2_HOOK=/ha_entrypoint.sh +# Keep the repository initialization hook, but return after it has prepared the +# cont-init scripts. Upstream s6 remains responsible for supervising services. COPY ha_entrypoint.sh /ha_entrypoint.sh -RUN chmod 777 /ha_entrypoint.sh +RUN chmod 0755 /ha_entrypoint.sh && \ + awk '!inserted && $0 == "if $PID1; then" { \ + print "if ! $PID1; then"; \ + print " echo \"Initialization hook complete\""; \ + print " exit 0"; \ + print "fi"; \ + inserted=1 \ + } { print }' /ha_entrypoint.sh > /ha_entrypoint.sh.tmp && \ + mv /ha_entrypoint.sh.tmp /ha_entrypoint.sh && \ + chmod 0755 /ha_entrypoint.sh # Install bashio COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh RUN chmod 0755 /usr/local/lib/bashio-standalone.sh -ENTRYPOINT [ "/usr/bin/env" ] -CMD [ "/ha_entrypoint.sh" ] +# Build-time migration tests cover path and content equality, symbolic links, +# marker checksums, idempotency, and fail-closed marker states. +RUN test -x /init && \ + test -x /ha_entrypoint.sh && \ + bash -n /ha_entrypoint.sh && \ + grep -q 'Initialization hook complete' /ha_entrypoint.sh && \ + bash -n /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + test_root="$(mktemp -d)" && \ + mkdir -p "$test_root/data/influxdb/engine/nested" "$test_root/share" "$test_root/stale/influxdb/engine/nested" && \ + printf 'bolt-test\n' > "$test_root/data/influxdb/influxd.bolt" && \ + printf 'shard-one\n' > "$test_root/data/influxdb/engine/shard-one" && \ + printf 'shard-two\n' > "$test_root/data/influxdb/engine/nested/shard-two" && \ + ln -s nested/shard-two "$test_root/data/influxdb/engine/current-link" && \ + printf 'stale-bolt\n' > "$test_root/stale/influxdb/influxd.bolt" && \ + printf 'stale-one\n' > "$test_root/stale/influxdb/engine/shard-one" && \ + printf 'shard-two\n' > "$test_root/stale/influxdb/engine/nested/shard-two" && \ + ln -s shard-one "$test_root/stale/influxdb/engine/current-link" && \ + tar -C "$test_root/stale" -czf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb && \ + stale_sha="$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + backup_sha="$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + test "$stale_sha" != "$backup_sha" && \ + test "$(tar -xOzf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb/engine/shard-one)" = "shard-one" && \ + tar -tvzf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb/engine/current-link | grep -q ' -> nested/shard-two$' && \ + grep -q '^state=legacy-backup$' "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q '^validation=content-sha256-v1$' "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q "^backup_sha256=${backup_sha}$" "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + test "$backup_sha" = "$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + printf 'corrupt\n' > "$test_root/share/influxdb-pre-2.9.tar.gz" && \ + if SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight; then exit 1; fi && \ + empty_root="$(mktemp -d)" && \ + mkdir -p "$empty_root/data/influxdb" "$empty_root/share" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$empty_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$empty_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + grep -q '^state=no-legacy-data$' "$empty_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q '^validation=no-data-v1$' "$empty_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + test ! -e "$empty_root/share/influxdb-pre-2.9.tar.gz" && \ + rm -rf "$test_root" "$empty_root" + +# Scrutiny's image already includes s6-overlay and defines all services under +# /etc/services.d. Keep /init as PID 1 so service readiness and s6-svc calls work. +ENTRYPOINT [ "/init" ] ############ # 5 Labels # @@ -137,4 +193,4 @@ HEALTHCHECK \ --retries=5 \ --start-period=30s \ --timeout=25s \ - CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1 \ No newline at end of file + CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1 diff --git a/scrutiny/config.yaml b/scrutiny/config.yaml index 809fca7126..24be896ff4 100644 --- a/scrutiny/config.yaml +++ b/scrutiny/config.yaml @@ -113,4 +113,4 @@ schema: slug: scrutiny udev: true url: https://github.com/Starosdev/scrutiny -version: "v1.67.0-2" \ No newline at end of file +version: "v1.67.0-8" diff --git a/scrutiny/rootfs/etc/cont-init.d/01-configuration.sh b/scrutiny/rootfs/etc/cont-init.d/01-configuration.sh index 2e7e7380a0..af7402839b 100755 --- a/scrutiny/rootfs/etc/cont-init.d/01-configuration.sh +++ b/scrutiny/rootfs/etc/cont-init.d/01-configuration.sh @@ -15,6 +15,11 @@ if [ -d /opt/scrutiny/influxdb ]; then rm -r /opt/scrutiny/influxdb; fi ln -s "$DATABASELOCATION"/config /opt/scrutiny ln -s "$DATABASELOCATION"/influxdb /opt/scrutiny +# Upstream 1.67 upgrades InfluxDB from 2.2 to 2.9 and requires a confirmed +# offline backup before starting against existing data. Services have not +# started yet, so this is the safe point to create and verify that backup. +/usr/local/bin/scrutiny-ha-influxdb-preflight + ############################### # Migrating previous database # ############################### @@ -84,6 +89,15 @@ case "$FREQUENCY" in fi ;; + *d) # Matches intervals in days, like "10d" + days="${interval%d}" + if [[ "$days" -gt 0 && "$days" -le 31 ]]; then + cron_schedule="0 0 */$days * *" + else + bashio::log.error "Invalid day interval: $interval" + fi + ;; + *w) # Matches intervals in weeks, like "1w" weeks="${interval%w}" if [[ "$weeks" -gt 0 && "$weeks" -le 4 ]]; then diff --git a/scrutiny/rootfs/etc/cont-init.d/32-nginx_ingress.sh b/scrutiny/rootfs/etc/cont-init.d/32-nginx_ingress.sh index 3b8d0806bd..99d851b34c 100755 --- a/scrutiny/rootfs/etc/cont-init.d/32-nginx_ingress.sh +++ b/scrutiny/rootfs/etc/cont-init.d/32-nginx_ingress.sh @@ -20,8 +20,8 @@ if bashio::var.has_value "${port}"; then keyfile=$(bashio::config 'keyfile') mv /etc/nginx/servers/direct-ssl.disabled /etc/nginx/servers/direct.conf - sed -i "s/%%certfile%%/${certfile}/g" /etc/nginx/servers/direct.conf - sed -i "s/%%keyfile%%/${keyfile}/g" /etc/nginx/servers/direct.conf + sed -i "s|%%certfile%%|${certfile}|g" /etc/nginx/servers/direct.conf + sed -i "s|%%keyfile%%|${keyfile}|g" /etc/nginx/servers/direct.conf else mv /etc/nginx/servers/direct.disabled /etc/nginx/servers/direct.conf diff --git a/scrutiny/rootfs/etc/cont-init.d/90-run.sh b/scrutiny/rootfs/etc/cont-init.d/90-run.sh index 9269a8eacb..581e4ab54a 100755 --- a/scrutiny/rootfs/etc/cont-init.d/90-run.sh +++ b/scrutiny/rootfs/etc/cont-init.d/90-run.sh @@ -9,13 +9,19 @@ set -e if bashio::config.true "expose_collector"; then bashio::log.info "collector.yaml exposed in /share/scrutiny" mkdir -p /share/scrutiny - if [ -f /data/config/collector.yaml ]; then - cp -rnf /data/config/collector.yaml /share/scrutiny || true - rm -R /data/config/collector.yaml + if [ -f /data/config/collector.yaml ] && [ ! -L /data/config/collector.yaml ]; then + if cp -n /data/config/collector.yaml /share/scrutiny; then + rm -f /data/config/collector.yaml + else + bashio::log.warning "Could not copy /data/config/collector.yaml; keeping the source file" + fi fi - if [ -f /opt/scrutiny/config/collector.yaml ]; then - cp -rnf /opt/scrutiny/config/collector.yaml /share/scrutiny || true - rm /opt/scrutiny/config/collector.yaml + if [ -f /opt/scrutiny/config/collector.yaml ] && [ ! -L /opt/scrutiny/config/collector.yaml ]; then + if cp -n /opt/scrutiny/config/collector.yaml /share/scrutiny; then + rm -f /opt/scrutiny/config/collector.yaml + else + bashio::log.warning "Could not copy /opt/scrutiny/config/collector.yaml; keeping the source file" + fi fi touch /share/scrutiny/collector.yaml ln -sf /share/scrutiny/collector.yaml /data/config || true @@ -31,9 +37,9 @@ fi if [[ "$(bashio::config "Mode")" == Collector ]]; then # Clean services bashio::log.warning "Collector only mode. WebUI and Influxdb will be disabled" - rm -r /etc/services.d/influxdb - rm -r /etc/services.d/scrutiny - rm -r /etc/services.d/nginx + rm -rf /etc/services.d/influxdb + rm -rf /etc/services.d/scrutiny + rm -rf /etc/services.d/nginx sed -i "/wait/d" /etc/services.d/collector-once/run sed -i "/scrutiny api not ready/d" /etc/services.d/collector-once/run diff --git a/scrutiny/rootfs/etc/services.d/nginx/run b/scrutiny/rootfs/etc/services.d/nginx/run index 623775fc40..43a7f85308 100755 --- a/scrutiny/rootfs/etc/services.d/nginx/run +++ b/scrutiny/rootfs/etc/services.d/nginx/run @@ -3,7 +3,7 @@ set -e # ============================================================================== -# Wait for transmission to become available +# Wait for Scrutiny to become available bashio::net.wait_for 8080 localhost 900 bashio::log.info "Starting NGinx..." diff --git a/scrutiny/rootfs/usr/local/bin/scrutiny-ha-influxdb-preflight b/scrutiny/rootfs/usr/local/bin/scrutiny-ha-influxdb-preflight new file mode 100644 index 0000000000..7d99aac7d8 --- /dev/null +++ b/scrutiny/rootfs/usr/local/bin/scrutiny-ha-influxdb-preflight @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +set -euo pipefail + +addon_slug="${SCRUTINY_HA_ADDON_SLUG:-scrutiny}" +data_dir="${SCRUTINY_INFLUXDB_DATA_DIR:-/data/influxdb}" +backup_dir="${SCRUTINY_INFLUXDB_BACKUP_DIR:-/share/${addon_slug}}" +marker_file="${data_dir}/.scrutiny-influxdb-2.9-preflight-complete" +marker_tmp="${marker_file}.tmp" +backup_file="${backup_dir}/influxdb-pre-2.9.tar.gz" +temporary_backup="${backup_file}.tmp" +archive_root="$(basename "$data_dir")" +validation_method="content-sha256-v1" +source_manifest="" +archive_manifest="" + +log() { + local level="$1" + local message="$2" + printf 'time="%s" level=%s msg="%s" type=ha-influxdb-upgrade-preflight\n' \ + "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$level" "$message" +} + +cleanup() { + rm -f "$temporary_backup" "$marker_tmp" + if [[ -n "$source_manifest" ]]; then rm -f "$source_manifest"; fi + if [[ -n "$archive_manifest" ]]; then rm -f "$archive_manifest"; fi +} +trap cleanup EXIT + +has_existing_data() { + if [[ -e "${data_dir}/influxd.bolt" || -e "${data_dir}/influxd.sqlite" ]]; then + return 0 + fi + + if [[ -d "${data_dir}/engine" ]] && find "${data_dir}/engine" -mindepth 1 -print -quit | grep -q .; then + return 0 + fi + + return 1 +} + +validate_archive() { + local archive="$1" + [[ -s "$archive" ]] || return 1 + gzip -t "$archive" >/dev/null 2>&1 || return 1 + tar -tzf "$archive" >/dev/null 2>&1 +} + +reset_manifests() { + rm -f "$source_manifest" "$archive_manifest" + source_manifest="" + archive_manifest="" +} + +validate_backup_against_source() { + local archive="$1" + local relative_path + local source_path + local archive_member + local listing + local source_checksum + local archive_checksum + local source_target + local archive_target + + validate_archive "$archive" || return 1 + source_manifest="$(mktemp)" + archive_manifest="$(mktemp)" + + ( + cd "$data_dir" + find . -mindepth 1 -print | sed -e 's#^\./##' -e 's#/$##' | LC_ALL=C sort + ) > "$source_manifest" + + tar -tzf "$archive" | \ + sed -e "s#^${archive_root}/##" -e '/^$/d' -e 's#/$##' | \ + LC_ALL=C sort > "$archive_manifest" + + if ! cmp -s "$source_manifest" "$archive_manifest"; then + reset_manifests + return 1 + fi + + while IFS= read -r relative_path; do + source_path="${data_dir}/${relative_path}" + archive_member="${archive_root}/${relative_path}" + listing="$(tar -tvzf "$archive" -- "$archive_member" 2>/dev/null)" || { + reset_manifests + return 1 + } + + if [[ -L "$source_path" ]]; then + [[ "${listing:0:1}" == "l" ]] || { + reset_manifests + return 1 + } + source_target="$(readlink "$source_path")" + archive_target="${listing##* -> }" + [[ "$source_target" == "$archive_target" ]] || { + reset_manifests + return 1 + } + elif [[ -f "$source_path" ]]; then + [[ "${listing:0:1}" == "-" || "${listing:0:1}" == "h" ]] || { + reset_manifests + return 1 + } + source_checksum="$(sha256sum "$source_path" | awk '{print $1}')" + archive_checksum="$(tar -xOzf "$archive" -- "$archive_member" | sha256sum | awk '{print $1}')" + [[ "$source_checksum" == "$archive_checksum" ]] || { + reset_manifests + return 1 + } + elif [[ -d "$source_path" ]]; then + [[ "${listing:0:1}" == "d" ]] || { + reset_manifests + return 1 + } + else + reset_manifests + return 1 + fi + done < "$source_manifest" + + reset_manifests +} + +marker_value() { + local key="$1" + awk -F= -v key="$key" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' "$marker_file" +} + +write_marker() { + local state="$1" + local checksum="${2:-}" + local validation="${3:-}" + + { + printf 'state=%s\n' "$state" + if [[ -n "$validation" ]]; then printf 'validation=%s\n' "$validation"; fi + if [[ -n "$checksum" ]]; then printf 'backup_sha256=%s\n' "$checksum"; fi + } > "$marker_tmp" + mv "$marker_tmp" "$marker_file" +} + +validate_existing_marker() { + local state + local validation + local expected_checksum + local actual_checksum + + state="$(marker_value state)" + validation="$(marker_value validation)" + + case "$state" in + no-legacy-data) + if [[ -n "$validation" && "$validation" != "no-data-v1" ]]; then + log error "no-data migration marker uses an unknown validation method" + return 1 + fi + if [[ -z "$validation" ]]; then + write_marker no-legacy-data "" no-data-v1 + log warning "upgraded no-data migration marker with explicit validation state" + fi + return 0 + ;; + legacy-backup) + if [[ "$validation" != "$validation_method" ]]; then + log error "legacy backup marker lacks content-level validation proof" + return 1 + fi + expected_checksum="$(marker_value backup_sha256)" + if [[ -z "$expected_checksum" ]] || ! validate_archive "$backup_file"; then + log error "legacy backup marker exists but its archive cannot be verified" + return 1 + fi + actual_checksum="$(sha256sum "$backup_file" | awk '{print $1}')" + if [[ "$actual_checksum" != "$expected_checksum" ]]; then + log error "legacy backup checksum does not match the migration marker" + return 1 + fi + return 0 + ;; + *) + log error "legacy migration marker lacks content-level validation proof" + return 1 + ;; + esac +} + +mkdir -p "$data_dir" + +if [[ -f "$marker_file" ]]; then + if validate_existing_marker; then exit 0; fi + exit 1 +fi + +if ! has_existing_data; then + write_marker no-legacy-data "" no-data-v1 + log info "no existing InfluxDB data detected; recorded no-data migration state" + exit 0 +fi + +mkdir -p "$backup_dir" + +if [[ -e "$backup_file" ]] && validate_backup_against_source "$backup_file"; then + log info "using existing content-verified InfluxDB backup at ${backup_file}" +else + if [[ -e "$backup_file" ]]; then + log warning "existing InfluxDB backup is stale, incomplete, or invalid and will be atomically replaced" + fi + + log warning "creating offline InfluxDB backup at ${backup_file}" + tar -C "$(dirname "$data_dir")" -czf "$temporary_backup" "$archive_root" + + if ! validate_backup_against_source "$temporary_backup"; then + log error "created InfluxDB backup failed content-level validation" + exit 1 + fi + + mv -f "$temporary_backup" "$backup_file" + log info "InfluxDB backup created and content-verified at ${backup_file}" +fi + +backup_checksum="$(sha256sum "$backup_file" | awk '{print $1}')" +write_marker legacy-backup "$backup_checksum" "$validation_method" +log info "InfluxDB 2.9 migration marker created with content-validation method and backup checksum" diff --git a/scrutiny_fa/CHANGELOG.md b/scrutiny_fa/CHANGELOG.md index 1fcb0a29e0..72c5514856 100644 --- a/scrutiny_fa/CHANGELOG.md +++ b/scrutiny_fa/CHANGELOG.md @@ -1,4 +1,19 @@ +## v1.67.0-8 (2026-07-18) +- Verify every backup file checksum and symbolic-link target, record the content-validation method in the migration marker, atomically preserve the previous archive until replacement validation succeeds, and remove the blocking synthetic FIFO test fixture. + +## v1.67.0-7 (2026-07-18) +- Address all review findings: restore the initialization-only s6 stage-two hook, verify complete InfluxDB backup manifests and checksums, preserve legacy migration backups, default to `/share/scrutiny_fa`, and harden custom scheduling, ingress path handling, and collector configuration migration. + +## v1.67.0-5 (2026-07-18) +- Replace the legacy cross-directory `rootfs` symlink with a materialized Full Access overlay so the complete add-on builds reproducibly on aarch64 and amd64. + +## v1.67.0-4 (2026-07-18) +- Include the InfluxDB migration helper directly in the Full Access build context so both supported architectures receive and test it reliably. + +## v1.67.0-3 (2026-07-18) +- Restore upstream s6 supervision for all Scrutiny services and automatically create a verified offline InfluxDB backup in `/share/scrutiny_fa` before the 2.9 migration. + ## v1.67.0-2 (2026-07-18) - Fix startup with the Home Assistant entrypoint by removing the upstream collector's unsupported s6 service wait. The collector still waits for the Scrutiny API health endpoint before running. diff --git a/scrutiny_fa/Dockerfile b/scrutiny_fa/Dockerfile index e6cd15a0d2..6047da813a 100644 --- a/scrutiny_fa/Dockerfile +++ b/scrutiny_fa/Dockerfile @@ -25,21 +25,21 @@ FROM ${BUILD_FROM} # Set S6 wait time ENV S6_CMD_WAIT_FOR_SERVICES=1 \ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ - S6_SERVICES_GRACETIME=0 + S6_SERVICES_GRACETIME=0 \ + S6_STAGE2_HOOK="/ha_entrypoint.sh" \ + SCRUTINY_HA_ADDON_SLUG="scrutiny_fa" ################## # 3 Install apps # ################## -# Add rootfs -COPY rootfs/ / -RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; - -# The Home Assistant entrypoint starts services directly instead of running s6. -# Scrutiny's collector already waits for the API health endpoint, so remove its s6-only wait. -RUN if grep -q 's6-svwait -u /run/service/scrutiny' /etc/services.d/collector-once/run; then \ - sed -i '\|s6-svwait -u /run/service/scrutiny|d' /etc/services.d/collector-once/run; \ - fi +# Add the materialized Home Assistant overlay. Keeping it inside this add-on's +# build context avoids Docker/BuildKit following the legacy cross-directory symlink. +COPY rootfs_overlay/ / +COPY scrutiny-ha-influxdb-preflight /usr/local/bin/scrutiny-ha-influxdb-preflight +RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \ + chmod 0755 /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + if [ -d /command ]; then ln -sf /command/* /usr/bin/; fi # Uses /bin for compatibility purposes # hadolint ignore=DL4005 @@ -57,6 +57,7 @@ RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_auto ENV PACKAGES="jq \ curl \ cifs-utils \ + gzip \ nginx" # Automatic apps & bashio @@ -67,19 +68,77 @@ RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps. # 4 Entrypoint # ################ -# Add entrypoint -ENV S6_STAGE2_HOOK=/ha_entrypoint.sh +# Keep the repository initialization hook, but return after it has prepared the +# cont-init scripts. Upstream s6 remains responsible for supervising services. COPY ha_entrypoint.sh /ha_entrypoint.sh -RUN chmod 777 /ha_entrypoint.sh +RUN chmod 0755 /ha_entrypoint.sh && \ + awk '!inserted && $0 == "if $PID1; then" { \ + print "if ! $PID1; then"; \ + print " echo \"Initialization hook complete\""; \ + print " exit 0"; \ + print "fi"; \ + inserted=1 \ + } { print }' /ha_entrypoint.sh > /ha_entrypoint.sh.tmp && \ + mv /ha_entrypoint.sh.tmp /ha_entrypoint.sh && \ + chmod 0755 /ha_entrypoint.sh # Install bashio COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh RUN chmod 0755 /usr/local/lib/bashio-standalone.sh +# Build-time migration tests cover path and content equality, symbolic links, +# marker checksums, idempotency, and fail-closed marker states. +RUN test -x /init && \ + test -x /ha_entrypoint.sh && \ + bash -n /ha_entrypoint.sh && \ + grep -q 'Initialization hook complete' /ha_entrypoint.sh && \ + bash -n /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + grep -q 'SCRUTINY_HA_ADDON_SLUG:-scrutiny_fa' /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + test_root="$(mktemp -d)" && \ + mkdir -p "$test_root/data/influxdb/engine/nested" "$test_root/share" "$test_root/stale/influxdb/engine/nested" && \ + printf 'bolt-test\n' > "$test_root/data/influxdb/influxd.bolt" && \ + printf 'shard-one\n' > "$test_root/data/influxdb/engine/shard-one" && \ + printf 'shard-two\n' > "$test_root/data/influxdb/engine/nested/shard-two" && \ + ln -s nested/shard-two "$test_root/data/influxdb/engine/current-link" && \ + printf 'stale-bolt\n' > "$test_root/stale/influxdb/influxd.bolt" && \ + printf 'stale-one\n' > "$test_root/stale/influxdb/engine/shard-one" && \ + printf 'shard-two\n' > "$test_root/stale/influxdb/engine/nested/shard-two" && \ + ln -s shard-one "$test_root/stale/influxdb/engine/current-link" && \ + tar -C "$test_root/stale" -czf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb && \ + stale_sha="$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + backup_sha="$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + test "$stale_sha" != "$backup_sha" && \ + test "$(tar -xOzf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb/engine/shard-one)" = "shard-one" && \ + tar -tvzf "$test_root/share/influxdb-pre-2.9.tar.gz" influxdb/engine/current-link | grep -q ' -> nested/shard-two$' && \ + grep -q '^state=legacy-backup$' "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q '^validation=content-sha256-v1$' "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q "^backup_sha256=${backup_sha}$" "$test_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + test "$backup_sha" = "$(sha256sum "$test_root/share/influxdb-pre-2.9.tar.gz" | awk '{print $1}')" && \ + printf 'corrupt\n' > "$test_root/share/influxdb-pre-2.9.tar.gz" && \ + if SCRUTINY_INFLUXDB_DATA_DIR="$test_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$test_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight; then exit 1; fi && \ + empty_root="$(mktemp -d)" && \ + mkdir -p "$empty_root/data/influxdb" "$empty_root/share" && \ + SCRUTINY_INFLUXDB_DATA_DIR="$empty_root/data/influxdb" \ + SCRUTINY_INFLUXDB_BACKUP_DIR="$empty_root/share" \ + /usr/local/bin/scrutiny-ha-influxdb-preflight && \ + grep -q '^state=no-legacy-data$' "$empty_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + grep -q '^validation=no-data-v1$' "$empty_root/data/influxdb/.scrutiny-influxdb-2.9-preflight-complete" && \ + test ! -e "$empty_root/share/influxdb-pre-2.9.tar.gz" && \ + rm -rf "$test_root" "$empty_root" + RUN sed -i "1a if ! bashio::require.unprotected; then bashio::addon.stop; fi" /etc/cont-init.d/90-run.sh -ENTRYPOINT [ "/usr/bin/env" ] -CMD [ "/ha_entrypoint.sh" ] +# Scrutiny's image already includes s6-overlay and defines all services under +# /etc/services.d. Keep /init as PID 1 so service readiness and s6-svc calls work. +ENTRYPOINT [ "/init" ] ############ # 5 Labels # @@ -139,4 +198,4 @@ HEALTHCHECK \ --retries=5 \ --start-period=30s \ --timeout=25s \ - CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1 \ No newline at end of file + CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1 diff --git a/scrutiny_fa/config.yaml b/scrutiny_fa/config.yaml index 4ad8794017..fe18b44ccf 100644 --- a/scrutiny_fa/config.yaml +++ b/scrutiny_fa/config.yaml @@ -45,4 +45,4 @@ schema: slug: scrutiny_fa udev: true url: https://github.com/Starosdev/scrutiny -version: "v1.67.0-2" \ No newline at end of file +version: "v1.67.0-8" diff --git a/scrutiny_fa/rootfs_overlay/etc/cont-init.d/01-configuration.sh b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/01-configuration.sh new file mode 100755 index 0000000000..af7402839b --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/01-configuration.sh @@ -0,0 +1,129 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e + +################# +# Create folder # +################# + +DATABASELOCATION="/data" +echo "Updating folders structure" +mkdir -p "$DATABASELOCATION"/config +mkdir -p "$DATABASELOCATION"/influxdb +if [ -d /opt/scrutiny/config ]; then rm -r /opt/scrutiny/config; fi +if [ -d /opt/scrutiny/influxdb ]; then rm -r /opt/scrutiny/influxdb; fi +ln -s "$DATABASELOCATION"/config /opt/scrutiny +ln -s "$DATABASELOCATION"/influxdb /opt/scrutiny + +# Upstream 1.67 upgrades InfluxDB from 2.2 to 2.9 and requires a confirmed +# offline backup before starting against existing data. Services have not +# started yet, so this is the safe point to create and verify that backup. +/usr/local/bin/scrutiny-ha-influxdb-preflight + +############################### +# Migrating previous database # +############################### + +if [ -f /data/scrutiny.db ]; then + bashio::log.warning "Previous database detected, migration will start. Backup stored in /share/scrutiny.db.bak" + cp /data/scrutiny.db /share/scrutiny.db.bak + mv /data/scrutiny.db "$DATABASELOCATION"/config/ +fi + +###### +# TZ # +###### + +# Align timezone with options +if bashio::config.has_value "TZ"; then + TZ="$(bashio::config 'TZ')" + bashio::log.info "Timezone : $TZ" + sed -i "1a export TZ=$TZ" /etc/cont-init.d/01-timezone +fi + +################ +# CRON OPTIONS # +################ + +# Align update with options +FREQUENCY="$(bashio::config 'Updates')" +bashio::log.info "$FREQUENCY updates as defined in the 'Updates' option" + +case "$FREQUENCY" in + "Quarterly") + sed -i "/customize the cron schedule/a export COLLECTOR_CRON_SCHEDULE=\"*/15 * * * *\"" /etc/cont-init.d/50-cron-config + ;; + + "Hourly") + sed -i "/customize the cron schedule/a export COLLECTOR_CRON_SCHEDULE=\"0 * * * *\"" /etc/cont-init.d/50-cron-config + ;; + + "Daily") + sed -i "/customize the cron schedule/a export COLLECTOR_CRON_SCHEDULE=\"0 0 * * *\"" /etc/cont-init.d/50-cron-config + ;; + + "Weekly") + sed -i "/customize the cron schedule/a export COLLECTOR_CRON_SCHEDULE=\"0 0 * * 0\"" /etc/cont-init.d/50-cron-config + ;; + + "Custom") + interval="$(bashio::config 'Updates_custom_time')" + bashio::log.info "... frequency is defined manually as $interval" + + case "$interval" in + *m) # Matches intervals in minutes, like "5m" or "30m" + minutes="${interval%m}" + if [[ "$minutes" -gt 0 && "$minutes" -le 59 ]]; then + cron_schedule="*/$minutes * * * *" + else + bashio::log.error "Invalid minute interval: $interval" + fi + ;; + + *h) # Matches intervals in hours, like "2h" + hours="${interval%h}" + if [[ "$hours" -gt 0 && "$hours" -le 23 ]]; then + cron_schedule="0 */$hours * * *" + else + bashio::log.error "Invalid hour interval: $interval" + fi + ;; + + *d) # Matches intervals in days, like "10d" + days="${interval%d}" + if [[ "$days" -gt 0 && "$days" -le 31 ]]; then + cron_schedule="0 0 */$days * *" + else + bashio::log.error "Invalid day interval: $interval" + fi + ;; + + *w) # Matches intervals in weeks, like "1w" + weeks="${interval%w}" + if [[ "$weeks" -gt 0 && "$weeks" -le 4 ]]; then + cron_schedule="0 0 * * 0" # Weekly on Sunday (adjust if needed for multi-week) + else + bashio::log.error "Invalid week interval: $interval" + fi + ;; + + *mo) # Matches intervals in months, like "1mo" + months="${interval%mo}" + if [[ "$months" -gt 0 && "$months" -le 12 ]]; then + cron_schedule="0 0 1 */$months *" # Monthly on the 1st + else + bashio::log.error "Invalid month interval: $interval" + fi + ;; + + *) + bashio::log.error "Empty or unsupported custom interval. It should be in the format of 5m (every 5 minutes), 10d (every 10 days), 3w (every 3 weeks), 3mo (every 3 months)" + ;; + esac + + if [[ -n "$cron_schedule" ]]; then + sed -i "/customize the cron schedule/a export COLLECTOR_CRON_SCHEDULE=\"$cron_schedule\"" /etc/cont-init.d/50-cron-config + bashio::log.info "Custom cron schedule set to: $cron_schedule" + fi + ;; +esac diff --git a/scrutiny_fa/rootfs_overlay/etc/cont-init.d/32-nginx_ingress.sh b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/32-nginx_ingress.sh new file mode 100755 index 0000000000..99d851b34c --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/32-nginx_ingress.sh @@ -0,0 +1,36 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e + +################# +# NGINX SETTING # +################# +declare port +declare certfile +declare ingress_interface +declare ingress_port +declare keyfile + +port=$(bashio::addon.port 80) +if bashio::var.has_value "${port}"; then + bashio::config.require.ssl + + if bashio::config.true 'ssl'; then + certfile=$(bashio::config 'certfile') + keyfile=$(bashio::config 'keyfile') + + mv /etc/nginx/servers/direct-ssl.disabled /etc/nginx/servers/direct.conf + sed -i "s|%%certfile%%|${certfile}|g" /etc/nginx/servers/direct.conf + sed -i "s|%%keyfile%%|${keyfile}|g" /etc/nginx/servers/direct.conf + + else + mv /etc/nginx/servers/direct.disabled /etc/nginx/servers/direct.conf + fi +fi + +ingress_port=$(bashio::addon.ingress_port) +ingress_interface=$(bashio::addon.ip_address) +ingress_entry=$(bashio::addon.ingress_entry) +sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf +sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf +sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf diff --git a/scrutiny_fa/rootfs_overlay/etc/cont-init.d/90-run.sh b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/90-run.sh new file mode 100755 index 0000000000..581e4ab54a --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/cont-init.d/90-run.sh @@ -0,0 +1,56 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e + +######################### +# EXPOSE COLLECTOR.YAML # +######################### + +if bashio::config.true "expose_collector"; then + bashio::log.info "collector.yaml exposed in /share/scrutiny" + mkdir -p /share/scrutiny + if [ -f /data/config/collector.yaml ] && [ ! -L /data/config/collector.yaml ]; then + if cp -n /data/config/collector.yaml /share/scrutiny; then + rm -f /data/config/collector.yaml + else + bashio::log.warning "Could not copy /data/config/collector.yaml; keeping the source file" + fi + fi + if [ -f /opt/scrutiny/config/collector.yaml ] && [ ! -L /opt/scrutiny/config/collector.yaml ]; then + if cp -n /opt/scrutiny/config/collector.yaml /share/scrutiny; then + rm -f /opt/scrutiny/config/collector.yaml + else + bashio::log.warning "Could not copy /opt/scrutiny/config/collector.yaml; keeping the source file" + fi + fi + touch /share/scrutiny/collector.yaml + ln -sf /share/scrutiny/collector.yaml /data/config || true + mkdir -p /opt/scrutiny/config + ln -sf /share/scrutiny/collector.yaml /opt/scrutiny/config/collector.yaml || true + chmod 755 -R /share/scrutiny +fi + +######## +# MODE # +######## + +if [[ "$(bashio::config "Mode")" == Collector ]]; then + # Clean services + bashio::log.warning "Collector only mode. WebUI and Influxdb will be disabled" + rm -rf /etc/services.d/influxdb + rm -rf /etc/services.d/scrutiny + rm -rf /etc/services.d/nginx + sed -i "/wait/d" /etc/services.d/collector-once/run + sed -i "/scrutiny api not ready/d" /etc/services.d/collector-once/run + + # Check collector + if bashio::config.has_value "COLLECTOR_API_ENDPOINT"; then + echo "export COLLECTOR_API_ENDPOINT=$(bashio::config "COLLECTOR_API_ENDPOINT")" >> /env.sh + sed -i "1a export COLLECTOR_API_ENDPOINT=$(bashio::config "COLLECTOR_API_ENDPOINT")" /etc/services.d/collector-once/run + if [ -d /var/run/s6/container_environment ]; then printf "%s" "$COLLECTOR_API_ENDPOINT" > /var/run/s6/container_environment/COLLECTOR_API_ENDPOINT; fi + printf "%s\n" "IN_BACKGROUND=\"$COLLECTOR_API_ENDPOINT\"" >> ~/.bashrc + bashio::log.info "Using 'COLLECTOR_API_ENDPOINT' $(bashio::config "COLLECTOR_API_ENDPOINT")" + else + bashio::exit.nok "Mode is set to 'Collector', but 'COLLECTOR_API_ENDPOINT' is not defined" + fi +fi diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/mime.types b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/mime.types new file mode 100644 index 0000000000..7c7cdef2d1 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/mime.types @@ -0,0 +1,96 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/proxy_params.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/proxy_params.conf new file mode 100644 index 0000000000..1990d49596 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/proxy_params.conf @@ -0,0 +1,15 @@ +proxy_http_version 1.1; +proxy_ignore_client_abort off; +proxy_read_timeout 86400s; +proxy_redirect off; +proxy_send_timeout 86400s; +proxy_max_temp_file_size 0; + +proxy_set_header Accept-Encoding ""; +proxy_set_header Connection $connection_upgrade; +proxy_set_header Host $http_host; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-NginX-Proxy true; +proxy_set_header X-Real-IP $remote_addr; diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/resolver.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/resolver.conf new file mode 100644 index 0000000000..70f4982b9b --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/resolver.conf @@ -0,0 +1 @@ +resolver 127.0.0.11 ipv6=off; diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/server_params.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/server_params.conf new file mode 100644 index 0000000000..a0ff6baa08 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/server_params.conf @@ -0,0 +1,5 @@ +server_name $hostname; + +add_header X-Content-Type-Options nosniff; +add_header X-XSS-Protection "1; mode=block"; +add_header X-Robots-Tag none; diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/ssl_params.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/ssl_params.conf new file mode 100644 index 0000000000..6f15005998 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/ssl_params.conf @@ -0,0 +1,9 @@ +ssl_protocols TLSv1.2; +ssl_prefer_server_ciphers on; +ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA; +ssl_ecdh_curve secp384r1; +ssl_session_timeout 10m; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; +ssl_stapling on; +ssl_stapling_verify on; diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/includes/upstream.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/upstream.conf new file mode 100644 index 0000000000..b292326bd7 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/includes/upstream.conf @@ -0,0 +1,3 @@ +upstream backend { + server 127.0.0.1:8080; +} diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/nginx.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/nginx.conf new file mode 100644 index 0000000000..7e5bc6f7cb --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/nginx.conf @@ -0,0 +1,56 @@ +# Run nginx in foreground. +daemon off; + +# This is run inside Docker. +user root; + +# Pid storage location. +pid /var/run/nginx.pid; + +# Set number of worker processes. +worker_processes 1; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +# Write error log to Hass.io add-on log. +error_log /proc/1/fd/1 error; + +# Load allowed environment vars +env HASSIO_TOKEN; + +# Load dynamic modules. +include /etc/nginx/modules/*.conf; + +# Max num of simultaneous connections by a worker process. +events { + worker_connections 512; +} + +http { + include /etc/nginx/includes/mime.types; + + log_format hassio '[$time_local] $status ' + '$http_x_forwarded_for($remote_addr) ' + '$request ($http_user_agent)'; + + access_log /proc/1/fd/1 hassio; + client_max_body_size 4G; + default_type application/octet-stream; + gzip on; + keepalive_timeout 65; + sendfile on; + server_tokens off; + tcp_nodelay on; + tcp_nopush on; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + include /etc/nginx/includes/resolver.conf; + include /etc/nginx/includes/upstream.conf; + + include /etc/nginx/servers/*.conf; +} diff --git a/scrutiny_fa/rootfs_overlay/etc/nginx/servers/ingress.conf b/scrutiny_fa/rootfs_overlay/etc/nginx/servers/ingress.conf new file mode 100644 index 0000000000..70b217f39b --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/nginx/servers/ingress.conf @@ -0,0 +1,28 @@ +server { + listen %%interface%%:%%port%% default_server; + + include /etc/nginx/includes/server_params.conf; + include /etc/nginx/includes/proxy_params.conf; + + client_max_body_size 0; + + root /opt/scrutiny/web; + + location = / { + absolute_redirect off; # Do not add port to redirect + return 301 %%ingress_entry%%/web/dashboard; + } + + location /api { + add_header Access-Control-Allow-Origin *; + proxy_read_timeout 30; + proxy_pass http://backend/api; + } + + location /web/ { + add_header Access-Control-Allow-Origin *; + proxy_read_timeout 30; + proxy_pass http://backend/web/; + } + +} diff --git a/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/finish b/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/finish new file mode 100755 index 0000000000..5ca74f2bf5 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/finish @@ -0,0 +1,9 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Stop the container when Nginx fails +# ============================================================================== +if [[ "$1" -ne 0 && "$1" -ne 256 ]]; then + bashio::log.error "Nginx exited with code $1" + kill -15 1 +fi diff --git a/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/run b/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/run new file mode 100755 index 0000000000..43a7f85308 --- /dev/null +++ b/scrutiny_fa/rootfs_overlay/etc/services.d/nginx/run @@ -0,0 +1,11 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e +# ============================================================================== + +# Wait for Scrutiny to become available +bashio::net.wait_for 8080 localhost 900 + +bashio::log.info "Starting NGinx..." + +exec nginx diff --git a/scrutiny_fa/scrutiny-ha-influxdb-preflight b/scrutiny_fa/scrutiny-ha-influxdb-preflight new file mode 100644 index 0000000000..5f7cfcb6ab --- /dev/null +++ b/scrutiny_fa/scrutiny-ha-influxdb-preflight @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +set -euo pipefail + +addon_slug="${SCRUTINY_HA_ADDON_SLUG:-scrutiny_fa}" +data_dir="${SCRUTINY_INFLUXDB_DATA_DIR:-/data/influxdb}" +backup_dir="${SCRUTINY_INFLUXDB_BACKUP_DIR:-/share/${addon_slug}}" +marker_file="${data_dir}/.scrutiny-influxdb-2.9-preflight-complete" +marker_tmp="${marker_file}.tmp" +backup_file="${backup_dir}/influxdb-pre-2.9.tar.gz" +temporary_backup="${backup_file}.tmp" +archive_root="$(basename "$data_dir")" +validation_method="content-sha256-v1" +source_manifest="" +archive_manifest="" + +log() { + local level="$1" + local message="$2" + printf 'time="%s" level=%s msg="%s" type=ha-influxdb-upgrade-preflight\n' \ + "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$level" "$message" +} + +cleanup() { + rm -f "$temporary_backup" "$marker_tmp" + if [[ -n "$source_manifest" ]]; then rm -f "$source_manifest"; fi + if [[ -n "$archive_manifest" ]]; then rm -f "$archive_manifest"; fi +} +trap cleanup EXIT + +has_existing_data() { + if [[ -e "${data_dir}/influxd.bolt" || -e "${data_dir}/influxd.sqlite" ]]; then + return 0 + fi + + if [[ -d "${data_dir}/engine" ]] && find "${data_dir}/engine" -mindepth 1 -print -quit | grep -q .; then + return 0 + fi + + return 1 +} + +validate_archive() { + local archive="$1" + [[ -s "$archive" ]] || return 1 + gzip -t "$archive" >/dev/null 2>&1 || return 1 + tar -tzf "$archive" >/dev/null 2>&1 +} + +reset_manifests() { + rm -f "$source_manifest" "$archive_manifest" + source_manifest="" + archive_manifest="" +} + +validate_backup_against_source() { + local archive="$1" + local relative_path + local source_path + local archive_member + local listing + local source_checksum + local archive_checksum + local source_target + local archive_target + + validate_archive "$archive" || return 1 + source_manifest="$(mktemp)" + archive_manifest="$(mktemp)" + + ( + cd "$data_dir" + find . -mindepth 1 -print | sed -e 's#^\./##' -e 's#/$##' | LC_ALL=C sort + ) > "$source_manifest" + + tar -tzf "$archive" | \ + sed -e "s#^${archive_root}/##" -e '/^$/d' -e 's#/$##' | \ + LC_ALL=C sort > "$archive_manifest" + + if ! cmp -s "$source_manifest" "$archive_manifest"; then + reset_manifests + return 1 + fi + + while IFS= read -r relative_path; do + source_path="${data_dir}/${relative_path}" + archive_member="${archive_root}/${relative_path}" + listing="$(tar -tvzf "$archive" -- "$archive_member" 2>/dev/null)" || { + reset_manifests + return 1 + } + + if [[ -L "$source_path" ]]; then + [[ "${listing:0:1}" == "l" ]] || { + reset_manifests + return 1 + } + source_target="$(readlink "$source_path")" + archive_target="${listing##* -> }" + [[ "$source_target" == "$archive_target" ]] || { + reset_manifests + return 1 + } + elif [[ -f "$source_path" ]]; then + [[ "${listing:0:1}" == "-" || "${listing:0:1}" == "h" ]] || { + reset_manifests + return 1 + } + source_checksum="$(sha256sum "$source_path" | awk '{print $1}')" + archive_checksum="$(tar -xOzf "$archive" -- "$archive_member" | sha256sum | awk '{print $1}')" + [[ "$source_checksum" == "$archive_checksum" ]] || { + reset_manifests + return 1 + } + elif [[ -d "$source_path" ]]; then + [[ "${listing:0:1}" == "d" ]] || { + reset_manifests + return 1 + } + else + reset_manifests + return 1 + fi + done < "$source_manifest" + + reset_manifests +} + +marker_value() { + local key="$1" + awk -F= -v key="$key" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' "$marker_file" +} + +write_marker() { + local state="$1" + local checksum="${2:-}" + local validation="${3:-}" + + { + printf 'state=%s\n' "$state" + if [[ -n "$validation" ]]; then printf 'validation=%s\n' "$validation"; fi + if [[ -n "$checksum" ]]; then printf 'backup_sha256=%s\n' "$checksum"; fi + } > "$marker_tmp" + mv "$marker_tmp" "$marker_file" +} + +validate_existing_marker() { + local state + local validation + local expected_checksum + local actual_checksum + + state="$(marker_value state)" + validation="$(marker_value validation)" + + case "$state" in + no-legacy-data) + if [[ -n "$validation" && "$validation" != "no-data-v1" ]]; then + log error "no-data migration marker uses an unknown validation method" + return 1 + fi + if [[ -z "$validation" ]]; then + write_marker no-legacy-data "" no-data-v1 + log warning "upgraded no-data migration marker with explicit validation state" + fi + return 0 + ;; + legacy-backup) + if [[ "$validation" != "$validation_method" ]]; then + log error "legacy backup marker lacks content-level validation proof" + return 1 + fi + expected_checksum="$(marker_value backup_sha256)" + if [[ -z "$expected_checksum" ]] || ! validate_archive "$backup_file"; then + log error "legacy backup marker exists but its archive cannot be verified" + return 1 + fi + actual_checksum="$(sha256sum "$backup_file" | awk '{print $1}')" + if [[ "$actual_checksum" != "$expected_checksum" ]]; then + log error "legacy backup checksum does not match the migration marker" + return 1 + fi + return 0 + ;; + *) + log error "legacy migration marker lacks content-level validation proof" + return 1 + ;; + esac +} + +mkdir -p "$data_dir" + +if [[ -f "$marker_file" ]]; then + if validate_existing_marker; then exit 0; fi + exit 1 +fi + +if ! has_existing_data; then + write_marker no-legacy-data "" no-data-v1 + log info "no existing InfluxDB data detected; recorded no-data migration state" + exit 0 +fi + +mkdir -p "$backup_dir" + +if [[ -e "$backup_file" ]] && validate_backup_against_source "$backup_file"; then + log info "using existing content-verified InfluxDB backup at ${backup_file}" +else + if [[ -e "$backup_file" ]]; then + log warning "existing InfluxDB backup is stale, incomplete, or invalid and will be atomically replaced" + fi + + log warning "creating offline InfluxDB backup at ${backup_file}" + tar -C "$(dirname "$data_dir")" -czf "$temporary_backup" "$archive_root" + + if ! validate_backup_against_source "$temporary_backup"; then + log error "created InfluxDB backup failed content-level validation" + exit 1 + fi + + mv -f "$temporary_backup" "$backup_file" + log info "InfluxDB backup created and content-verified at ${backup_file}" +fi + +backup_checksum="$(sha256sum "$backup_file" | awk '{print $1}')" +write_marker legacy-backup "$backup_checksum" "$validation_method" +log info "InfluxDB 2.9 migration marker created with content-validation method and backup checksum"