Neues Modul-Verzeichnis code/archive/ (Monorepo-Muster wie code/dms/). PostgreSQL-17-natives inkrementelles Backup (pg_basebackup --incremental, WAL-Summarization) statt WAL-Archiving, um den geteilten Testhost ohne Neustart umzustellen (summarize_wal=on per pg_reload_conf). Rolle nexarch_backup mit REPLICATION-Attribut angelegt. internal/backup: FullBackup/IncrementalBackup (pg_basebackup-Wrapper), Verify (vollstaendiges Lesen von base.tar.gz, gzip+tar, nicht nur Header), Rotate/ListGenerations (generationsbasiert, aeltere zuerst entfernt). cmd/backup-cli fuer systemd-Timer-Aufruf (deploy/systemd/ nexarch-archive-backup-*.timer, taeglich/stuendlich/taeglich). Auf 192.168.1.131 verifiziert: 4/4 Tests gegen echte Postgres-17-Instanz (kein Mock) - inkrementelle Sicherung real kleiner als Vollsicherung, Verifikation erkennt absichtlich beschaedigte Datei, Rotation entfernt nur die aeltesten Generationen. Zusaetzlich ECHT verdrahtet: backup-cli gebaut, 3 systemd-Timer installiert+aktiviert, jeder der drei Dienste einmal ueber systemctl start end-to-end ausgeloest (status=0/SUCCESS je Dienst) - nicht nur go test, sondern der reale Automatisierungspfad selbst geprueft. Siehe archive/docs/BAK-01-PRUEFPROTOKOLL.md fuer alle Pruefungsergebnisse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
// Package backup implementiert BAK-01: automatisierte, inkrementelle
|
|
// Sicherung der PostgreSQL-Datenbank per pg_basebackup (PostgreSQL 17s
|
|
// natives inkrementelles Backup über WAL-Summarization, siehe
|
|
// `summarize_wal`), mit Verifikation jeder Sicherung und
|
|
// generationsbasierter Rotation. Kein pg_dump-basierter Ansatz, weil
|
|
// pg_dump ausschließlich logische Vollsicherungen kennt — "inkrementell"
|
|
// im Sinne des Tickets erfordert das physische, WAL-summary-gestützte
|
|
// Verfahren aus PostgreSQL 17.
|
|
package backup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Config enthält die Verbindungsdaten für pg_basebackup — ausschließlich
|
|
// über Umgebungsvariablen befüllt, nie im Code (siehe Ticket-Abschluss-
|
|
// Regel).
|
|
type Config struct {
|
|
Host string
|
|
Port string
|
|
User string
|
|
Password string
|
|
BackupDir string
|
|
PgBaseBackupPath string // Default "pg_basebackup", überschreibbar für Tests
|
|
}
|
|
|
|
func (c Config) binary() string {
|
|
if c.PgBaseBackupPath != "" {
|
|
return c.PgBaseBackupPath
|
|
}
|
|
return "pg_basebackup"
|
|
}
|
|
|
|
// FullBackupDirName/IncrementalDirName sind die festen Unterverzeichnis-
|
|
// namen je Generation.
|
|
const (
|
|
FullBackupDirName = "full"
|
|
IncrementalSubdir = "incremental"
|
|
BackupManifestFile = "backup_manifest"
|
|
BaseTarGzFile = "base.tar.gz"
|
|
)
|
|
|
|
// NewGenerationID liefert eine sortierbare, eindeutige Generation-Kennung
|
|
// (RFC3339-artig, dateisystemtauglich) — Generationen werden anhand dieser
|
|
// Kennung chronologisch sortiert (Rotate, ListGenerations).
|
|
func NewGenerationID(t time.Time) string {
|
|
return t.UTC().Format("20060102T150405Z")
|
|
}
|
|
|
|
// FullBackup erstellt eine neue Vollsicherung (Akzeptanzkriterium 1) als
|
|
// eigene Generation. Liefert den Pfad zum backup_manifest, das spätere
|
|
// IncrementalBackup-Aufrufe als Referenz brauchen.
|
|
func FullBackup(ctx context.Context, cfg Config, generationID string) (manifestPath string, err error) {
|
|
dir := filepath.Join(cfg.BackupDir, generationID, FullBackupDirName)
|
|
if err := os.MkdirAll(filepath.Dir(dir), 0o750); err != nil {
|
|
return "", fmt.Errorf("backup: generationsverzeichnis anlegen: %w", err)
|
|
}
|
|
|
|
args := []string{
|
|
"-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User,
|
|
"-D", dir, "-Ft", "-z", "--checkpoint=fast", "--no-password",
|
|
}
|
|
if err := runPgBaseBackup(ctx, cfg, args); err != nil {
|
|
return "", fmt.Errorf("backup: vollsicherung: %w", err)
|
|
}
|
|
return filepath.Join(dir, BackupManifestFile), nil
|
|
}
|
|
|
|
// IncrementalBackup erstellt eine inkrementelle Sicherung gegen die zuletzt
|
|
// bekannte Vollsicherung ODER die letzte Inkrement-Sicherung (priorManifestPath
|
|
// zeigt jeweils auf das backup_manifest der Referenz).
|
|
func IncrementalBackup(ctx context.Context, cfg Config, generationID, incrementID, priorManifestPath string) (manifestPath string, err error) {
|
|
dir := filepath.Join(cfg.BackupDir, generationID, IncrementalSubdir, incrementID)
|
|
if err := os.MkdirAll(filepath.Dir(dir), 0o750); err != nil {
|
|
return "", fmt.Errorf("backup: inkrement-verzeichnis anlegen: %w", err)
|
|
}
|
|
|
|
args := []string{
|
|
"-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User,
|
|
"-D", dir, "-Ft", "-z", "--checkpoint=fast", "--no-password",
|
|
"--incremental=" + priorManifestPath,
|
|
}
|
|
if err := runPgBaseBackup(ctx, cfg, args); err != nil {
|
|
return "", fmt.Errorf("backup: inkrementelle sicherung: %w", err)
|
|
}
|
|
return filepath.Join(dir, BackupManifestFile), nil
|
|
}
|
|
|
|
func runPgBaseBackup(ctx context.Context, cfg Config, args []string) error {
|
|
cmd := exec.CommandContext(ctx, cfg.binary(), args...)
|
|
cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%s fehlgeschlagen: %w (ausgabe: %s)", cfg.binary(), err, string(output))
|
|
}
|
|
return nil
|
|
}
|