internal/tenantbackup: datenbank-scharfes pg_dump/pg_restore statt BAK-01s Cluster-weitem pg_basebackup - bei Modell C (TEN-01, physisch isolierte DB je Mandant) wuerde ein Cluster-Restore zwangslaeufig ALLE Mandanten ueberschreiben. Objekt-Seite nutzt BAK-02 direkt (Mandanten haben eigene Buckets/Pfad-Roots). Eigene Postgres-Rolle nexarch_tenantbackup (CREATEDB, kein Superuser, getrennt von nexarch_backup). Zwei-Tenant-Isolation real in beide Richtungen bewiesen (Markerwert-Nachweis), JSONL-Protokoll fuer Sicherung UND Restore. Realer End-zu-Ende-Lauf ueber tenantbackup-cli auf 131.
147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
// tenantbackup-cli ist der Aufrufpunkt fuer BAK-04: Sicherung/
|
|
// Wiederherstellung EINES einzelnen Mandanten (Datenbank UND
|
|
// Objekt-Storage-Root), unabhaengig vom Gesamt-Backup (BAK-01/BAK-02)
|
|
// planbar - eigener systemd-Timer, eigenes Log.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/objectbackup"
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/tenantbackup"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
logPath := os.Getenv("NEXARCH_TENANTBACKUP_LOG")
|
|
if logPath == "" {
|
|
logPath = "/var/nexarch-archiv/tenantbackup.log"
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "backup":
|
|
runBackup(logPath, os.Args[2:])
|
|
case "restore":
|
|
runRestore(logPath, os.Args[2:])
|
|
default:
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "usage: tenantbackup-cli backup <tenant-db> <objekt-storage-root>")
|
|
fmt.Fprintln(os.Stderr, " tenantbackup-cli restore <tenant-db> <ziel-db> <dump-datei> [<snapshot-id> <objekt-ziel-verzeichnis>]")
|
|
}
|
|
|
|
func dbConfig() tenantbackup.Config {
|
|
return tenantbackup.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_TENANTBACKUP_DIR"),
|
|
}
|
|
}
|
|
|
|
func objConfig(tenantDB string) objectbackup.Config {
|
|
return objectbackup.Config{
|
|
RepoDir: os.Getenv("NEXARCH_TENANTBACKUP_OBJECT_REPO_ROOT") + "/" + tenantDB,
|
|
Password: os.Getenv("NEXARCH_OBJECTBACKUP_PASSWORD"),
|
|
}
|
|
}
|
|
|
|
func runBackup(logPath string, args []string) {
|
|
fs := flag.NewFlagSet("backup", flag.ExitOnError)
|
|
if err := fs.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
if fs.NArg() != 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
tenantDB, storageRoot := fs.Arg(0), fs.Arg(1)
|
|
ctx := context.Background()
|
|
genID := time.Now().UTC().Format("20060102T150405Z")
|
|
|
|
dbCfg := dbConfig()
|
|
dumpPath, err := tenantbackup.Backup(ctx, dbCfg, tenantDB, genID)
|
|
logEntry(logPath, tenantbackup.OpBackupDB, tenantDB, "", dumpPath, err)
|
|
if err != nil {
|
|
log.Fatalf("datenbank-sicherung: %v", err)
|
|
}
|
|
if err := tenantbackup.Verify(ctx, dbCfg, dumpPath); err != nil {
|
|
log.Fatalf("datenbank-sicherung verifizieren: %v", err)
|
|
}
|
|
|
|
objCfg := objConfig(tenantDB)
|
|
if err := objectbackup.InitRepo(ctx, objCfg); err != nil {
|
|
log.Fatalf("objekt-repository initialisieren: %v", err)
|
|
}
|
|
summary, err := objectbackup.Backup(ctx, objCfg, storageRoot)
|
|
logEntry(logPath, tenantbackup.OpBackupObj, tenantDB, storageRoot, summary.SnapshotID, err)
|
|
if err != nil {
|
|
log.Fatalf("objekt-storage-sicherung: %v", err)
|
|
}
|
|
|
|
fmt.Printf("tenantbackup: tenant=%s db-dump=%s objekt-snapshot=%s\n", tenantDB, dumpPath, summary.SnapshotID)
|
|
}
|
|
|
|
func runRestore(logPath string, args []string) {
|
|
fs := flag.NewFlagSet("restore", flag.ExitOnError)
|
|
if err := fs.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
if fs.NArg() < 3 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
tenantDB, targetDB, dumpPath := fs.Arg(0), fs.Arg(1), fs.Arg(2)
|
|
ctx := context.Background()
|
|
|
|
dbCfg := dbConfig()
|
|
err := tenantbackup.CreateEmptyDatabase(ctx, dbCfg, targetDB)
|
|
if err == nil {
|
|
err = tenantbackup.Restore(ctx, dbCfg, dumpPath, targetDB)
|
|
}
|
|
logEntry(logPath, tenantbackup.OpRestoreDB, tenantDB, dumpPath, targetDB, err)
|
|
if err != nil {
|
|
log.Fatalf("datenbank-restore: %v", err)
|
|
}
|
|
fmt.Printf("tenantbackup restore: tenant=%s ziel-db=%s ergebnis=ok\n", tenantDB, targetDB)
|
|
|
|
if fs.NArg() < 5 {
|
|
return
|
|
}
|
|
snapshotID, objTarget := fs.Arg(3), fs.Arg(4)
|
|
objCfg := objConfig(tenantDB)
|
|
err = objectbackup.Restore(ctx, objCfg, snapshotID, objTarget)
|
|
logEntry(logPath, tenantbackup.OpRestoreObj, tenantDB, snapshotID, objTarget, err)
|
|
if err != nil {
|
|
log.Fatalf("objekt-storage-restore: %v", err)
|
|
}
|
|
fmt.Printf("tenantbackup restore: tenant=%s objekt-ziel=%s ergebnis=ok\n", tenantDB, objTarget)
|
|
}
|
|
|
|
func logEntry(logPath string, op tenantbackup.Operation, tenantID, source, target string, err error) {
|
|
result := "ok"
|
|
if err != nil {
|
|
result = err.Error()
|
|
}
|
|
entry := tenantbackup.LogEntry{
|
|
Timestamp: time.Now().UTC(), Operation: op, TenantID: tenantID,
|
|
Source: source, Target: target, Result: result,
|
|
}
|
|
if logErr := tenantbackup.AppendLog(logPath, entry); logErr != nil {
|
|
log.Fatalf("protokoll schreiben: %v", logErr)
|
|
}
|
|
}
|