Files
homelab-infra/ops/restore-tests/check-restore-freshness.ps1
T
Micha bc9ace315a Backup-Audit-Hardening: Dump-Frische-Monitoring und Scope-Konsistenz
Findings aus dem Backup-/Restore-Audit 2026-06-18 umgesetzt:

- Dump-Frische als Prometheus-Metrik (homelab_borg_dump_present /
  homelab_borg_dump_age_seconds) im Host-Exporter; schliesst den
  Blindfleck, dass Borg weiterlaeuft und stale Dumps archiviert, ohne
  Job-Fehler.
- Neue Alerts HomelabBorgDumpMissing / HomelabBorgDumpStale (critical)
  plus ALERT_RULES.md.
- Freshness-Gate (.sh + .ps1) und H:-Nearline-Pull um n8n.sqlite.dump
  und postgresql17-globals.sql ergaenzt.
- Critical-Container-Watch um mail-archiver, n8n, homeassistant,
  smarthome-mosquitto erweitert.
- BACKUP_SCOPE: /mnt/user/projekte und sonstige User-Shares ausserhalb
  App-Scope als bewusste offene Operator-Entscheidung dokumentiert;
  Hermes-data-Pfad als geparkt klargestellt.
- MASTER_TODO: Nearline-Pull-Ueberwachung, Host-Pull-Nachzug und
  projekte-Scope-Entscheidung aufgenommen.

Enthaelt ausserdem die zuvor vorbereiteten Scope-Erweiterungen
(nextcloud html+data, n8n, filebrowser, influxdb3) und Scope-Drift-/
Retention-/Compact-/Check-Alerts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:25:54 +02:00

107 lines
3.6 KiB
PowerShell

param(
[string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest",
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
[int]$MaxDumpAgeHours = 26,
[int]$MaxReportAgeDays = 45
)
$checks = @(
@{ Name = "postgresql17-globals.sql"; Path = Join-Path $DumpRoot "postgresql17-globals.sql" },
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
@{ Name = "immich.dump"; Path = Join-Path $DumpRoot "immich.dump" },
@{ Name = "nextcloud.dump"; Path = Join-Path $DumpRoot "nextcloud.dump" },
@{ Name = "gitea.sqlite.dump"; Path = Join-Path $DumpRoot "gitea.sqlite.dump" },
@{ Name = "vaultwarden.sqlite.dump"; Path = Join-Path $DumpRoot "vaultwarden.sqlite.dump" },
@{ Name = "n8n.sqlite.dump"; Path = Join-Path $DumpRoot "n8n.sqlite.dump" },
@{ Name = "speedtest-tracker.sqlite.dump"; Path = Join-Path $DumpRoot "speedtest-tracker.sqlite.dump" },
@{ Name = "filebrowser.bolt.dump"; Path = Join-Path $DumpRoot "filebrowser.bolt.dump" },
@{ Name = "unraid-flash-config.tar.gz"; Path = Join-Path $DumpRoot "unraid-flash-config.tar.gz" }
)
$reportChecks = @(
@{ Name = "vaultwarden"; Path = Join-Path $ReportRoot "vaultwarden-*.md" },
@{ Name = "gitea"; Path = Join-Path $ReportRoot "gitea-*.md" },
@{ Name = "paperless"; Path = Join-Path $ReportRoot "paperless-*.md" }
)
$now = Get-Date
$critical = New-Object System.Collections.Generic.List[string]
$warnings = New-Object System.Collections.Generic.List[string]
$info = New-Object System.Collections.Generic.List[string]
foreach ($check in $checks) {
if (-not (Test-Path $check.Path)) {
$critical.Add("DUMP_MISSING $($check.Name)")
continue
}
$item = Get-Item $check.Path
if ($item.Length -le 0) {
$critical.Add("DUMP_EMPTY $($check.Name)")
continue
}
$ageHours = ($now - $item.LastWriteTime).TotalHours
if ($ageHours -gt $MaxDumpAgeHours) {
$critical.Add(("DUMP_STALE {0} age={1:N1}h" -f $check.Name, $ageHours))
} else {
$info.Add(("DUMP_OK {0} age={1:N1}h" -f $check.Name, $ageHours))
}
}
foreach ($check in $reportChecks) {
if (-not (Test-Path $ReportRoot)) {
$warnings.Add("REPORT_ROOT_MISSING $ReportRoot")
break
}
$latest = Get-ChildItem -Path $ReportRoot -Filter ([System.IO.Path]::GetFileName($check.Path)) -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $latest) {
$warnings.Add("REPORT_MISSING $($check.Name)")
continue
}
$ageDays = ($now - $latest.LastWriteTime).TotalDays
if ($ageDays -gt $MaxReportAgeDays) {
$warnings.Add(("REPORT_STALE {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
} else {
$info.Add(("REPORT_OK {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
}
}
Write-Output "# Restore Freshness Check"
Write-Output ""
Write-Output ("Timestamp: {0}" -f $now.ToString("yyyy-MM-dd HH:mm:ss"))
Write-Output ("Critical: {0}" -f $critical.Count)
Write-Output ("Warnings: {0}" -f $warnings.Count)
Write-Output ("Info: {0}" -f $info.Count)
Write-Output ""
if ($critical.Count -gt 0) {
Write-Output "## Critical"
$critical | ForEach-Object { Write-Output ("- {0}" -f $_) }
Write-Output ""
}
if ($warnings.Count -gt 0) {
Write-Output "## Warnings"
$warnings | ForEach-Object { Write-Output ("- {0}" -f $_) }
Write-Output ""
}
if ($info.Count -gt 0) {
Write-Output "## Info"
$info | ForEach-Object { Write-Output ("- {0}" -f $_) }
}
if ($critical.Count -gt 0) {
exit 1
}
exit 0