From 38c3d87722cba194f444f2a395c1e233b05bdec1 Mon Sep 17 00:00:00 2001 From: Micha Date: Wed, 27 May 2026 06:25:47 +0200 Subject: [PATCH] Prepare H drive nearline pull --- docs/AUDIT_2026-05-25_TODO.md | 4 +- docs/CAPACITY_AND_LIFECYCLE.md | 3 + docs/H_DRIVE_NEARLINE_PULL.md | 78 ++++++++++ docs/IMMICH_RESTORE_TEST.md | 6 +- docs/MIGRATION_LOG.md | 7 + .../pull-critical-backups.ps1 | 138 ++++++++++++++++++ 6 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 docs/H_DRIVE_NEARLINE_PULL.md create mode 100644 ops/h-drive-nearline/pull-critical-backups.ps1 diff --git a/docs/AUDIT_2026-05-25_TODO.md b/docs/AUDIT_2026-05-25_TODO.md index 52f3c7f..8524d19 100644 --- a/docs/AUDIT_2026-05-25_TODO.md +++ b/docs/AUDIT_2026-05-25_TODO.md @@ -61,7 +61,7 @@ Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren: | Status | Aufgabe | Ergebnis | |---|---|---| -| in Arbeit (vorbereitet) | Immich-Restore-Test implementieren | `ops/restore-tests/immich-restore-test.sh`, `immich-compose.test.yml` und Dispatcher-Eintrag vorbereitet; lokaler `--what-if` erfolgreich; Abschluss erst nach echtem Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/` | +| in Arbeit (vorbereitet) | Immich-Restore-Test implementieren | `ops/restore-tests/immich-restore-test.sh`, `immich-compose.test.yml` und Dispatcher-Eintrag vorbereitet; lokaler und Host-`--what-if` erfolgreich; Host-Preflight 2026-05-27: `immich.dump` 66M, `/mnt/user/backups` ca. 3.7T frei; Abschluss erst nach echtem Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/` | | offen | Borg-Stale-Alert bauen | Alarm feuert, wenn Borg-Archiv zu alt ist | | offen | TLS-Cert-Expiry-Alert bauen | Alarm feuert bei Restlaufzeit unter Schwellwert | | offen | Container-Down-Alert bauen | Unerwartet fehlende Container werden sichtbar | @@ -75,7 +75,7 @@ Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren: | erledigt (Baseline) | Capacity-/Lifecycle-Review erstellen | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; H:/-Nearline-Bewertung ergaenzt; externe Cold-Storage-Groessen bleiben offen | | erledigt | USV-Test oder USV-Entscheidung | Operator-Entscheidung 2026-05-26: aktuell keine USV-Anschaffung; Power-Loss-Risiko wird bewusst akzeptiert und dokumentiert | | erledigt (Baseline) | H:/ als zusaetzliches lokales Backupziel bewerten | Als zweite Nearline-Kopie und Freeze-Sicherung sinnvoll; kein Offsite-Ersatz, kein CIFS-Hard-Mount am Unraid; Pull-Modell vom Windows-PC ist der getestete Weg (siehe `docs/CAPACITY_AND_LIFECYCLE.md`) | -| in Arbeit | H:/ Groesse und Pull-Schedule festschreiben | Groesse erfasst: 8.0T NTFS, 3.91T belegt, 4.10T frei, `Healthy`; Pull der Borg-Dumps, Gitea-Bundles und Flash-Backups noch planen | +| in Arbeit (vorbereitet) | H:/ Groesse und Pull-Schedule festschreiben | Groesse erfasst: 8.0T NTFS, 3.91T belegt, 4.10T frei, `Healthy`; `docs/H_DRIVE_NEARLINE_PULL.md` und `ops/h-drive-nearline/pull-critical-backups.ps1` vorbereitet; SMB-Quelle erreichbar; empfohlener Schedule taeglich 05:30, Task noch nicht aktiviert | | offen | FRITZ!Box-Portfreigaben gegen Repo-Soll abgleichen | UI -> Internet -> Freigaben pruefen; nur `443/tcp -> 192.168.178.58` und `222/tcp -> 192.168.178.58` sollten aktiv sein | ## Sprint 5 - Auth und Frontdoor, bewusst zuletzt diff --git a/docs/CAPACITY_AND_LIFECYCLE.md b/docs/CAPACITY_AND_LIFECYCLE.md index 8fbc9a0..aeb3a1d 100644 --- a/docs/CAPACITY_AND_LIFECYCLE.md +++ b/docs/CAPACITY_AND_LIFECYCLE.md @@ -78,6 +78,8 @@ H:/ ist **keine echte Offsite-/Airgap-Kopie und kein Ersatz fuer Hetzner**. Es i | Pull von `/mnt/user/backups/borg/dumps/latest` auf H:/ | Windows-seitiger Scheduled Task per `robocopy` oder `rclone` von einem SMB-Read-Share | keine CIFS-Hard-Mounts auf Unraid; STORAGE_LAYOUT-Konstitution bleibt erhalten | | Pull der Gitea-Bundles aus `/mnt/user/backups/git-bundles/gitea` | identisch | Bundles sind klein und schnell synchronisiert | | Pull des Unraid-Flash-Artefakts `unraid-flash-config.tar.gz` | identisch | wie Secret behandeln, Windows-seitig nicht in geteilten Ordnern ablegen | + +Der konkrete Pull-Pfad ist in `docs/H_DRIVE_NEARLINE_PULL.md` und `ops/h-drive-nearline/pull-critical-backups.ps1` vorbereitet. Der Windows Scheduled Task wird erst nach Operator-Sichtpruefung aktiviert. | **Nicht** als Ersatz fuer Hetzner-Off-site | — | 3-2-1 bleibt mit Hetzner als einzigem Off-site weiterhin unerfuellt; siehe `docs/AUDIT_2026-05-25.md` F-03 | | **Nicht** als zweites Borg-Repo am Unraid | — | dauerhafte CIFS-Verbindung im Borg-Lauf verletzt Hard Rule §12.6 | @@ -108,3 +110,4 @@ H:/ ist **keine echte Offsite-/Airgap-Kopie und kein Ersatz fuer Hetzner**. Es i | 2026-05-26 | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; keine validierte USV-Abschaltung | Capacity gruen; USV wird aktuell nicht angeschafft, Power-Loss-Risiko bewusst akzeptiert; externe Backup-/Cold-Storage-Groessen bleiben offen | | 2026-05-26 | H:/ als dauerhaft verbundenes Windows-Laufwerk evaluiert | als zweite lokale Nearline-Kopie und Freeze-Sicherung sinnvoll; nicht als Offsite-Ersatz und nicht als Borg-CIFS-Hard-Mount am Unraid; Pull-Modell vom Windows-PC bleibt der getestete Weg | | 2026-05-26 | H:/ Kapazitaet erfasst: 8.0T NTFS, 3.91T belegt, 4.10T frei, `Healthy` | genug Reserve fuer Nearline-Pull der kritischen Restore-Artefakte; Pull-Schedule bleibt offen | +| 2026-05-27 | H:/ Pull-Workflow vorbereitet | SMB-Quelle `\\192.168.178.58\backups` erreichbar; PowerShell-Skript und Runbook erstellt; empfohlener Schedule taeglich 05:30, aber Task noch nicht aktiviert | diff --git a/docs/H_DRIVE_NEARLINE_PULL.md b/docs/H_DRIVE_NEARLINE_PULL.md new file mode 100644 index 0000000..82994e2 --- /dev/null +++ b/docs/H_DRIVE_NEARLINE_PULL.md @@ -0,0 +1,78 @@ +# H:/ Nearline Pull + +Status: vorbereitet 2026-05-27; noch kein geplanter Windows Task aktiv. + +## Zweck + +`H:/` ist eine zweite lokale Nearline-Kopie fuer die wichtigsten Restore-Artefakte. Es ersetzt weder Hetzner/Borg noch ein echtes Off-site-/Airgap-Ziel, reduziert aber das Risiko, dass ein lokaler Restore nur vom Unraid-Array abhaengt. + +## Quelle und Ziel + +| Zweck | Quelle | Ziel | +|---|---|---| +| Aktuelle Dumps inklusive Flash-Backup | `\\192.168.178.58\backups\borg\dumps\latest` | `H:\kallilab-nearline-backups\borg-dumps\latest` | +| Gitea-Bundles | `\\192.168.178.58\backups\git-bundles\gitea` | `H:\kallilab-nearline-backups\git-bundles\gitea` | + +Das Skript kopiert bewusst **nicht** mit `/MIR` und loescht keine Dateien auf `H:/`. Alte Artefakte duerfen dort erst nach manueller Sichtpruefung geloescht werden. + +## Skript + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1 -WhatIf +``` + +Echter Lauf: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1 +``` + +Reports landen unter: + +```text +H:\kallilab-nearline-backups\_reports +``` + +Robocopy-Logs landen unter: + +```text +H:\kallilab-nearline-backups\_logs +``` + +## Geplanter Schedule + +Empfohlen: taeglich 05:30 Uhr, nach dem Borg-Dump-Fenster um ca. 04:00 Uhr. + +Der Task wird erst nach Operator-Sichtpruefung angelegt. Vorschlag: + +```powershell +$Action = New-ScheduledTaskAction ` + -Execute "powershell.exe" ` + -Argument "-NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1" + +$Trigger = New-ScheduledTaskTrigger -Daily -At 05:30 + +Register-ScheduledTask ` + -TaskName "KalliLab H Drive Nearline Pull" ` + -Action $Action ` + -Trigger $Trigger ` + -Description "Copies critical KalliLab restore artifacts from Unraid SMB backup share to H:/ nearline disk." ` + -RunLevel LeastPrivilege +``` + +## Erfolgscheck + +Nach einem echten Lauf muessen mindestens diese Artefakte unter `H:\kallilab-nearline-backups` liegen: + +- `borg-dumps\latest\immich.dump` +- `borg-dumps\latest\komodo-mongo.archive.gz` +- `borg-dumps\latest\unraid-flash-config.tar.gz` +- `git-bundles\gitea\latest-report.md` +- `git-bundles\gitea\micha\*.bundle` + +## Schutzregeln + +- Kein CIFS-/SMB-Hard-Mount von `H:/` auf Unraid. +- Kein Borg-Repo direkt auf `H:/` ueber SMB. +- Kein `/MIR` und kein automatisches Loeschen auf `H:/`. +- Flash-Backup wie Secret behandeln; `H:/` bleibt lokaler Operator-Datentraeger. diff --git a/docs/IMMICH_RESTORE_TEST.md b/docs/IMMICH_RESTORE_TEST.md index aa18f02..3c03dbf 100644 --- a/docs/IMMICH_RESTORE_TEST.md +++ b/docs/IMMICH_RESTORE_TEST.md @@ -42,10 +42,10 @@ Der Test deckt **Stufe 4 (kritische Anwendungen)** aus `docs/DISASTER_RECOVERY.m | Pruefung | Verantwortlich | Wo | |---|---|---| -| Dump-Groesse von `immich.dump` bestimmen | Operator | `ls -lh /mnt/user/backups/borg/dumps/latest/immich.dump` | -| Freier Platz unter `/mnt/user/backups/restore-lab/` | Operator | `df -h /mnt/user/backups` | +| Dump-Groesse von `immich.dump` bestimmen | erledigt 2026-05-27 | 66M unter `/mnt/user/backups/borg/dumps/latest/immich.dump` | +| Freier Platz unter `/mnt/user/backups/restore-lab/` | erledigt 2026-05-27 | ca. 3.7T frei auf `/mnt/user/backups` | | Borg-UI-Container laeuft | Operator | `docker ps | grep borg-ui` | -| Trockenlauf mit `--what-if` | Operator | `bash ops/restore-tests/immich-restore-test.sh --what-if` | +| Trockenlauf mit `--what-if` | erledigt 2026-05-27 | Host-Clone auf `c5d231a`, `bash ops/restore-tests/run-restore-checks.sh immich --what-if` erfolgreich | | Erster echter Lauf mit `--keep-data` zur Zeitmessung | Operator | `bash ops/restore-tests/immich-restore-test.sh --keep-data` | ## Nach dem ersten erfolgreichen Lauf diff --git a/docs/MIGRATION_LOG.md b/docs/MIGRATION_LOG.md index b710f7c..5d93434 100644 --- a/docs/MIGRATION_LOG.md +++ b/docs/MIGRATION_LOG.md @@ -22,8 +22,15 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab - `docs/IMMICH_RESTORE_TEST.md` und `ops/restore-tests/immich-plan.md`/`immich-runbook.md` beschreiben den geplanten Immich-Mini-Restore: `immich.dump` aus Borg, isolierter pgvecto-rs-Test-Postgres, Test-Redis, Immich-Server ohne ML, lokaler Port `127.0.0.1:12283`, keine produktiven Foto-Mounts. - `ops/restore-tests/immich-restore-test.sh`, `immich-restore-test.ps1` und `immich-compose.test.yml` wurden vorbereitet; der Dispatcher kennt `immich --what-if`. - Lokal verifiziert: Bash-Syntax, `run-restore-checks.sh immich --what-if`, PowerShell-Dispatcher `-Mode immich -WhatIf`, Docker-Compose-Render und Policy-Check. Kein echter Host-Restore, kein Borg-Extract, kein Produktiv-Container-Eingriff. +- Host-Preflight 2026-05-27: Host-Clone per Fast-forward auf `c5d231a`, `immich.dump` 66M, `/mnt/user/backups` ca. 3.7T frei, `run-restore-checks.sh immich --what-if` erfolgreich. Kein echter Restore-Lauf. - F-11 bleibt fachlich offen bis zum ersten Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`. +### 2026-05-27 - H:/ Nearline-Pull vorbereitet + +- `docs/H_DRIVE_NEARLINE_PULL.md` und `ops/h-drive-nearline/pull-critical-backups.ps1` definieren den Windows-seitigen Pull von `\\192.168.178.58\backups\borg\dumps\latest` und `\\192.168.178.58\backups\git-bundles\gitea` nach `H:\kallilab-nearline-backups`. +- SMB-Quelle `\\192.168.178.58\backups` und H:/ sind erreichbar; `-WhatIf` prueft den Plan ohne Kopie. +- Kein Scheduled Task angelegt und kein echter Kopierlauf gestartet. Empfohlen ist taeglich 05:30 nach dem Borg-Dump-Fenster, Aktivierung erst nach Operator-Sichtpruefung. + ### 2026-05-26 - Audit F-16 und F-20 abgeschlossen (Doku-only) - F-16: `infra/redis`-Etikett auf die Realitaet abgeglichen. `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md`, `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13 und `docs/DISASTER_RECOVERY.md` Bootstrap-Stufe 2 beschreiben Redis jetzt als "primaer Paperless-Redis (App-Cache); historisch als shared angelegt, faktisch nur von Paperless genutzt". Immich, Nextcloud, Mealie eigene Redis-Instanzen; Authelia bewusst ohne Redis. Keine Compose-Aenderung. diff --git a/ops/h-drive-nearline/pull-critical-backups.ps1 b/ops/h-drive-nearline/pull-critical-backups.ps1 new file mode 100644 index 0000000..2063f43 --- /dev/null +++ b/ops/h-drive-nearline/pull-critical-backups.ps1 @@ -0,0 +1,138 @@ +param( + [string]$SourceRoot = "\\192.168.178.58\backups", + [string]$DestinationRoot = "H:\kallilab-nearline-backups", + [switch]$WhatIf +) + +$ErrorActionPreference = "Stop" + +$Jobs = @( + @{ + Name = "borg-dumps-latest" + Source = Join-Path $SourceRoot "borg\dumps\latest" + Destination = Join-Path $DestinationRoot "borg-dumps\latest" + Purpose = "Latest database/application dumps, including unraid-flash-config.tar.gz" + }, + @{ + Name = "gitea-bundles" + Source = Join-Path $SourceRoot "git-bundles\gitea" + Destination = Join-Path $DestinationRoot "git-bundles\gitea" + Purpose = "Verified bare-repository bundles for Gitea bootstrap" + } +) + +function Assert-PathExists { + param( + [string]$Path, + [string]$Label + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "$Label not found: $Path" + } +} + +function Invoke-RobocopyJob { + param( + [hashtable]$Job, + [string]$LogRoot + ) + + $logPath = Join-Path $LogRoot ("{0}-{1}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss"), $Job.Name) + New-Item -ItemType Directory -Force -Path $Job.Destination | Out-Null + + $args = @( + $Job.Source, + $Job.Destination, + "/E", + "/COPY:DAT", + "/DCOPY:DAT", + "/R:2", + "/W:5", + "/FFT", + "/XJ", + "/XD", + ".tmp", + "/NP", + "/TEE", + "/LOG:$logPath" + ) + + Write-Host "Running robocopy job: $($Job.Name)" + Write-Host " Source: $($Job.Source)" + Write-Host " Destination: $($Job.Destination)" + + & robocopy @args + $code = $LASTEXITCODE + + if ($code -gt 7) { + throw "Robocopy job '$($Job.Name)' failed with exit code $code. See log: $logPath" + } + + [pscustomobject]@{ + Name = $Job.Name + Source = $Job.Source + Destination = $Job.Destination + ExitCode = $code + Log = $logPath + } +} + +Assert-PathExists -Path $SourceRoot -Label "Source root" + +foreach ($job in $Jobs) { + Assert-PathExists -Path $job.Source -Label "Source for job '$($job.Name)'" +} + +if ($WhatIf) { + Write-Host "H:/ nearline pull plan only. No files will be copied." + Write-Host "SourceRoot: $SourceRoot" + Write-Host "DestinationRoot: $DestinationRoot" + Write-Host "" + foreach ($job in $Jobs) { + Write-Host "- $($job.Name)" + Write-Host " Purpose: $($job.Purpose)" + Write-Host " Source: $($job.Source)" + Write-Host " Destination: $($job.Destination)" + } + exit 0 +} + +$destinationDrive = Split-Path -Qualifier $DestinationRoot +Assert-PathExists -Path $destinationDrive -Label "Destination drive" + +$logRoot = Join-Path $DestinationRoot "_logs" +$reportRoot = Join-Path $DestinationRoot "_reports" +New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null + +$results = foreach ($job in $Jobs) { + Invoke-RobocopyJob -Job $job -LogRoot $logRoot +} + +$reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss")) + +$lines = @() +$lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +$lines += "" +$lines += "- Source root: ``$SourceRoot``" +$lines += "- Destination root: ``$DestinationRoot``" +$lines += "- Mode: non-destructive copy, no ``/MIR``, no purge" +$lines += "" +$lines += "| Job | Exit code | Source | Destination | Log |" +$lines += "|---|---:|---|---|---|" +foreach ($result in $results) { + $lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |" +} +$lines += "" +$lines += "Expected critical artifacts after run:" +$lines += "" +$lines += "- ``borg-dumps/latest/immich.dump``" +$lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``" +$lines += "- ``borg-dumps/latest/unraid-flash-config.tar.gz``" +$lines += "- ``git-bundles/gitea/latest-report.md``" +$lines += "- ``git-bundles/gitea/micha/*.bundle``" + +$lines | Set-Content -LiteralPath $reportPath -Encoding UTF8 + +Write-Host "H:/ nearline pull completed." +Write-Host "Report: $reportPath"