From fb18caa3c267d1349f09c148574f1224b2ce630a Mon Sep 17 00:00:00 2001 From: Paul R Kartchner Date: Sun, 25 Jan 2026 21:39:32 -0700 Subject: [PATCH] feat: add comprehensive PostgreSQL backup and restore scripts Added production-grade backup and restore scripts for PostgreSQL servers that can backup all databases automatically with retention management. New scripts: - scripts/backup-all-postgres-databases.sh - Backs up all databases on a PostgreSQL server with automatic retention, compression, verification, and notification support - scripts/restore-postgres-database.sh - Restores individual databases with safety backups and verification - scripts/README-POSTGRES-BACKUP.md - Complete documentation with examples, best practices, and troubleshooting Features: - Automatic detection and backup of all user databases - Excludes system databases (postgres, template0, template1) - Backs up global objects (roles, tablespaces) - Optional gzip compression (80-90% space savings) - Automatic retention management (configurable days) - Integrity verification (gzip -t for compressed files) - Safety backups before restore operations - Detailed logging with color-coded output - Backup summary reporting - Email/Slack notification support (optional) - Interactive restore with confirmation prompts - Force mode for automation - Verbose debugging mode - Comprehensive error handling Backup directory structure: /var/backups/postgresql/YYYYMMDD/ - globals_YYYYMMDD_HHMMSS.sql.gz - database1_YYYYMMDD_HHMMSS.sql.gz - database2_YYYYMMDD_HHMMSS.sql.gz Usage examples: # Backup all databases with compression ./backup-all-postgres-databases.sh -c # Custom configuration ./backup-all-postgres-databases.sh -h db.server.com -U backup_user -d /mnt/backups -r 60 -c # Restore database ./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz # Force restore (skip confirmation) ./restore-postgres-database.sh backup.sql.gz -d mydb -f Automation: # Add to crontab for daily backups at 2 AM 0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1 Co-Authored-By: Claude Opus 4.5 --- scripts/README-POSTGRES-BACKUP.md | 458 +++++++++++++++++++++++ scripts/backup-all-postgres-databases.sh | 402 ++++++++++++++++++++ scripts/restore-postgres-database.sh | 396 ++++++++++++++++++++ 3 files changed, 1256 insertions(+) create mode 100644 scripts/README-POSTGRES-BACKUP.md create mode 100755 scripts/backup-all-postgres-databases.sh create mode 100755 scripts/restore-postgres-database.sh diff --git a/scripts/README-POSTGRES-BACKUP.md b/scripts/README-POSTGRES-BACKUP.md new file mode 100644 index 0000000..4a1b10a --- /dev/null +++ b/scripts/README-POSTGRES-BACKUP.md @@ -0,0 +1,458 @@ +# PostgreSQL Backup Scripts + +Comprehensive backup and restore scripts for PostgreSQL databases. + +## Scripts Overview + +### 1. `backup-all-postgres-databases.sh` +Backs up all databases on a PostgreSQL server (excluding system databases). + +**Features:** +- ✅ Backs up all user databases automatically +- ✅ Includes global objects (roles, tablespaces) +- ✅ Optional gzip compression +- ✅ Automatic retention management +- ✅ Integrity verification +- ✅ Detailed logging with color output +- ✅ Backup summary reporting +- ✅ Email/Slack notification support (optional) + +### 2. `restore-postgres-database.sh` +Restores a single database from backup. + +**Features:** +- ✅ Safety backup before restore +- ✅ Interactive confirmation +- ✅ Automatic database name detection +- ✅ Compressed file support +- ✅ Integrity verification +- ✅ Post-restore verification + +--- + +## Quick Start + +### Backup All Databases + +```bash +# Basic usage +./backup-all-postgres-databases.sh + +# With compression (recommended) +./backup-all-postgres-databases.sh -c + +# Custom configuration +./backup-all-postgres-databases.sh \ + -h db.example.com \ + -U postgres \ + -d /mnt/backups \ + -r 60 \ + -c +``` + +### Restore a Database + +```bash +# Interactive restore (with confirmation) +./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz + +# Force restore (skip confirmation) +./restore-postgres-database.sh backup.sql.gz -d mydb -f +``` + +--- + +## Detailed Usage + +### Backup Script Options + +```bash +./backup-all-postgres-databases.sh [options] + +Options: + -h HOST Database host (default: localhost) + -p PORT Database port (default: 5432) + -U USER Database user (default: postgres) + -d BACKUP_DIR Backup directory (default: /var/backups/postgresql) + -r DAYS Retention days (default: 30) + -c Enable compression (gzip) + -v Verbose output + -H Show help +``` + +### Restore Script Options + +```bash +./restore-postgres-database.sh [options] + +Options: + -h HOST Database host (default: localhost) + -p PORT Database port (default: 5432) + -U USER Database user (default: postgres) + -d DBNAME Target database name (default: from filename) + -f Force restore (skip confirmation) + -v Verbose output + -H Show help +``` + +--- + +## Automated Backups with Cron + +### Daily Backups (Recommended) + +```bash +# Edit crontab +sudo crontab -e + +# Add daily backup at 2 AM with compression +0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1 +``` + +### Alternative Schedules + +```bash +# Every 6 hours +0 */6 * * * /path/to/backup-all-postgres-databases.sh -c + +# Twice daily (2 AM and 2 PM) +0 2,14 * * * /path/to/backup-all-postgres-databases.sh -c + +# Weekly on Sundays at 3 AM +0 3 * * 0 /path/to/backup-all-postgres-databases.sh -c -r 90 +``` + +--- + +## Backup Directory Structure + +``` +/var/backups/postgresql/ +├── 20260120/ # Date-based subdirectory +│ ├── globals_20260120_020001.sql.gz # Global objects backup +│ ├── basil_20260120_020001.sql.gz # Database backup +│ ├── myapp_20260120_020001.sql.gz # Database backup +│ └── wiki_20260120_020001.sql.gz # Database backup +├── 20260121/ +│ ├── globals_20260121_020001.sql.gz +│ └── ... +└── 20260122/ + └── ... +``` + +--- + +## Configuration Examples + +### Local PostgreSQL Server + +```bash +./backup-all-postgres-databases.sh \ + -h localhost \ + -U postgres \ + -c +``` + +### Remote PostgreSQL Server + +```bash +./backup-all-postgres-databases.sh \ + -h db.example.com \ + -p 5432 \ + -U backup_user \ + -d /mnt/network/backups \ + -r 60 \ + -c \ + -v +``` + +### High-Frequency Backups + +```bash +# Short retention for frequent backups +./backup-all-postgres-databases.sh \ + -r 7 \ + -c +``` + +--- + +## Authentication Setup + +### Option 1: .pgpass File (Recommended) + +Create `~/.pgpass` with connection credentials: + +```bash +echo "localhost:5432:*:postgres:your-password" >> ~/.pgpass +chmod 600 ~/.pgpass +``` + +Format: `hostname:port:database:username:password` + +### Option 2: Environment Variables + +```bash +export PGPASSWORD="your-password" +./backup-all-postgres-databases.sh +``` + +### Option 3: Peer Authentication (Local Only) + +Run as the postgres system user: + +```bash +sudo -u postgres ./backup-all-postgres-databases.sh +``` + +--- + +## Monitoring and Notifications + +### Email Notifications + +Edit the scripts and uncomment the email notification section: + +```bash +# In backup-all-postgres-databases.sh, uncomment: +if command -v mail &> /dev/null; then + echo "$summary" | mail -s "PostgreSQL Backup $status - $(hostname)" admin@example.com +fi +``` + +### Slack Notifications + +Set webhook URL and uncomment: + +```bash +export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" + +# In script, uncomment: +if [ -n "$SLACK_WEBHOOK_URL" ]; then + curl -X POST "$SLACK_WEBHOOK_URL" \ + -H 'Content-Type: application/json' \ + -d "{\"text\":\"PostgreSQL Backup $status\n$summary\"}" +fi +``` + +### Log Rotation + +Create `/etc/logrotate.d/postgres-backup`: + +``` +/var/log/postgres-backup.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty +} +``` + +--- + +## Backup Verification + +### Manual Verification + +```bash +# List backups +ls -lh /var/backups/postgresql/$(date +%Y%m%d)/ + +# Verify compressed file integrity +gzip -t /var/backups/postgresql/20260120/basil_20260120_020001.sql.gz + +# Preview backup contents +gunzip -c backup.sql.gz | head -50 +``` + +### Test Restore (Recommended Monthly) + +```bash +# Restore to a test database +./restore-postgres-database.sh backup.sql.gz -d test_restore -f + +# Verify +psql -d test_restore -c "\dt" + +# Cleanup +dropdb test_restore +``` + +--- + +## Disaster Recovery + +### Full Server Restore + +1. **Install PostgreSQL** on new server +2. **Restore global objects first**: + ```bash + gunzip -c globals_YYYYMMDD_HHMMSS.sql.gz | psql -U postgres -d postgres + ``` +3. **Restore each database**: + ```bash + ./restore-postgres-database.sh basil_20260120_020001.sql.gz + ./restore-postgres-database.sh myapp_20260120_020001.sql.gz + ``` + +### Point-in-Time Recovery + +For PITR capabilities, enable WAL archiving in `postgresql.conf`: + +``` +wal_level = replica +archive_mode = on +archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f' +max_wal_senders = 3 +``` + +Then use `pg_basebackup` and WAL replay for PITR. + +--- + +## Troubleshooting + +### Permission Denied + +```bash +# Fix backup directory permissions +sudo chown -R postgres:postgres /var/backups/postgresql +sudo chmod 755 /var/backups/postgresql + +# Fix script permissions +chmod +x backup-all-postgres-databases.sh +``` + +### Connection Failed + +```bash +# Test connection manually +psql -h localhost -U postgres -c "SELECT version();" + +# Check pg_hba.conf +sudo cat /etc/postgresql/*/main/pg_hba.conf + +# Ensure proper authentication line exists: +# local all postgres peer +# host all all 127.0.0.1/32 scram-sha-256 +``` + +### Out of Disk Space + +```bash +# Check disk usage +df -h /var/backups + +# Clean old backups manually +find /var/backups/postgresql -type d -name "????????" -mtime +30 -exec rm -rf {} \; + +# Reduce retention period +./backup-all-postgres-databases.sh -r 7 +``` + +### Backup File Corrupted + +```bash +# Verify integrity +gzip -t backup.sql.gz + +# If corrupted, use previous backup +ls -lt /var/backups/postgresql/*/basil_*.sql.gz | head +``` + +--- + +## Performance Optimization + +### Large Databases + +For very large databases, consider: + +```bash +# Parallel dump (PostgreSQL 9.3+) +pg_dump -Fd -j 4 -f backup_directory mydb + +# Custom format (smaller, faster restore) +pg_dump -Fc mydb > backup.custom + +# Restore from custom format +pg_restore -d mydb backup.custom +``` + +### Network Backups + +```bash +# Direct SSH backup (no local storage) +pg_dump mydb | gzip | ssh backup-server "cat > /backups/mydb.sql.gz" +``` + +--- + +## Best Practices + +1. **Always test restores** - Backups are worthless if you can't restore +2. **Monitor backup completion** - Set up alerts for failed backups +3. **Use compression** - Saves 80-90% of disk space +4. **Multiple backup locations** - Local + remote/cloud storage +5. **Verify backup integrity** - Run gzip -t on compressed backups +6. **Document procedures** - Keep runbooks for disaster recovery +7. **Encrypt sensitive backups** - Use gpg for encryption if needed +8. **Regular retention review** - Adjust based on compliance requirements + +--- + +## Security Considerations + +### Encryption at Rest + +```bash +# Encrypt backup with GPG +pg_dump mydb | gzip | gpg --encrypt --recipient admin@example.com > backup.sql.gz.gpg + +# Decrypt for restore +gpg --decrypt backup.sql.gz.gpg | gunzip | psql mydb +``` + +### Secure Transfer + +```bash +# Use SCP with key authentication +scp -i ~/.ssh/backup_key backup.sql.gz backup-server:/secure/backups/ + +# Or rsync over SSH +rsync -av -e "ssh -i ~/.ssh/backup_key" \ + /var/backups/postgresql/ \ + backup-server:/secure/backups/ +``` + +### Access Control + +```bash +# Restrict backup directory permissions +chmod 700 /var/backups/postgresql +chown postgres:postgres /var/backups/postgresql + +# Restrict script permissions +chmod 750 backup-all-postgres-databases.sh +chown root:postgres backup-all-postgres-databases.sh +``` + +--- + +## Additional Resources + +- [PostgreSQL Backup Documentation](https://www.postgresql.org/docs/current/backup.html) +- [pg_dump Manual](https://www.postgresql.org/docs/current/app-pgdump.html) +- [pg_restore Manual](https://www.postgresql.org/docs/current/app-pgrestore.html) +- [Continuous Archiving and PITR](https://www.postgresql.org/docs/current/continuous-archiving.html) + +--- + +## Support + +For issues or questions: +- Check script help: `./backup-all-postgres-databases.sh -H` +- Review logs: `tail -f /var/log/postgres-backup.log` +- Test connection: `psql -h localhost -U postgres` diff --git a/scripts/backup-all-postgres-databases.sh b/scripts/backup-all-postgres-databases.sh new file mode 100755 index 0000000..afb53ef --- /dev/null +++ b/scripts/backup-all-postgres-databases.sh @@ -0,0 +1,402 @@ +#!/bin/bash +# +# PostgreSQL All Databases Backup Script +# Backs up all databases on a PostgreSQL server using pg_dump +# +# Usage: +# ./backup-all-postgres-databases.sh [options] +# +# Options: +# -h HOST Database host (default: localhost) +# -p PORT Database port (default: 5432) +# -U USER Database user (default: postgres) +# -d BACKUP_DIR Backup directory (default: /var/backups/postgresql) +# -r DAYS Retention days (default: 30) +# -c Enable compression (gzip) +# -v Verbose output +# +# Cron example (daily at 2 AM): +# 0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1 + +set -e +set -o pipefail + +# Default configuration +DB_HOST="localhost" +DB_PORT="5432" +DB_USER="postgres" +BACKUP_DIR="/var/backups/postgresql" +RETENTION_DAYS=30 +COMPRESS=false +VERBOSE=false + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Parse command line arguments +while getopts "h:p:U:d:r:cvH" opt; do + case $opt in + h) DB_HOST="$OPTARG" ;; + p) DB_PORT="$OPTARG" ;; + U) DB_USER="$OPTARG" ;; + d) BACKUP_DIR="$OPTARG" ;; + r) RETENTION_DAYS="$OPTARG" ;; + c) COMPRESS=true ;; + v) VERBOSE=true ;; + H) + echo "PostgreSQL All Databases Backup Script" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h HOST Database host (default: localhost)" + echo " -p PORT Database port (default: 5432)" + echo " -U USER Database user (default: postgres)" + echo " -d BACKUP_DIR Backup directory (default: /var/backups/postgresql)" + echo " -r DAYS Retention days (default: 30)" + echo " -c Enable compression (gzip)" + echo " -v Verbose output" + echo " -H Show this help" + echo "" + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2 +} + +log_debug() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" + fi +} + +# Check dependencies +check_dependencies() { + log_debug "Checking dependencies..." + + if ! command -v psql &> /dev/null; then + log_error "psql not found. Please install PostgreSQL client tools." + exit 1 + fi + + if ! command -v pg_dump &> /dev/null; then + log_error "pg_dump not found. Please install PostgreSQL client tools." + exit 1 + fi + + if [ "$COMPRESS" = true ] && ! command -v gzip &> /dev/null; then + log_error "gzip not found. Please install gzip or disable compression." + exit 1 + fi + + log_debug "All dependencies satisfied" +} + +# Test database connection +test_connection() { + log_debug "Testing database connection..." + + if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT version();" &> /dev/null; then + log_error "Cannot connect to PostgreSQL server at $DB_HOST:$DB_PORT" + log_error "Check credentials, network connectivity, and pg_hba.conf settings" + exit 1 + fi + + log_debug "Database connection successful" +} + +# Create backup directory structure +create_backup_dirs() { + local timestamp=$(date +%Y%m%d) + local backup_subdir="$BACKUP_DIR/$timestamp" + + log_debug "Creating backup directory: $backup_subdir" + + mkdir -p "$backup_subdir" + + if [ ! -w "$backup_subdir" ]; then + log_error "Backup directory is not writable: $backup_subdir" + exit 1 + fi + + echo "$backup_subdir" +} + +# Get list of databases to backup +get_databases() { + log_debug "Retrieving database list..." + + # Get all databases except system databases + local databases=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c \ + "SELECT datname FROM pg_database + WHERE datname NOT IN ('postgres', 'template0', 'template1') + AND datistemplate = false + ORDER BY datname;") + + if [ -z "$databases" ]; then + log_warn "No user databases found to backup" + return 1 + fi + + echo "$databases" +} + +# Backup a single database +backup_database() { + local db_name="$1" + local backup_dir="$2" + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_file="$backup_dir/${db_name}_${timestamp}.sql" + + log_info "Backing up database: $db_name" + + # Add compression extension if enabled + if [ "$COMPRESS" = true ]; then + backup_file="${backup_file}.gz" + fi + + # Perform backup + local start_time=$(date +%s) + + if [ "$COMPRESS" = true ]; then + if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" \ + --no-owner --no-privileges --create --clean | gzip > "$backup_file"; then + local status="SUCCESS" + else + local status="FAILED" + fi + else + if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" \ + --no-owner --no-privileges --create --clean > "$backup_file"; then + local status="SUCCESS" + else + local status="FAILED" + fi + fi + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + if [ "$status" = "SUCCESS" ]; then + # Verify backup file exists and has content + if [ ! -s "$backup_file" ]; then + log_error "Backup file is empty: $backup_file" + return 1 + fi + + # Verify compressed file integrity if compression is enabled + if [ "$COMPRESS" = true ]; then + if ! gzip -t "$backup_file" 2>/dev/null; then + log_error "Backup file is corrupted: $backup_file" + return 1 + fi + fi + + local file_size=$(du -h "$backup_file" | cut -f1) + log_info "✓ $db_name backup completed - Size: $file_size, Duration: ${duration}s" + log_debug " File: $backup_file" + return 0 + else + log_error "✗ $db_name backup failed" + # Remove failed backup file + rm -f "$backup_file" + return 1 + fi +} + +# Backup global objects (roles, tablespaces, etc.) +backup_globals() { + local backup_dir="$1" + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_file="$backup_dir/globals_${timestamp}.sql" + + log_info "Backing up global objects (roles, tablespaces)..." + + if [ "$COMPRESS" = true ]; then + backup_file="${backup_file}.gz" + fi + + if [ "$COMPRESS" = true ]; then + if pg_dumpall -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" --globals-only | gzip > "$backup_file"; then + local status="SUCCESS" + else + local status="FAILED" + fi + else + if pg_dumpall -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" --globals-only > "$backup_file"; then + local status="SUCCESS" + else + local status="FAILED" + fi + fi + + if [ "$status" = "SUCCESS" ]; then + local file_size=$(du -h "$backup_file" | cut -f1) + log_info "✓ Global objects backup completed - Size: $file_size" + return 0 + else + log_error "✗ Global objects backup failed" + rm -f "$backup_file" + return 1 + fi +} + +# Clean up old backups +cleanup_old_backups() { + log_info "Cleaning up backups older than $RETENTION_DAYS days..." + + local deleted_count=0 + + # Find and delete old backup directories + while IFS= read -r old_dir; do + log_debug "Deleting old backup directory: $old_dir" + rm -rf "$old_dir" + ((deleted_count++)) + done < <(find "$BACKUP_DIR" -maxdepth 1 -type d -name "????????" -mtime +$RETENTION_DAYS 2>/dev/null) + + if [ $deleted_count -gt 0 ]; then + log_info "Deleted $deleted_count old backup directories" + else + log_debug "No old backups to delete" + fi +} + +# Generate backup summary +generate_summary() { + local backup_dir="$1" + local total_dbs="$2" + local successful_dbs="$3" + local failed_dbs="$4" + local total_size=$(du -sh "$backup_dir" 2>/dev/null | cut -f1) + + echo "" + log_info "================================================" + log_info "Backup Summary" + log_info "================================================" + log_info "Backup Directory: $backup_dir" + log_info "Total Databases: $total_dbs" + log_info "Successful: $successful_dbs" + log_info "Failed: $failed_dbs" + log_info "Total Size: $total_size" + log_info "Retention: $RETENTION_DAYS days" + log_info "Compression: $([ "$COMPRESS" = true ] && echo "Enabled" || echo "Disabled")" + log_info "================================================" + echo "" +} + +# Send notification (optional) +send_notification() { + local status="$1" + local summary="$2" + + # Uncomment and configure to enable email notifications + # if command -v mail &> /dev/null; then + # echo "$summary" | mail -s "PostgreSQL Backup $status - $(hostname)" your-email@example.com + # fi + + # Uncomment and configure to enable Slack notifications + # if [ -n "$SLACK_WEBHOOK_URL" ]; then + # curl -X POST "$SLACK_WEBHOOK_URL" \ + # -H 'Content-Type: application/json' \ + # -d "{\"text\":\"PostgreSQL Backup $status\n$summary\"}" + # fi +} + +# Main execution +main() { + local start_time=$(date +%s) + + log_info "================================================" + log_info "PostgreSQL All Databases Backup Script" + log_info "================================================" + log_info "Host: $DB_HOST:$DB_PORT" + log_info "User: $DB_USER" + log_info "Backup Directory: $BACKUP_DIR" + log_info "Compression: $([ "$COMPRESS" = true ] && echo "Enabled" || echo "Disabled")" + log_info "Retention: $RETENTION_DAYS days" + log_info "================================================" + echo "" + + # Perform checks + check_dependencies + test_connection + + # Create backup directory + local backup_subdir=$(create_backup_dirs) + + # Get list of databases + local databases=$(get_databases) + + if [ -z "$databases" ]; then + log_warn "No databases to backup. Exiting." + exit 0 + fi + + # Backup global objects first + backup_globals "$backup_subdir" + + # Backup each database + local total_dbs=0 + local successful_dbs=0 + local failed_dbs=0 + + while IFS= read -r db; do + # Trim whitespace + db=$(echo "$db" | xargs) + + if [ -n "$db" ]; then + ((total_dbs++)) + + if backup_database "$db" "$backup_subdir"; then + ((successful_dbs++)) + else + ((failed_dbs++)) + fi + fi + done <<< "$databases" + + # Cleanup old backups + cleanup_old_backups + + # Calculate total execution time + local end_time=$(date +%s) + local total_duration=$((end_time - start_time)) + + # Generate summary + generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs" + + log_info "Total execution time: ${total_duration}s" + + # Send notification + if [ $failed_dbs -gt 0 ]; then + send_notification "COMPLETED WITH ERRORS" "$(generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs")" + exit 1 + else + send_notification "SUCCESS" "$(generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs")" + log_info "All backups completed successfully! ✓" + exit 0 + fi +} + +# Run main function +main diff --git a/scripts/restore-postgres-database.sh b/scripts/restore-postgres-database.sh new file mode 100755 index 0000000..b61acf2 --- /dev/null +++ b/scripts/restore-postgres-database.sh @@ -0,0 +1,396 @@ +#!/bin/bash +# +# PostgreSQL Database Restore Script +# Restores a single database from backup created by backup-all-postgres-databases.sh +# +# Usage: +# ./restore-postgres-database.sh [options] +# +# Options: +# -h HOST Database host (default: localhost) +# -p PORT Database port (default: 5432) +# -U USER Database user (default: postgres) +# -d DBNAME Target database name (default: extracted from backup filename) +# -f Force restore (skip confirmation) +# -v Verbose output +# +# Examples: +# ./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz +# ./restore-postgres-database.sh backup.sql -d mydb -f + +set -e +set -o pipefail + +# Default configuration +DB_HOST="localhost" +DB_PORT="5432" +DB_USER="postgres" +DB_NAME="" +FORCE=false +VERBOSE=false + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2 +} + +log_debug() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" + fi +} + +# Show usage +show_usage() { + echo "PostgreSQL Database Restore Script" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h HOST Database host (default: localhost)" + echo " -p PORT Database port (default: 5432)" + echo " -U USER Database user (default: postgres)" + echo " -d DBNAME Target database name (default: extracted from filename)" + echo " -f Force restore (skip confirmation)" + echo " -v Verbose output" + echo " -H Show this help" + echo "" + echo "Examples:" + echo " $0 /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz" + echo " $0 backup.sql -d mydb -f" + echo "" +} + +# Extract database name from backup filename +extract_db_name() { + local filename=$(basename "$1") + # Remove extension(s) and timestamp + # Format: dbname_YYYYMMDD_HHMMSS.sql[.gz] + echo "$filename" | sed -E 's/_[0-9]{8}_[0-9]{6}\.sql(\.gz)?$//' +} + +# Check if file is compressed +is_compressed() { + [[ "$1" == *.gz ]] +} + +# Verify backup file +verify_backup() { + local backup_file="$1" + + log_debug "Verifying backup file: $backup_file" + + if [ ! -f "$backup_file" ]; then + log_error "Backup file not found: $backup_file" + exit 1 + fi + + if [ ! -r "$backup_file" ]; then + log_error "Backup file is not readable: $backup_file" + exit 1 + fi + + if [ ! -s "$backup_file" ]; then + log_error "Backup file is empty: $backup_file" + exit 1 + fi + + # Verify compressed file integrity + if is_compressed "$backup_file"; then + log_debug "Verifying gzip integrity..." + if ! gzip -t "$backup_file" 2>/dev/null; then + log_error "Backup file is corrupted (gzip test failed)" + exit 1 + fi + fi + + log_debug "Backup file verification passed" +} + +# Test database connection +test_connection() { + log_debug "Testing database connection..." + + if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT version();" &> /dev/null; then + log_error "Cannot connect to PostgreSQL server at $DB_HOST:$DB_PORT" + log_error "Check credentials, network connectivity, and pg_hba.conf settings" + exit 1 + fi + + log_debug "Database connection successful" +} + +# Check if database exists +database_exists() { + local db_name="$1" + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c \ + "SELECT 1 FROM pg_database WHERE datname='$db_name';" | grep -q 1 +} + +# Create safety backup +create_safety_backup() { + local db_name="$1" + local timestamp=$(date +%Y%m%d_%H%M%S) + local safety_file="/tmp/${db_name}_pre-restore_${timestamp}.sql.gz" + + log_info "Creating safety backup before restore..." + + if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" | gzip > "$safety_file"; then + log_info "Safety backup created: $safety_file" + echo "$safety_file" + return 0 + else + log_error "Failed to create safety backup" + return 1 + fi +} + +# Drop and recreate database +recreate_database() { + local db_name="$1" + + log_info "Dropping and recreating database: $db_name" + + # Terminate existing connections + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres < pg_backend_pid(); +EOF + + # Drop and recreate + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres <