Compare commits

..

3 Commits

Author SHA1 Message Date
9d95a193db Fix: Enterprise cluster restore (postgres user via su)
All checks were successful
CI/CD / Test (push) Successful in 1m16s
CI/CD / Lint (push) Successful in 1m29s
CI/CD / Build & Release (push) Successful in 3m13s
Critical fixes for enterprise environments where dbbackup runs as
postgres user via 'su postgres' without sudo access:

1. canRestartPostgreSQL(): New function that detects if we can restart
   PostgreSQL. Returns false immediately if running as postgres user
   without sudo access, avoiding wasted time and potential hangs.

2. tryRestartPostgreSQL(): Now calls canRestartPostgreSQL() first to
   skip restart attempts in restricted environments.

3. Changed restart warning from ERROR to WARN level - it's expected
   behavior in enterprise environments, not an error.

4. Context cancellation check: Goroutines now check ctx.Err() before
   starting and properly count cancelled databases as failures.

5. Goroutine accounting: After wg.Wait(), verify all databases were
   accounted for (success + fail = total). Catches goroutine crashes
   or deadlocks.

6. Port argument fix: Always pass -p port to psql for localhost
   restores, fixing non-standard port configurations.

This should fix the issue where cluster restore showed success but
0 databases were actually restored when running on enterprise systems.
2026-01-16 14:17:04 +01:00
3201f0fb6a Fix: Critical bug - cluster restore showing success with 0 databases restored
All checks were successful
CI/CD / Test (push) Successful in 1m14s
CI/CD / Lint (push) Successful in 1m25s
CI/CD / Build & Release (push) Successful in 3m23s
CRITICAL FIXES:
- Add check for successCount == 0 to properly fail when no databases restored
- Fix tryRestartPostgreSQL to use non-interactive sudo (-n flag)
- Add 10-second timeout per restart attempt to prevent blocking
- Try pg_ctl directly for postgres user (no sudo needed)
- Set stdin to nil to prevent sudo from waiting for password input

This fixes the issue where cluster restore showed success but no databases
were actually restored due to sudo blocking on password prompts.
2026-01-16 14:03:02 +01:00
62ddc57fb7 Fix: Remove sudo usage from auth detection to avoid password prompts
All checks were successful
CI/CD / Test (push) Successful in 1m16s
CI/CD / Lint (push) Successful in 1m27s
CI/CD / Build & Release (push) Successful in 3m16s
- Remove sudo cat attempt for reading pg_hba.conf
- Prevents password prompts when running as postgres via 'su postgres'
- Auth detection now relies on connection attempts when file is unreadable
- Fixes issue where returning to menu after restore triggers sudo prompt
2026-01-16 13:52:41 +01:00
3 changed files with 120 additions and 41 deletions

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information
- **Version**: 3.42.34
- **Build Time**: 2026-01-16_08:42:47_UTC
- **Git Commit**: a85ad0c
- **Build Time**: 2026-01-16_13:03:20_UTC
- **Git Commit**: 3201f0f
## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output

View File

@@ -84,20 +84,14 @@ func findHbaFileViaPostgres() string {
// parsePgHbaConf parses pg_hba.conf and returns the authentication method
func parsePgHbaConf(path string, user string) AuthMethod {
// Try with sudo if we can't read directly
// Try to read the file directly - do NOT use sudo as it triggers password prompts
// If we can't read pg_hba.conf, we'll rely on connection attempts to determine auth
file, err := os.Open(path)
if err != nil {
// Try with sudo (with timeout)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "cat", path)
output, err := cmd.Output()
if err != nil {
// If we can't read the file, return unknown and let the connection determine auth
// This avoids sudo password prompts when running as postgres via su
return AuthUnknown
}
return parseHbaContent(string(output), user)
}
defer file.Close()
scanner := bufio.NewScanner(file)

View File

@@ -442,16 +442,18 @@ func (e *Engine) restorePostgreSQLSQL(ctx context.Context, archivePath, targetDB
var cmd []string
// For localhost, omit -h to use Unix socket (avoids Ident auth issues)
// But always include -p for port (in case of non-standard port)
hostArg := ""
portArg := fmt.Sprintf("-p %d", e.cfg.Port)
if e.cfg.Host != "localhost" && e.cfg.Host != "" {
hostArg = fmt.Sprintf("-h %s -p %d", e.cfg.Host, e.cfg.Port)
hostArg = fmt.Sprintf("-h %s", e.cfg.Host)
}
if compressed {
// Use ON_ERROR_STOP=1 to fail fast on first error (prevents millions of errors on truncated dumps)
psqlCmd := fmt.Sprintf("psql -U %s -d %s -v ON_ERROR_STOP=1", e.cfg.User, targetDB)
psqlCmd := fmt.Sprintf("psql %s -U %s -d %s -v ON_ERROR_STOP=1", portArg, e.cfg.User, targetDB)
if hostArg != "" {
psqlCmd = fmt.Sprintf("psql %s -U %s -d %s -v ON_ERROR_STOP=1", hostArg, e.cfg.User, targetDB)
psqlCmd = fmt.Sprintf("psql %s %s -U %s -d %s -v ON_ERROR_STOP=1", hostArg, portArg, e.cfg.User, targetDB)
}
// Set PGPASSWORD in the bash command for password-less auth
cmd = []string{
@@ -472,6 +474,7 @@ func (e *Engine) restorePostgreSQLSQL(ctx context.Context, archivePath, targetDB
} else {
cmd = []string{
"psql",
"-p", fmt.Sprintf("%d", e.cfg.Port),
"-U", e.cfg.User,
"-d", targetDB,
"-v", "ON_ERROR_STOP=1",
@@ -1084,6 +1087,16 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
}
}()
// Check for context cancellation before starting
if ctx.Err() != nil {
e.log.Warn("Context cancelled - skipping database restore", "file", filename)
atomic.AddInt32(&failCount, 1)
restoreErrorsMu.Lock()
restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%s: restore skipped (context cancelled)", strings.TrimSuffix(strings.TrimSuffix(filename, ".dump"), ".sql.gz")))
restoreErrorsMu.Unlock()
return
}
// Track timing for this database restore
dbRestoreStart := time.Now()
@@ -1201,6 +1214,35 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
successCountFinal := int(atomic.LoadInt32(&successCount))
failCountFinal := int(atomic.LoadInt32(&failCount))
// SANITY CHECK: Verify all databases were accounted for
// This catches any goroutine that exited without updating counters
accountedFor := successCountFinal + failCountFinal
if accountedFor != totalDBs {
missingCount := totalDBs - accountedFor
e.log.Error("INTERNAL ERROR: Some database restore goroutines did not report status",
"expected", totalDBs,
"success", successCountFinal,
"failed", failCountFinal,
"unaccounted", missingCount)
// Treat unaccounted databases as failures
failCountFinal += missingCount
restoreErrorsMu.Lock()
restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%d database(s) did not complete (possible goroutine crash or deadlock)", missingCount))
restoreErrorsMu.Unlock()
}
// CRITICAL: Check if no databases were restored at all
if successCountFinal == 0 {
e.progress.Fail(fmt.Sprintf("Cluster restore FAILED: 0 of %d databases restored", totalDBs))
operation.Fail("No databases were restored")
if failCountFinal > 0 && restoreErrors != nil {
return fmt.Errorf("cluster restore failed: all %d database(s) failed:\n%s", failCountFinal, restoreErrors.Error())
}
return fmt.Errorf("cluster restore failed: no databases were restored (0 of %d total). Check PostgreSQL logs for details", totalDBs)
}
if failCountFinal > 0 {
// Format multi-error with detailed output
restoreErrors.ErrorFormat = func(errs []error) string {
@@ -2038,56 +2080,100 @@ func (e *Engine) boostPostgreSQLSettings(ctx context.Context, lockBoostValue int
// Wait for PostgreSQL to be ready
time.Sleep(3 * time.Second)
} else {
// Cannot restart - warn user loudly
e.log.Error("=" + strings.Repeat("=", 70))
e.log.Error("WARNING: max_locks_per_transaction change requires PostgreSQL restart!")
e.log.Error("Current value: " + strconv.Itoa(original.MaxLocks) + ", needed: " + strconv.Itoa(lockBoostValue))
e.log.Error("Restore may fail with 'out of shared memory' error on BLOB-heavy databases.")
e.log.Error("")
e.log.Error("To fix manually:")
e.log.Error(" 1. sudo systemctl restart postgresql")
e.log.Error(" 2. Or: sudo -u postgres pg_ctl restart -D $PGDATA")
e.log.Error(" 3. Then re-run the restore")
e.log.Error("=" + strings.Repeat("=", 70))
// Continue anyway - might work for small restores
// Cannot restart - warn user but continue
// The setting is written to postgresql.auto.conf and will take effect on next restart
e.log.Warn("=" + strings.Repeat("=", 70))
e.log.Warn("NOTE: max_locks_per_transaction change requires PostgreSQL restart")
e.log.Warn("Current value: " + strconv.Itoa(original.MaxLocks) + ", target: " + strconv.Itoa(lockBoostValue))
e.log.Warn("")
e.log.Warn("The setting has been saved to postgresql.auto.conf and will take")
e.log.Warn("effect on the next PostgreSQL restart. If restore fails with")
e.log.Warn("'out of shared memory' errors, ask your DBA to restart PostgreSQL.")
e.log.Warn("")
e.log.Warn("Continuing with restore - this may succeed if your databases")
e.log.Warn("don't have many large objects (BLOBs).")
e.log.Warn("=" + strings.Repeat("=", 70))
// Continue anyway - might work for small restores or DBs without BLOBs
}
}
return original, nil
}
// canRestartPostgreSQL checks if we have the ability to restart PostgreSQL
// Returns false if running in a restricted environment (e.g., su postgres on enterprise systems)
func (e *Engine) canRestartPostgreSQL() bool {
// Check if we're running as postgres user - if so, we likely can't restart
// because PostgreSQL is managed by init/systemd, not directly by pg_ctl
currentUser := os.Getenv("USER")
if currentUser == "" {
currentUser = os.Getenv("LOGNAME")
}
// If we're the postgres user, check if we have sudo access
if currentUser == "postgres" {
// Try a quick sudo check - if this fails, we can't restart
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-n", "true")
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
e.log.Info("Running as postgres user without sudo access - cannot restart PostgreSQL",
"user", currentUser,
"hint", "Ask system administrator to restart PostgreSQL if needed")
return false
}
}
return true
}
// tryRestartPostgreSQL attempts to restart PostgreSQL using various methods
// Returns true if restart was successful
// IMPORTANT: Uses short timeouts and non-interactive sudo to avoid blocking on password prompts
// NOTE: This function will return false immediately if running as postgres without sudo
func (e *Engine) tryRestartPostgreSQL(ctx context.Context) bool {
// First check if we can even attempt a restart
if !e.canRestartPostgreSQL() {
e.log.Info("Skipping PostgreSQL restart attempt (no privileges)")
return false
}
e.progress.Update("Attempting PostgreSQL restart for lock settings...")
// Method 1: systemctl (most common on modern Linux)
cmd := exec.CommandContext(ctx, "sudo", "systemctl", "restart", "postgresql")
if err := cmd.Run(); err == nil {
// Use short timeout for each restart attempt (don't block on sudo password prompts)
runWithTimeout := func(args ...string) bool {
cmdCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(cmdCtx, args[0], args[1:]...)
// Set stdin to /dev/null to prevent sudo from waiting for password
cmd.Stdin = nil
return cmd.Run() == nil
}
// Method 1: systemctl (most common on modern Linux) - use sudo -n for non-interactive
if runWithTimeout("sudo", "-n", "systemctl", "restart", "postgresql") {
return true
}
// Method 2: systemctl with version suffix (e.g., postgresql-15)
for _, ver := range []string{"17", "16", "15", "14", "13", "12"} {
cmd = exec.CommandContext(ctx, "sudo", "systemctl", "restart", "postgresql-"+ver)
if err := cmd.Run(); err == nil {
if runWithTimeout("sudo", "-n", "systemctl", "restart", "postgresql-"+ver) {
return true
}
}
// Method 3: service command (older systems)
cmd = exec.CommandContext(ctx, "sudo", "service", "postgresql", "restart")
if err := cmd.Run(); err == nil {
if runWithTimeout("sudo", "-n", "service", "postgresql", "restart") {
return true
}
// Method 4: pg_ctl as postgres user
cmd = exec.CommandContext(ctx, "sudo", "-u", "postgres", "pg_ctl", "restart", "-D", "/var/lib/postgresql/data", "-m", "fast")
if err := cmd.Run(); err == nil {
// Method 4: pg_ctl as postgres user (if we ARE postgres user, no sudo needed)
if runWithTimeout("pg_ctl", "restart", "-D", "/var/lib/postgresql/data", "-m", "fast") {
return true
}
// Method 5: Try common PGDATA paths
// Method 5: Try common PGDATA paths with pg_ctl directly (for postgres user)
pgdataPaths := []string{
"/var/lib/pgsql/data",
"/var/lib/pgsql/17/data",
@@ -2098,8 +2184,7 @@ func (e *Engine) tryRestartPostgreSQL(ctx context.Context) bool {
"/var/lib/postgresql/15/main",
}
for _, pgdata := range pgdataPaths {
cmd = exec.CommandContext(ctx, "sudo", "-u", "postgres", "pg_ctl", "restart", "-D", pgdata, "-m", "fast")
if err := cmd.Run(); err == nil {
if runWithTimeout("pg_ctl", "restart", "-D", pgdata, "-m", "fast") {
return true
}
}