diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 7033e8b..b7cf7ae 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -76,6 +76,9 @@ Vor lokaler Arbeit: Nach lokaler Arbeit: 1. Aenderungen pruefen +2. bei Compose-/Backup-/Restore-Aenderungen relevante manuelle Repo-Checks ausfuehren + - `ops/policy-checks/check_repo.ps1` + - `ops/restore-tests/check-restore-freshness.ps1` oder gezielte Restore-Checks 2. Commit mit sauberer Nachricht 3. `Push origin` 4. Komodo-Webhook im Hinterkopf behalten diff --git a/ops/restore-tests/README.md b/ops/restore-tests/README.md index 4f42fbb..a2bf1f8 100644 --- a/ops/restore-tests/README.md +++ b/ops/restore-tests/README.md @@ -29,6 +29,9 @@ Ziel: - `paperless-restore-test.ps1`: Paperless-Mini-Restore-Ablauf - `paperless-plan.md`: konkreter Paperless-Testplan - `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis +- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports +- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs +- `automation-plan.md`: Host-Job- und Automatisierungsmodell ## Automatisierungsmodell @@ -38,6 +41,11 @@ Ziel: - Meldung: `ntfy` - Hermes: optional nur fuer Zusammenfassung und Auswertung +Wichtig: + +- `check-restore-freshness.ps1` und spaetere automatische Restore-Jobs sind fuer den Unraid-Host gedacht +- im Windows-Clone fehlen die `/mnt/user/...`-Pfade naturgemaess + ## Validiertes Grundmuster Stand nach dem ersten echten Vaultwarden-Test: diff --git a/ops/restore-tests/automation-plan.md b/ops/restore-tests/automation-plan.md new file mode 100644 index 0000000..33e7107 --- /dev/null +++ b/ops/restore-tests/automation-plan.md @@ -0,0 +1,70 @@ +# Restore Automation Plan + +## Ziel + +Die bereits validierten Restore-Tests fuer `vaultwarden`, `gitea` und `paperless` sollen regelmaessig mit wenig Handarbeit laufen. + +## Prinzip + +- Ausfuehrung bleibt hostseitig +- Logik bleibt im Repo +- Reports bleiben unter `/mnt/user/backups/restore-reports` +- Restore-Arbeitsdaten bleiben unter `/mnt/user/backups/restore-lab` +- Hermes ist Reporter, nicht Operator + +## V1 + +### Woechentlicher Frische-Check + +- Script: `check-restore-freshness.ps1` +- Ziel: + - Dump-Dateien vorhanden + - Dump-Dateien nicht zu alt + - letzte Restore-Reports vorhanden +- Wirkung: + - schneller Fruehwarncheck ohne Containerstart + +### Monatliche / zweimonatliche Restore-Jobs + +- Script-Dispatcher: `run-restore-checks.ps1` +- Modi: + - `freshness` + - `vaultwarden` + - `gitea` + - `paperless` +- V1 ruft die existierenden dienstspezifischen Scripts zunaechst im `WhatIf`- oder Plan-Modus auf, bis die Vollautomatisierung je Dienst gezielt nachgezogen wird. + +## V2 + +- echte Vollautomatisierung pro Dienst +- `ntfy` Erfolg/Fehler +- optional Hermes-Zusammenfassung ueber vorhandene Reports + +## Host-Integration + +Empfohlen: + +- Unraid User Scripts +- je ein geplanter Job pro Laufklasse +- Ausfuehrung auf dem Unraid-Host, nicht im Windows-Clone + +Beispiel: + +1. `restore-freshness-weekly` +2. `restore-vaultwarden-monthly` +3. `restore-gitea-monthly` +4. `restore-paperless-bimonthly` + +## Erfolgskriterium + +Ein automatisierter Lauf ist nur dann erfolgreich, wenn: + +- Script sauber endet +- Report geschrieben wird +- bei echten Restore-Laeufen der definierte Smoke-Test erfolgreich war + +## Nicht automatisieren + +- neue Restore-Typen ohne bewusste Freigabe +- invasive Produktiv-Restores +- Komodo-/Auth-/Secret-Umbauten im selben Job diff --git a/ops/restore-tests/check-restore-freshness.ps1 b/ops/restore-tests/check-restore-freshness.ps1 new file mode 100644 index 0000000..6d7473f --- /dev/null +++ b/ops/restore-tests/check-restore-freshness.ps1 @@ -0,0 +1,88 @@ +param( + [string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest", + [string]$ReportRoot = "/mnt/user/backups/restore-reports", + [int]$MaxDumpAgeHours = 36, + [int]$MaxReportAgeDays = 45 +) + +$checks = @( + @{ 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" } +) + +$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 + $ageHours = ($now - $item.LastWriteTime).TotalHours + if ($ageHours -gt $MaxDumpAgeHours) { + $warnings.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) { + $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 diff --git a/ops/restore-tests/run-restore-checks.ps1 b/ops/restore-tests/run-restore-checks.ps1 new file mode 100644 index 0000000..e8897e1 --- /dev/null +++ b/ops/restore-tests/run-restore-checks.ps1 @@ -0,0 +1,38 @@ +param( + [ValidateSet("freshness","vaultwarden","gitea","paperless")] + [string]$Mode, + [switch]$WhatIf +) + +$base = Split-Path -Parent $MyInvocation.MyCommand.Path + +switch ($Mode) { + "freshness" { + & (Join-Path $base "check-restore-freshness.ps1") + exit $LASTEXITCODE + } + "vaultwarden" { + if ($WhatIf) { + & (Join-Path $base "vaultwarden-restore-test.ps1") -WhatIf + } else { + & (Join-Path $base "vaultwarden-restore-test.ps1") + } + exit $LASTEXITCODE + } + "gitea" { + if ($WhatIf) { + & (Join-Path $base "gitea-restore-test.ps1") -WhatIf + } else { + & (Join-Path $base "gitea-restore-test.ps1") + } + exit $LASTEXITCODE + } + "paperless" { + if ($WhatIf) { + & (Join-Path $base "paperless-restore-test.ps1") -WhatIf + } else { + & (Join-Path $base "paperless-restore-test.ps1") + } + exit $LASTEXITCODE + } +} diff --git a/ops/restore-tests/schedule.md b/ops/restore-tests/schedule.md index 01b7b98..62dd7d3 100644 --- a/ops/restore-tests/schedule.md +++ b/ops/restore-tests/schedule.md @@ -37,6 +37,30 @@ Spaeter: - `immich` als eigener Sprint +## Konkreter Kalender + +- Jeden Montag, 06:30: + - `check-restore-freshness.ps1` +- Jeden 1. Samstag im Monat, 07:00: + - `vaultwarden` +- Jeden 3. Samstag im Monat, 07:00: + - `gitea` +- Jeden 2. Monat am 2. Samstag, 08:00: + - `paperless` +- Quartalsweise am 1. Werktag des Quartals: + - DR-/Restore-Sanity-Check + +## Betriebsmodus + +- V1: + - Jobs laufen hostseitig manuell oder per User Script + - `ntfy` ist optional und folgt nach stabiler Basis + - Hermes wertet spaeter nur Reports aus +- V2: + - fester Host-Schedule + - `ntfy` bei Erfolg/Fehler + - Hermes erzeugt Zusammenfassungen und Overviews + ## Automatisierung Automatisch: