// 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 ") fmt.Fprintln(os.Stderr, " tenantbackup-cli restore [ ]") } 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) } }