feat(archive): BAK-04 Tenant-Backup & -Restore einzelner Mandant
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.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user