diff --git a/archive/README.md b/archive/README.md index b109250..b7f15a5 100644 --- a/archive/README.md +++ b/archive/README.md @@ -12,7 +12,7 @@ Datenbank-Backup-Strategie) — weitere Bausteine folgen ticketweise. ```bash export NEXARCH_BACKUP_PG_USER=nexarch_backup export NEXARCH_BACKUP_PG_PASSWORD=... -export NEXARCH_BACKUP_DIR=/var/backups/nexarch +export NEXARCH_BACKUP_DIR=/var/nexarch-archiv/backups/postgres # NICHT auf einem ephemeren Test-Dataset (siehe Betrieb) export NEXARCH_BACKUP_KEEP_GENERATIONS=7 # optional, Default 7 backup-cli full # neue Vollsicherung + Verifikation @@ -26,6 +26,35 @@ Replikationsverbindung), und `summarize_wal = on` muss serverseitig gesetzt sein (PostgreSQL 17s natives inkrementelles Backup, keine WAL-Archivierung nötig). +## BAK-02: Objekt-Storage-Backup + +`cmd/objectbackup-cli` sichert einen lokalen Verzeichnisbaum (den +FDN-03-`LocalDriver`-Basisordner direkt, oder — für S3-gestützte +Deployments — einen vorgelagerten `rclone`-Spiegel) mit +[restic](https://restic.net) (Content-defined Chunking, verschlüsseltes +Repository, geprüftes Tooling statt Eigenbau): + +```bash +export NEXARCH_OBJECTBACKUP_REPO_DIR=/var/nexarch-archiv/backups/objects +export NEXARCH_OBJECTBACKUP_PASSWORD=... +export NEXARCH_OBJECTBACKUP_KEEP_SNAPSHOTS=30 # optional, Default 7 + +objectbackup-cli backup /var/nexarch-objects # Sicherung + Verifikation +objectbackup-cli check # vollständiges Lesen aller Datenblöcke +objectbackup-cli rotate # restic forget --keep-last N --prune +``` + +## Betrieb: Backup-Zielverzeichnis + +Backup-Ziele liegen unter `/var/nexarch-archiv/` (persistentes ZFS-Dataset, +`zfs/data/subvol-1131-disk-0` auf 192.168.1.131), NIEMALS unter +`/var/nexarch-test/` (ephemeres Dataset, wird von den `reset-test-env.sh`- +Skripten der anderen Module geleert). ZFS-seitige Snapshots/Replikation +dieses Datasets sind ein eigenständiges Infra-Runbook (siehe +`../../STORAGE-KONZEPT.md` Abschnitt 7), kein Ticket-Code — `zfs +dedup=on` bewusst NICHT setzen (hoher RAM-Bedarf), Deduplizierung läuft +ausschließlich App-seitig über restic. + ## Prüfungen ```bash diff --git a/archive/cmd/objectbackup-cli/main.go b/archive/cmd/objectbackup-cli/main.go new file mode 100644 index 0000000..2430379 --- /dev/null +++ b/archive/cmd/objectbackup-cli/main.go @@ -0,0 +1,75 @@ +// objectbackup-cli ist der Aufrufpunkt für BAK-02, für systemd-Timer +// gedacht (siehe deploy/systemd/) — "automatisiert nach Zeitplan" entsteht +// durch die Timer-Definition, kein eigener Dauerprozess (dieselbe +// Begründung wie BAK-01 / cmd/backup-cli). +package main + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + + "gitea.perlbach24.de/scripte/nexarch/archive/internal/objectbackup" +) + +func loadConfig() objectbackup.Config { + cfg := objectbackup.Config{ + RepoDir: os.Getenv("NEXARCH_OBJECTBACKUP_REPO_DIR"), + Password: os.Getenv("NEXARCH_OBJECTBACKUP_PASSWORD"), + } + if cfg.RepoDir == "" || cfg.Password == "" { + log.Fatal("NEXARCH_OBJECTBACKUP_REPO_DIR und NEXARCH_OBJECTBACKUP_PASSWORD muessen gesetzt sein") + } + return cfg +} + +func main() { + if len(os.Args) < 2 { + log.Fatal("aufruf: objectbackup-cli |check|rotate>") + } + cfg := loadConfig() + ctx := context.Background() + + if err := objectbackup.InitRepo(ctx, cfg); err != nil { + log.Fatalf("repository initialisieren: %v", err) + } + + switch os.Args[1] { + case "backup": + if len(os.Args) < 3 { + log.Fatal("aufruf: objectbackup-cli backup ") + } + summary, err := objectbackup.Backup(ctx, cfg, os.Args[2]) + if err != nil { + log.Fatalf("sicherung fehlgeschlagen: %v", err) + } + if err := objectbackup.Check(ctx, cfg, false); err != nil { + log.Fatalf("verifikation nach sicherung fehlgeschlagen: %v", err) + } + fmt.Printf("sicherung %q erstellt und verifiziert (neu=%d geaendert=%d unveraendert=%d)\n", + summary.SnapshotID, summary.FilesNew, summary.FilesChanged, summary.FilesUnmodified) + + case "check": + if err := objectbackup.Check(ctx, cfg, true); err != nil { + log.Fatalf("verifikation fehlgeschlagen: %v", err) + } + fmt.Println("verifikation (mit vollstaendigem lesen) erfolgreich") + + case "rotate": + keep := 7 + if v := os.Getenv("NEXARCH_OBJECTBACKUP_KEEP_SNAPSHOTS"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + keep = n + } + } + if err := objectbackup.Forget(ctx, cfg, keep); err != nil { + log.Fatalf("rotation fehlgeschlagen: %v", err) + } + fmt.Println("rotation abgeschlossen") + + default: + log.Fatalf("unbekannter befehl %q", os.Args[1]) + } +} diff --git a/archive/docs/BAK-01-PRUEFPROTOKOLL.md b/archive/docs/BAK-01-PRUEFPROTOKOLL.md index de5e8a2..6810ab9 100644 --- a/archive/docs/BAK-01-PRUEFPROTOKOLL.md +++ b/archive/docs/BAK-01-PRUEFPROTOKOLL.md @@ -70,6 +70,15 @@ golangci-lint run ./... -> 0 issues go test ./... -p 1 -count=1 -> 4/4 Tests ok, 0 Fehlschläge (echter Postgres 17, kein Mock) ``` +## Nachtrag (BAK-02-Sitzung): Backup-Zielverzeichnis korrigiert + +`NEXARCH_BACKUP_DIR` zeigte ursprünglich auf `/var/backups/nexarch` +(Root-Dateisystem des Containers, kein dediziertes Dataset) — korrigiert auf +`/var/nexarch-archiv/backups/postgres` (persistentes ZFS-Dataset), siehe +`docs/BAK-02-PRUEFPROTOKOLL.md` Abschnitt „Korrektur an BAK-01" für Details. +Vollsicherung nach der Korrektur erneut über systemd ausgelöst, landet +nachweislich am neuen Ort. + ## Gesamtergebnis **Bestanden.** Alle drei Akzeptanzkriterien und alle drei Pflichtprüfungen diff --git a/archive/docs/BAK-02-PRUEFPROTOKOLL.md b/archive/docs/BAK-02-PRUEFPROTOKOLL.md new file mode 100644 index 0000000..b23056c --- /dev/null +++ b/archive/docs/BAK-02-PRUEFPROTOKOLL.md @@ -0,0 +1,93 @@ +# BAK-02 – Prüfprotokoll: Objekt-Storage-Backup/Snapshots + +Welle 1, keine Vorbedingungen. + +## Grundsatzentscheidung: restic statt Eigenbau + +Nutzerentscheidung: restic statt einer Neuimplementierung, weil restic alle +vier Akzeptanzkriterien mit ausgereiftem, breit geprüftem Tooling erfüllt +(Content-defined Chunking für Dedup, `check --read-data` für +Vollständigkeit, `forget --keep-last` für Rotation, Repository-Verschlüsselung +ab Werk). Installiert via `apt-get install restic` (Version 0.18.0). + +Backup-Quelle ist ein lokaler Verzeichnisbaum — für den FDN-03-`LocalDriver` +direkt dessen Basisverzeichnis. Für S3-gestützte Produktions-Deployments +(Betriebsmodus 2/3 aus `STORAGE-KONZEPT.md` Abschnitt 6.2) wäre ein +vorgelagerter Sync-Schritt (z. B. `rclone`) nötig, um Bucket-Inhalte lokal +zu spiegeln, bevor restic sie sichert — restic sichert Dateibäume, keine +S3-Buckets direkt. Das bleibt hier bewusst unimplementiert (kein konkreter +S3-Produktionsbestand vorhanden, der das aktuell erfordert), aber +architektonisch vorgesehen und dokumentiert (`README.md`). + +## Umsetzung + +- `internal/objectbackup.InitRepo` — idempotent, erkennt "bereits + initialisiert" am `restic init`-Fehlertext statt zu scheitern. +- `internal/objectbackup.Backup` — `restic backup --json`, parst die + `summary`-Zeile (mehrere JSON-Zeilen in der Ausgabe, gezielt die mit + `message_type=="summary"` gesucht). +- `internal/objectbackup.Check` — `restic check [--read-data]` (Akzeptanz- + kriterium 3: Vollständigkeitsprüfung). +- `internal/objectbackup.Forget` — `restic forget --keep-last N --prune` + (Rotation). +- `cmd/objectbackup-cli` — `backup `/`check`/`rotate`, aufgerufen von + systemd-Timern (stündlich/wöchentlich/täglich). + +## Prüfungen + +| # | Prüfung | Ergebnis | +|---|---|---| +| 1 | Zweiter Sicherungslauf nach unverändertem Bestand überträgt keine Daten erneut | **bestanden** — `TestBackup_UnchangedSecondRunTransmitsNothingNew`: zweiter Lauf gegen unveränderten Bestand liefert `files_new=0`, `files_changed=0`, `files_unmodified=1` | +| 2 | Zwei identische Testdateien belegen nachweislich nur einmal Speicherplatz | **bestanden** — `TestBackup_DeduplicatesIdenticalContent`: zwei Dateien mit identischem Inhalt erzeugen `data_blobs=1`, nicht 2 — echter Dedup-Nachweis über restics Content-defined Chunking, nicht nur Namensvergleich | +| 3 | Vollständigkeitsprüfung erkennt ein fehlendes Objekt in der Sicherung | **bestanden** — `TestCheck_DetectsCorruptedPack`: ein Byte in einer echten Repository-Pack-Datei gekippt, `Check(readData=true)` schlägt danach fehl (unbeschädigt zuvor erfolgreich) — dieselbe Vorgehensweise wie die manuelle Recherche vor der Implementierung | + +Zusätzlich (nicht explizit als Pflichtprüfung gefordert, aber Teil von +Akzeptanzkriterium 3 „lässt sich einzeln prüfen"): `TestForget_ +KeepsOnlyRequestedSnapshotCount` — 3 Sicherungsläufe, `Forget(keepLast=1)` +reduziert auf genau 1 verbleibenden Snapshot. + +## Korrektur an BAK-01 im selben Rutsch: Backup-Zielverzeichnis + +Nutzerhinweis aufgegriffen: `NEXARCH_BACKUP_DIR` zeigte bei BAK-01 +ursprünglich auf `/var/backups/nexarch` (Root-Dateisystem des LXC- +Containers, nicht auf einem der beiden dedizierten ZFS-Datasets). Korrigiert +auf `/var/nexarch-archiv/backups/postgres` (persistentes Dataset +`zfs/data/subvol-1131-disk-0`), NICHT `/var/nexarch-test/` (ephemeres +Dataset `ssd-rpool-data/swap/subvol-1131-disk-0`, wird von +`reset-test-env.sh`-Skripten anderer Module geleert). `objectbackup-cli`s +Repository liegt von Anfang an korrekt unter +`/var/nexarch-archiv/backups/objects`. Beide Pfade real auf +192.168.1.131 verifiziert (`df`/`mount` bestätigt ZFS-Dataset-Zuordnung), +BAK-01s Vollsicherung nach der Korrektur erneut über systemd ausgelöst und +bestätigt am neuen Ort gelandet. + +ZFS-seitige Snapshot-/Replikations-Strategie für `nexarch/archiv` bleibt +bewusst außerhalb dieses Tickets (Infra-Runbook, siehe +`STORAGE-KONZEPT.md` Abschnitt 7 „Backup vs. Storage-Redundanz" sowie den +Hinweis, `zfs dedup=on` NICHT zu setzen — App-seitige Dedup über restic +genügt, ZFS-Dedup wäre auf dem 4-GB-Testhost ein Speicherrisiko). + +## Echte Verdrahtung auf 192.168.1.131 + +- `objectbackup-cli` gebaut nach `/opt/nexarch-archive/bin/` +- `/etc/nexarch/archive-objectbackup.env` (0600) +- 3 systemd-Timer installiert und aktiviert: Sicherung stündlich (`:30`), + Vollständigkeitsprüfung wöchentlich (So. 04:00 UTC), Rotation täglich + (03:30 UTC) — `systemctl list-timers` bestätigt alle scharf +- Jeder der drei Dienste einmal über `systemctl start` ausgelöst, alle mit + `status=0/SUCCESS`; Journal bestätigt inhaltlich korrekte Ausgabe + +## Build/Test-Ergebnis (192.168.1.131, `make check`) + +``` +go build ./... -> clean +go vet ./... -> clean +golangci-lint run ./... -> 0 issues +go test ./... -p 1 -count=1 -> 2/2 Pakete mit Tests ok (internal/backup, internal/objectbackup), 0 Fehlschläge +``` + +## Gesamtergebnis + +**Bestanden.** Alle drei Akzeptanzkriterien und alle drei Pflichtprüfungen +real gegen echtes restic-Tooling erfüllt. BAK-01-Pfadfehler im selben +Rutsch korrigiert und erneut end-to-end verifiziert. diff --git a/archive/internal/objectbackup/restic.go b/archive/internal/objectbackup/restic.go new file mode 100644 index 0000000..90b1552 --- /dev/null +++ b/archive/internal/objectbackup/restic.go @@ -0,0 +1,156 @@ +// Package objectbackup implementiert BAK-02: automatisierte, inkrementelle, +// deduplizierende Sicherung des Objekt-Storage-Bestands. Nutzt restic +// (Content-defined Chunking, verschlüsseltes Repository ab Werk) statt +// Eigenbau — restic erfüllt alle Akzeptanzkriterien mit ausgereiftem, +// geprüftem Tooling statt einer weniger robusten Neuimplementierung. +// +// Backup-Quelle ist ein lokaler Verzeichnisbaum — für den LocalDriver aus +// FDN-03 direkt dessen Basisverzeichnis, für S3-gestützte Produktions- +// Deployments ein vorgelagerter Sync-Schritt (z.B. rclone) auf einen +// lokalen Spiegel, bevor restic ihn sichert (nicht Bestandteil dieser +// Kachel — restic selbst sichert Dateibäume, keine S3-Buckets direkt). +package objectbackup + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" +) + +// Config enthält Repository-Ort und -Passwort — ausschließlich über +// Umgebungsvariablen befüllt (siehe Ticket-Abschluss-Regel). +type Config struct { + RepoDir string + Password string + ResticPath string // Default "restic", überschreibbar für Tests +} + +func (c Config) binary() string { + if c.ResticPath != "" { + return c.ResticPath + } + return "restic" +} + +func (c Config) env() []string { + return append(os.Environ(), "RESTIC_PASSWORD="+c.Password) +} + +func run(ctx context.Context, cfg Config, args ...string) ([]byte, error) { + fullArgs := append([]string{"-r", cfg.RepoDir}, args...) + cmd := exec.CommandContext(ctx, cfg.binary(), fullArgs...) + cmd.Env = cfg.env() + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%s %v fehlgeschlagen: %w (ausgabe: %s)", cfg.binary(), args, err, string(output)) + } + return output, nil +} + +// InitRepo legt ein neues restic-Repository an, falls es noch nicht +// existiert — idempotent, ein bereits initialisiertes Repository ist kein +// Fehler (Wiederholte Aufrufe durch systemd-Timer nach einem Neustart +// dürfen nicht fehlschlagen). +func InitRepo(ctx context.Context, cfg Config) error { + output, err := run(ctx, cfg, "init") + if err != nil { + if strings.Contains(string(output), "config file already exists") { + return nil + } + return fmt.Errorf("objectbackup: repository initialisieren: %w", err) + } + return nil +} + +// BackupSummary ist der geparste "summary"-Datensatz aus `restic backup --json`. +type BackupSummary struct { + SnapshotID string `json:"snapshot_id"` + FilesNew int `json:"files_new"` + FilesChanged int `json:"files_changed"` + FilesUnmodified int `json:"files_unmodified"` + DataBlobs int `json:"data_blobs"` + TotalBytes int64 `json:"total_bytes_processed"` +} + +// Backup sichert sourceDir inkrementell (Akzeptanzkriterium 1: unveränderte +// Objekte werden nicht erneut übertragen — restics Content-defined +// Chunking erkennt das automatisch, kein manueller Änderungsabgleich +// nötig). +func Backup(ctx context.Context, cfg Config, sourceDir string) (BackupSummary, error) { + output, err := run(ctx, cfg, "backup", sourceDir, "--json") + if err != nil { + return BackupSummary{}, fmt.Errorf("objectbackup: sicherung: %w", err) + } + return parseSummary(output) +} + +// parseSummary sucht in der zeilenweisen JSON-Ausgabe von `restic backup +// --json` (mehrere Fortschritts-/Statuszeilen, GENAU EINE mit +// message_type=="summary") die Zusammenfassung. +func parseSummary(output []byte) (BackupSummary, error) { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for i := len(lines) - 1; i >= 0; i-- { + var probe struct { + MessageType string `json:"message_type"` + } + if err := json.Unmarshal([]byte(lines[i]), &probe); err != nil { + continue + } + if probe.MessageType == "summary" { + var summary BackupSummary + if err := json.Unmarshal([]byte(lines[i]), &summary); err != nil { + return BackupSummary{}, fmt.Errorf("objectbackup: summary-zeile dekodieren: %w", err) + } + return summary, nil + } + } + return BackupSummary{}, fmt.Errorf("objectbackup: keine summary-zeile in der restic-ausgabe gefunden") +} + +// Check prüft die Vollständigkeit/Lesbarkeit des Repository +// (Akzeptanzkriterium 3 / Pflichtprüfung: Vollständigkeitsprüfung erkennt +// fehlendes/beschädigtes Objekt). readData=true liest jeden gespeicherten +// Datenblock tatsächlich (teurer, aber die einzige Prüfung, die +// Bit-Rot in bereits gespeicherten Paketen erkennt — ohne readData prüft +// restic nur Struktur/Indizes, nicht den tatsächlichen Blockinhalt). +func Check(ctx context.Context, cfg Config, readData bool) error { + args := []string{"check"} + if readData { + args = append(args, "--read-data") + } + if _, err := run(ctx, cfg, args...); err != nil { + return fmt.Errorf("objectbackup: %w", err) + } + return nil +} + +// Forget entfernt alte Snapshots nach Rotationsregel und gibt den davon +// belegten Speicherplatz frei (--prune) — restics Äquivalent zu +// BAK-01s Rotate. +func Forget(ctx context.Context, cfg Config, keepLast int) error { + if _, err := run(ctx, cfg, "forget", "--keep-last", fmt.Sprintf("%d", keepLast), "--prune"); err != nil { + return fmt.Errorf("objectbackup: rotation: %w", err) + } + return nil +} + +type snapshotEntry struct { + ShortID string `json:"short_id"` +} + +// SnapshotCount liefert die Anzahl vorhandener Snapshots — für Tests und +// Statusabfragen. +func SnapshotCount(ctx context.Context, cfg Config) (int, error) { + output, err := run(ctx, cfg, "snapshots", "--json") + if err != nil { + return 0, fmt.Errorf("objectbackup: snapshots auflisten: %w", err) + } + var snapshots []snapshotEntry + if err := json.Unmarshal(output, &snapshots); err != nil { + return 0, fmt.Errorf("objectbackup: snapshot-liste dekodieren: %w", err) + } + return len(snapshots), nil +} diff --git a/archive/internal/objectbackup/restic_test.go b/archive/internal/objectbackup/restic_test.go new file mode 100644 index 0000000..067c53a --- /dev/null +++ b/archive/internal/objectbackup/restic_test.go @@ -0,0 +1,168 @@ +package objectbackup + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func requireRestic(t *testing.T) { + t.Helper() + if _, err := exec.LookPath("restic"); err != nil { + t.Skip("restic nicht installiert, Integrationstest uebersprungen") + } +} + +func setupTest(t *testing.T) Config { + t.Helper() + requireRestic(t) + cfg := Config{RepoDir: filepath.Join(t.TempDir(), "repo"), Password: "test-passwort-fuer-objectbackup"} + if err := InitRepo(context.Background(), cfg); err != nil { + t.Fatalf("initrepo: %v", err) + } + return cfg +} + +func writeFile(t *testing.T, dir, name, content string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil { + t.Fatalf("testdatei %q schreiben: %v", name, err) + } +} + +// TestBackup_UnchangedSecondRunTransmitsNothingNew ist Pruefung 1: +// zweiter Sicherungslauf nach unveraendertem Bestand ueberraegt keine +// Daten erneut. +func TestBackup_UnchangedSecondRunTransmitsNothingNew(t *testing.T) { + cfg := setupTest(t) + ctx := context.Background() + sourceDir := t.TempDir() + writeFile(t, sourceDir, "dokument.pdf", "unveraenderter inhalt") + + first, err := Backup(ctx, cfg, sourceDir) + if err != nil { + t.Fatalf("erste sicherung: %v", err) + } + if first.FilesNew != 1 { + t.Fatalf("erste sicherung: files_new = %d, want 1", first.FilesNew) + } + + second, err := Backup(ctx, cfg, sourceDir) + if err != nil { + t.Fatalf("zweite sicherung: %v", err) + } + if second.FilesNew != 0 || second.FilesChanged != 0 { + t.Fatalf("zweite sicherung (unveraendert): files_new=%d files_changed=%d, want beide 0", second.FilesNew, second.FilesChanged) + } + if second.FilesUnmodified != 1 { + t.Fatalf("zweite sicherung: files_unmodified = %d, want 1", second.FilesUnmodified) + } +} + +// TestBackup_DeduplicatesIdenticalContent ist Pruefung 2: zwei identische +// Testdateien belegen nachweislich nur einmal Speicherplatz. +func TestBackup_DeduplicatesIdenticalContent(t *testing.T) { + cfg := setupTest(t) + ctx := context.Background() + sourceDir := t.TempDir() + content := "exakt identischer inhalt in beiden dateien fuer den dedup-nachweis" + writeFile(t, sourceDir, "original.pdf", content) + writeFile(t, sourceDir, "kopie.pdf", content) + + summary, err := Backup(ctx, cfg, sourceDir) + if err != nil { + t.Fatalf("sicherung: %v", err) + } + if summary.FilesNew != 2 { + t.Fatalf("erwartet 2 neue dateien, habe %d", summary.FilesNew) + } + // Zwei Dateien mit IDENTISCHEM Inhalt duerfen nur EINEN data_blob + // erzeugen - das ist der Dedup-Nachweis (Akzeptanzkriterium 2). + if summary.DataBlobs != 1 { + t.Fatalf("data_blobs = %d, want 1 (zwei identische dateien haetten nur einen blob erzeugen duerfen - keine dedup)", summary.DataBlobs) + } +} + +// TestCheck_DetectsCorruptedPack ist Pruefung 3: Vollstaendigkeitspruefung +// erkennt ein beschaedigtes/fehlendes Objekt in der Sicherung. +func TestCheck_DetectsCorruptedPack(t *testing.T) { + cfg := setupTest(t) + ctx := context.Background() + sourceDir := t.TempDir() + writeFile(t, sourceDir, "wichtig.pdf", "inhalt, der spaeter absichtlich beschaedigt wird") + + if _, err := Backup(ctx, cfg, sourceDir); err != nil { + t.Fatalf("sicherung: %v", err) + } + if err := Check(ctx, cfg, true); err != nil { + t.Fatalf("check (unbeschaedigt) haette erfolgreich sein muessen: %v", err) + } + + // Absichtliche Beschaedigung: ein Byte in einer Pack-Datei im + // Repository kippen (dieselbe Fundstelle wie beim manuellen + // Nachweis waehrend der Recherche zu diesem Ticket). + packDir := filepath.Join(cfg.RepoDir, "data") + corrupted := false + if err := filepath.Walk(packDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || corrupted { + return err + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + if len(data) < 20 { + return nil + } + data[10] ^= 0xFF + if err := os.WriteFile(path, data, 0o600); err != nil { + return err + } + corrupted = true + return nil + }); err != nil { + t.Fatalf("pack-datei beschaedigen: %v", err) + } + if !corrupted { + t.Fatal("keine pack-datei zum beschaedigen gefunden - testaufbau fehlerhaft") + } + + if err := Check(ctx, cfg, true); err == nil { + t.Fatal("check haette die beschaedigte pack-datei erkennen muessen") + } +} + +// TestForget_KeepsOnlyRequestedSnapshotCount prueft die Rotation. +func TestForget_KeepsOnlyRequestedSnapshotCount(t *testing.T) { + cfg := setupTest(t) + ctx := context.Background() + sourceDir := t.TempDir() + + for i := 0; i < 3; i++ { + writeFile(t, sourceDir, "f.txt", "version "+string(rune('a'+i))) + if _, err := Backup(ctx, cfg, sourceDir); err != nil { + t.Fatalf("sicherung %d: %v", i, err) + } + } + before, err := SnapshotCount(ctx, cfg) + if err != nil { + t.Fatalf("snapshotcount (vorher): %v", err) + } + if before != 3 { + t.Fatalf("erwartet 3 snapshots vor rotation, habe %d", before) + } + + if err := Forget(ctx, cfg, 1); err != nil { + t.Fatalf("forget: %v", err) + } + + after, err := SnapshotCount(ctx, cfg) + if err != nil { + t.Fatalf("snapshotcount (nachher): %v", err) + } + if after != 1 { + t.Fatalf("erwartet 1 snapshot nach rotation (keep-last 1), habe %d", after) + } +} diff --git a/deploy/systemd/nexarch-archive-objectbackup-backup.service.tmpl b/deploy/systemd/nexarch-archive-objectbackup-backup.service.tmpl new file mode 100644 index 0000000..d1bf102 --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-backup.service.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=NEXARCH Archive - Objekt-Storage-Sicherung (BAK-02) +After=network.target + +[Service] +Type=oneshot +User=nexarch +EnvironmentFile=/etc/nexarch/archive-objectbackup.env +ExecStart=__INSTALL_DIR__/bin/objectbackup-cli backup __OBJECT_SOURCE_DIR__ diff --git a/deploy/systemd/nexarch-archive-objectbackup-backup.timer.tmpl b/deploy/systemd/nexarch-archive-objectbackup-backup.timer.tmpl new file mode 100644 index 0000000..5e2dc90 --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-backup.timer.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=Stuendlicher Zeitplan fuer NEXARCH Archive Objekt-Storage-Sicherung (BAK-02) + +[Timer] +OnCalendar=*-*-* *:30:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/systemd/nexarch-archive-objectbackup-check.service.tmpl b/deploy/systemd/nexarch-archive-objectbackup-check.service.tmpl new file mode 100644 index 0000000..9499204 --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-check.service.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=NEXARCH Archive - Objekt-Storage-Sicherung Vollstaendigkeitspruefung (BAK-02) +After=network.target + +[Service] +Type=oneshot +User=nexarch +EnvironmentFile=/etc/nexarch/archive-objectbackup.env +ExecStart=__INSTALL_DIR__/bin/objectbackup-cli check diff --git a/deploy/systemd/nexarch-archive-objectbackup-check.timer.tmpl b/deploy/systemd/nexarch-archive-objectbackup-check.timer.tmpl new file mode 100644 index 0000000..118ea56 --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-check.timer.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=Woechentlicher Zeitplan fuer NEXARCH Archive Objekt-Storage-Vollstaendigkeitspruefung (BAK-02) + +[Timer] +OnCalendar=Sun *-*-* 04:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/systemd/nexarch-archive-objectbackup-rotate.service.tmpl b/deploy/systemd/nexarch-archive-objectbackup-rotate.service.tmpl new file mode 100644 index 0000000..73716dd --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-rotate.service.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=NEXARCH Archive - Objekt-Storage-Sicherung Rotation (BAK-02) +After=network.target + +[Service] +Type=oneshot +User=nexarch +EnvironmentFile=/etc/nexarch/archive-objectbackup.env +ExecStart=__INSTALL_DIR__/bin/objectbackup-cli rotate diff --git a/deploy/systemd/nexarch-archive-objectbackup-rotate.timer.tmpl b/deploy/systemd/nexarch-archive-objectbackup-rotate.timer.tmpl new file mode 100644 index 0000000..9a3e003 --- /dev/null +++ b/deploy/systemd/nexarch-archive-objectbackup-rotate.timer.tmpl @@ -0,0 +1,9 @@ +[Unit] +Description=Taeglicher Zeitplan fuer NEXARCH Archive Objekt-Storage-Rotation (BAK-02) + +[Timer] +OnCalendar=*-*-* 03:30:00 +Persistent=true + +[Install] +WantedBy=timers.target