internal/restore: Atomarer Restore ueber Temp-Verzeichnis + Rename, nicht-leeres Ziel ohne -force bricht VOR jeder Beruehrung ab, JSONL- Protokoll jedes Laufs. Drei reale Defekte beim Bau gefunden und behoben: pg_combinebackup braucht Plain- statt Tar-Format (Extraktionsschritt ergaenzt), pg_wal.tar.gz wurde nie verifiziert/wiederhergestellt (BAK-01s Verify jetzt erweitert), Go-exec haengt bei pg_ctl start wegen vererbter Pipes (Testfix: echte Logdatei statt CombinedOutput). Beide Restore-Pfade real auf 131 ueber restore-cli nachgewiesen, inkl. echtem Postgres-Start aus wiederhergestelltem Verzeichnis.
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
// restore-cli ist der Aufrufpunkt fuer BAK-03: dokumentiertes,
|
|
// wiederholbares Restore-Verfahren fuer Datenbank (BAK-01) und
|
|
// Objekt-Storage (BAK-02). Kein systemd-Timer (Notfall-/Einzelfall-
|
|
// Werkzeug, kein Zeitplan) - manueller Aufruf mit expliziter Bestaetigung
|
|
// bei nicht-leerem Ziel.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/backup"
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/objectbackup"
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/restore"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
logPath := os.Getenv("NEXARCH_RESTORE_LOG")
|
|
if logPath == "" {
|
|
logPath = "/var/nexarch-archiv/restore.log"
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "database":
|
|
runDatabase(logPath, os.Args[2:])
|
|
case "objects":
|
|
runObjects(logPath, os.Args[2:])
|
|
default:
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "usage: restore-cli database [-force] <generation-id> <ziel-verzeichnis>")
|
|
fmt.Fprintln(os.Stderr, " restore-cli objects [-force] <snapshot-id> <ziel-verzeichnis>")
|
|
fmt.Fprintln(os.Stderr, "(Flag -force MUSS vor den Positionsargumenten stehen, Standardverhalten des Go-flag-Pakets)")
|
|
}
|
|
|
|
func runDatabase(logPath string, args []string) {
|
|
fs := flag.NewFlagSet("database", flag.ExitOnError)
|
|
force := fs.Bool("force", false, "bestehenden, nicht-leeren Inhalt im Zielverzeichnis ersetzen")
|
|
if err := fs.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
if fs.NArg() != 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
generationID, target := fs.Arg(0), fs.Arg(1)
|
|
|
|
cfg := backup.Config{
|
|
Host: os.Getenv("NEXARCH_BACKUP_PG_HOST"),
|
|
Port: os.Getenv("NEXARCH_BACKUP_PG_PORT"),
|
|
User: os.Getenv("NEXARCH_BACKUP_PG_USER"),
|
|
Password: os.Getenv("NEXARCH_BACKUP_PG_PASSWORD"),
|
|
BackupDir: os.Getenv("NEXARCH_BACKUP_DIR"),
|
|
}
|
|
ctx := context.Background()
|
|
|
|
entry, err := restore.AtomicRestore(restore.KindDatabase, generationID, target, *force, logPath, func(tempDir string) error {
|
|
return backup.Restore(ctx, cfg, generationID, tempDir)
|
|
})
|
|
report(entry, err)
|
|
}
|
|
|
|
func runObjects(logPath string, args []string) {
|
|
fs := flag.NewFlagSet("objects", flag.ExitOnError)
|
|
force := fs.Bool("force", false, "bestehenden, nicht-leeren Inhalt im Zielverzeichnis ersetzen")
|
|
if err := fs.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
if fs.NArg() != 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
snapshotID, target := fs.Arg(0), fs.Arg(1)
|
|
|
|
cfg := objectbackup.Config{
|
|
RepoDir: os.Getenv("NEXARCH_OBJECTBACKUP_REPO_DIR"),
|
|
Password: os.Getenv("NEXARCH_OBJECTBACKUP_PASSWORD"),
|
|
}
|
|
ctx := context.Background()
|
|
|
|
entry, err := restore.AtomicRestore(restore.KindObjects, snapshotID, target, *force, logPath, func(tempDir string) error {
|
|
return objectbackup.Restore(ctx, cfg, snapshotID, tempDir)
|
|
})
|
|
report(entry, err)
|
|
}
|
|
|
|
func report(entry restore.LogEntry, err error) {
|
|
fmt.Printf("restore %s: quelle=%s ziel=%s ergebnis=%s\n", entry.Kind, entry.Source, entry.Target, entry.Result)
|
|
if err != nil {
|
|
log.Fatalf("restore fehlgeschlagen: %v", err)
|
|
}
|
|
}
|