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
This commit is contained in:
Alexandre
2026-07-18 21:51:14 +02:00
committed by GitHub
parent 505ac96536
commit bbaa0900a4
25 changed files with 1116 additions and 44 deletions

View File

@@ -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.

View File

@@ -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
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1

View File

@@ -113,4 +113,4 @@ schema:
slug: scrutiny
udev: true
url: https://github.com/Starosdev/scrutiny
version: "v1.67.0-2"
version: "v1.67.0-8"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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..."

View File

@@ -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"

View File

@@ -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.

View File

@@ -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
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1

View File

@@ -45,4 +45,4 @@ schema:
slug: scrutiny_fa
udev: true
url: https://github.com/Starosdev/scrutiny
version: "v1.67.0-2"
version: "v1.67.0-8"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
upstream backend {
server 127.0.0.1:8080;
}

View File

@@ -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;
}

View File

@@ -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/;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"