427 lines
17 KiB
Bash
Executable File
427 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Database Rollback Script
|
|
# Usage: ./rollback-database.sh [environment]
|
|
# Environments: Development, SandboxRemote, ProductionRemote, Oda
|
|
|
|
set -e # Exit on any error
|
|
|
|
ENVIRONMENT=${1:-"Development"} # Default to Development for safer initial testing
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_DIR_NAME="backups" # Just the directory name
|
|
LOGS_DIR_NAME="logs" # Just the directory name
|
|
|
|
# Get the directory where the script is located
|
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
|
|
# Create logs directory first (before LOG_FILE is used)
|
|
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
|
|
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
|
|
|
|
LOG_FILE="$SCRIPT_DIR/logs/rollback_${ENVIRONMENT}_${TIMESTAMP}.log"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Logging function
|
|
log() {
|
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
error() {
|
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | tee -a "$LOG_FILE"
|
|
exit 1
|
|
}
|
|
|
|
info() {
|
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
# --- Determine Base Paths ---
|
|
# Get the directory where the script is located
|
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
log "Script is located in: $SCRIPT_DIR"
|
|
|
|
# Define absolute paths for projects and common directories relative to the script
|
|
# Assuming the project structure is:
|
|
# your_repo/
|
|
# ├── scripts/rollback-database.sh
|
|
# └── src/
|
|
# ├── Managing.Api/
|
|
# └── Managing.Docker/
|
|
PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" # One level up from scripts/
|
|
SRC_DIR="$PROJECT_ROOT_DIR/src"
|
|
API_PROJECT_PATH="$SRC_DIR/Managing.Api"
|
|
DOCKER_DIR="$SRC_DIR/Managing.Docker" # Adjust if your docker-compose files are elsewhere
|
|
|
|
# Define absolute path for backup directory with environment subfolder
|
|
BACKUP_DIR="$SCRIPT_DIR/$BACKUP_DIR_NAME/$ENVIRONMENT"
|
|
|
|
# --- Pre-checks and Setup ---
|
|
info "Pre-flight checks..."
|
|
command -v dotnet >/dev/null 2>&1 || error ".NET SDK is not installed. Please install .NET SDK to run this script."
|
|
command -v docker >/dev/null 2>&1 || warn "Docker is not installed. This is fine if not running Development or Oda environment with Docker."
|
|
command -v psql >/dev/null 2>&1 || error "PostgreSQL CLI (psql) is required for database rollback. Please install PostgreSQL client tools."
|
|
command -v pg_restore >/dev/null 2>&1 || warn "pg_restore not available. Will use psql for SQL script restoration."
|
|
|
|
# Create backup directory (with environment subfolder) - for storing rollback logs
|
|
mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory: $BACKUP_DIR"
|
|
log "Backup directory created/verified: $BACKUP_DIR"
|
|
|
|
log "🔄 Starting database rollback for environment: $ENVIRONMENT"
|
|
|
|
# Validate environment
|
|
case $ENVIRONMENT in
|
|
"Development"|"SandboxRemote"|"ProductionRemote"|"Oda")
|
|
log "✅ Environment '$ENVIRONMENT' is valid"
|
|
;;
|
|
*)
|
|
error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, SandboxRemote, ProductionRemote, or Oda"
|
|
;;
|
|
esac
|
|
|
|
# Helper function to start PostgreSQL for Development (if still using Docker Compose)
|
|
start_postgres_if_needed() {
|
|
if [ "$ENVIRONMENT" = "Development" ] || [ "$ENVIRONMENT" = "Oda" ]; then # Assuming Oda also uses local Docker
|
|
log "🔍 Checking if PostgreSQL is running for $ENVIRONMENT..."
|
|
if ! docker ps --filter "name=postgres" --format "{{.Names}}" | grep -q "postgres"; then
|
|
log "🐳 Starting PostgreSQL container for $ENVIRONMENT from $DOCKER_DIR..."
|
|
# Execute docker-compose from the DOCKER_DIR
|
|
(cd "$DOCKER_DIR" && docker-compose -f docker-compose.yml -f docker-compose.local.yml up -d postgres) || error "Failed to start PostgreSQL container."
|
|
log "⏳ Waiting for PostgreSQL to be ready (15 seconds)..."
|
|
sleep 15
|
|
else
|
|
log "✅ PostgreSQL container is already running."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Helper function to extract connection details from appsettings
|
|
extract_connection_details() {
|
|
local appsettings_file="$API_PROJECT_PATH/appsettings.$ENVIRONMENT.json"
|
|
local default_appsettings="$API_PROJECT_PATH/appsettings.json"
|
|
|
|
# Try environment-specific file first, then default
|
|
if [ -f "$appsettings_file" ]; then
|
|
log "📋 Reading connection string from: appsettings.$ENVIRONMENT.json"
|
|
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
|
|
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$appsettings_file" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
|
|
if [ -z "$CONNECTION_STRING" ]; then
|
|
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$appsettings_file" | cut -d'"' -f4)
|
|
fi
|
|
elif [ -f "$default_appsettings" ]; then
|
|
log "📋 Reading connection string from: appsettings.json (default)"
|
|
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
|
|
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$default_appsettings" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
|
|
if [ -z "$CONNECTION_STRING" ]; then
|
|
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$default_appsettings" | cut -d'"' -f4)
|
|
fi
|
|
else
|
|
warn "⚠️ Could not find appsettings file for environment $ENVIRONMENT"
|
|
return 1
|
|
fi
|
|
|
|
if [ -z "$CONNECTION_STRING" ]; then
|
|
error "❌ Could not extract connection string from appsettings file"
|
|
return 1
|
|
fi
|
|
|
|
log "📋 Found connection string: $CONNECTION_STRING"
|
|
|
|
# Parse connection string
|
|
DB_HOST=$(echo "$CONNECTION_STRING" | grep -o 'Host=[^;]*' | cut -d'=' -f2)
|
|
DB_PORT=$(echo "$CONNECTION_STRING" | grep -o 'Port=[^;]*' | cut -d'=' -f2)
|
|
DB_NAME=$(echo "$CONNECTION_STRING" | grep -o 'Database=[^;]*' | cut -d'=' -f2)
|
|
DB_USER=$(echo "$CONNECTION_STRING" | grep -o 'Username=[^;]*' | cut -d'=' -f2)
|
|
DB_PASSWORD=$(echo "$CONNECTION_STRING" | grep -o 'Password=[^;]*' | cut -d'=' -f2)
|
|
|
|
# Set defaults if not found
|
|
DB_HOST=${DB_HOST:-"localhost"}
|
|
DB_PORT=${DB_PORT:-"5432"}
|
|
DB_NAME=${DB_NAME:-"postgres"}
|
|
DB_USER=${DB_USER:-"postgres"}
|
|
DB_PASSWORD=${DB_PASSWORD:-"postgres"}
|
|
|
|
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER)"
|
|
}
|
|
|
|
# Helper function to test PostgreSQL connectivity
|
|
test_postgres_connectivity() {
|
|
log "🔍 Testing PostgreSQL connectivity with psql..."
|
|
|
|
# Test basic connectivity
|
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT version();" >/dev/null 2>&1; then
|
|
log "✅ PostgreSQL connectivity test passed"
|
|
|
|
# Get database info
|
|
log "📊 Database Information:"
|
|
DB_INFO=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
|
|
SELECT
|
|
'Database: ' || current_database() || ' (Size: ' || pg_size_pretty(pg_database_size(current_database())) || ')',
|
|
'PostgreSQL Version: ' || version(),
|
|
'Connection: ' || inet_server_addr() || ':' || inet_server_port()
|
|
" 2>/dev/null | tr '\n' ' ')
|
|
log " $DB_INFO"
|
|
return 0
|
|
else
|
|
error "❌ PostgreSQL connectivity test failed"
|
|
error " Host: $DB_HOST, Port: $DB_PORT, Database: $DB_NAME, User: $DB_USER"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# --- Core Logic ---
|
|
# Set ASPNETCORE_ENVIRONMENT to load the correct appsettings
|
|
export ASPNETCORE_ENVIRONMENT="$ENVIRONMENT"
|
|
log "ASPNETCORE_ENVIRONMENT set to: $ASPNETCORE_ENVIRONMENT"
|
|
|
|
# If Development or Oda, start local PostgreSQL
|
|
start_postgres_if_needed
|
|
|
|
# Extract connection details from appsettings
|
|
extract_connection_details
|
|
|
|
# Step 1: Check Database Connection
|
|
log "🔧 Step 1: Checking database connection..."
|
|
|
|
# Test connectivity
|
|
test_postgres_connectivity
|
|
|
|
# Step 2: Find and list available backups
|
|
log "🔍 Step 2: Finding available backups..."
|
|
|
|
# Look for backup files in the environment-specific backup directory
|
|
BACKUP_FILES=$(ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_backup_*.sql 2>/dev/null || true)
|
|
|
|
if [ -z "$BACKUP_FILES" ]; then
|
|
error "❌ No backup files found for environment '$ENVIRONMENT'"
|
|
error " Expected backup files in: $BACKUP_DIR/managing_${ENVIRONMENT}_backup_*.sql"
|
|
error " Please ensure backups exist before attempting rollback."
|
|
error " You can create a backup using: ./apply-migrations.sh $ENVIRONMENT"
|
|
fi
|
|
|
|
# Get the last 5 backups (most recent first)
|
|
RECENT_BACKUPS=$(echo "$BACKUP_FILES" | head -5)
|
|
BACKUP_COUNT=$(echo "$RECENT_BACKUPS" | wc -l | tr -d ' ')
|
|
|
|
log "✅ Found $BACKUP_COUNT backup(s) for environment '$ENVIRONMENT'"
|
|
|
|
# Display available backups
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "📋 AVAILABLE BACKUPS FOR $ENVIRONMENT"
|
|
echo "=========================================="
|
|
echo "Last 5 backups (most recent first):"
|
|
echo ""
|
|
|
|
BACKUP_ARRAY=()
|
|
INDEX=1
|
|
|
|
echo "$RECENT_BACKUPS" | while read -r backup_file; do
|
|
if [ -f "$backup_file" ]; then
|
|
BACKUP_FILENAME=$(basename "$backup_file")
|
|
BACKUP_SIZE=$(ls -lh "$backup_file" | awk '{print $5}')
|
|
BACKUP_LINES=$(wc -l < "$backup_file")
|
|
BACKUP_TIMESTAMP=$(echo "$BACKUP_FILENAME" | sed "s/managing_${ENVIRONMENT}_backup_\(.*\)\.sql/\1/")
|
|
BACKUP_DATE=$(date -r "$backup_file" "+%Y-%m-%d %H:%M:%S")
|
|
|
|
echo "[$INDEX] $BACKUP_FILENAME"
|
|
echo " Date: $BACKUP_DATE"
|
|
echo " Size: $BACKUP_SIZE"
|
|
echo " Lines: $BACKUP_LINES"
|
|
echo ""
|
|
|
|
BACKUP_ARRAY+=("$backup_file")
|
|
((INDEX++))
|
|
fi
|
|
done
|
|
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
# Let user choose which backup to use
|
|
read -p "🔄 Enter the number of the backup to rollback to (1-$BACKUP_COUNT, or 'cancel' to abort): " user_choice
|
|
|
|
if [ "$user_choice" = "cancel" ]; then
|
|
log "❌ Rollback cancelled by user."
|
|
exit 0
|
|
fi
|
|
|
|
# Validate user choice
|
|
if ! [[ "$user_choice" =~ ^[0-9]+$ ]] || [ "$user_choice" -lt 1 ] || [ "$user_choice" -gt "$BACKUP_COUNT" ]; then
|
|
error "❌ Invalid choice '$user_choice'. Please enter a number between 1 and $BACKUP_COUNT, or 'cancel' to abort."
|
|
fi
|
|
|
|
# Get the selected backup file
|
|
SELECTED_BACKUP=$(echo "$RECENT_BACKUPS" | sed -n "${user_choice}p")
|
|
BACKUP_FILENAME=$(basename "$SELECTED_BACKUP")
|
|
BACKUP_TIMESTAMP=$(echo "$BACKUP_FILENAME" | sed "s/managing_${ENVIRONMENT}_backup_\(.*\)\.sql/\1/")
|
|
|
|
log "✅ Selected backup: $BACKUP_FILENAME"
|
|
log " Location: $SELECTED_BACKUP"
|
|
log " Timestamp: $BACKUP_TIMESTAMP"
|
|
|
|
# Get backup file info
|
|
if [ -f "$SELECTED_BACKUP" ]; then
|
|
BACKUP_SIZE=$(ls -lh "$SELECTED_BACKUP" | awk '{print $5}')
|
|
BACKUP_LINES=$(wc -l < "$SELECTED_BACKUP")
|
|
log "📄 Selected backup file details:"
|
|
log " Size: $BACKUP_SIZE"
|
|
log " Lines: $BACKUP_LINES"
|
|
else
|
|
error "❌ Selected backup file does not exist or is not readable: $SELECTED_BACKUP"
|
|
fi
|
|
|
|
# Step 3: Show backup preview and get user confirmation
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "🔄 DATABASE ROLLBACK CONFIRMATION"
|
|
echo "=========================================="
|
|
echo "Environment: $ENVIRONMENT"
|
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
|
echo "Selected Backup: $BACKUP_FILENAME"
|
|
echo "Backup Size: $BACKUP_SIZE"
|
|
echo "Backup Lines: $BACKUP_LINES"
|
|
echo ""
|
|
echo "⚠️ WARNING: This will DROP and RECREATE the database!"
|
|
echo " All current data will be lost and replaced with the backup."
|
|
echo " This action cannot be undone!"
|
|
echo ""
|
|
echo "📋 BACKUP PREVIEW (first 20 lines):"
|
|
echo "----------------------------------------"
|
|
head -20 "$SELECTED_BACKUP" | sed 's/^/ /'
|
|
if [ "$BACKUP_LINES" -gt 20 ]; then
|
|
echo " ... (showing first 20 lines of $BACKUP_LINES total)"
|
|
fi
|
|
echo "----------------------------------------"
|
|
echo ""
|
|
|
|
read -p "🔄 Are you sure you want to rollback to this backup? Type 'yes' to proceed: " user_confirmation
|
|
|
|
if [ "$user_confirmation" != "yes" ]; then
|
|
log "❌ Rollback cancelled by user."
|
|
exit 0
|
|
fi
|
|
|
|
log "✅ User confirmed rollback. Proceeding with database restoration..."
|
|
|
|
# Step 4: Create a final backup before rollback (safety measure)
|
|
log "📦 Step 4: Creating final backup before rollback..."
|
|
FINAL_BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_pre_rollback_backup_${TIMESTAMP}.sql"
|
|
|
|
log "Creating final backup of current state..."
|
|
if PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" --no-password --verbose --clean --if-exists --create --format=plain > "$FINAL_BACKUP_FILE" 2>/dev/null; then
|
|
log "✅ Pre-rollback backup created: $(basename "$FINAL_BACKUP_FILE")"
|
|
else
|
|
warn "⚠️ Failed to create pre-rollback backup. Proceeding anyway..."
|
|
fi
|
|
|
|
# Step 5: Perform the rollback
|
|
log "🔄 Step 5: Performing database rollback..."
|
|
|
|
# Terminate active connections to the database (except our own)
|
|
log "🔌 Terminating active connections to database '$DB_NAME'..."
|
|
TERMINATE_QUERY="
|
|
SELECT pg_terminate_backend(pid)
|
|
FROM pg_stat_activity
|
|
WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();
|
|
"
|
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "$TERMINATE_QUERY" >/dev/null 2>&1; then
|
|
log "✅ Active connections terminated"
|
|
else
|
|
warn "⚠️ Could not terminate active connections. This may cause issues."
|
|
fi
|
|
|
|
# Drop and recreate the database
|
|
log "💥 Dropping and recreating database '$DB_NAME'..."
|
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "DROP DATABASE IF EXISTS \"$DB_NAME\";" >/dev/null 2>&1; then
|
|
log "✅ Database '$DB_NAME' dropped successfully"
|
|
else
|
|
error "❌ Failed to drop database '$DB_NAME'"
|
|
fi
|
|
|
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then
|
|
log "✅ Database '$DB_NAME' created successfully"
|
|
else
|
|
error "❌ Failed to create database '$DB_NAME'"
|
|
fi
|
|
|
|
# Restore from backup
|
|
log "📥 Restoring database from backup..."
|
|
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$SELECTED_BACKUP" >/dev/null 2>&1; then
|
|
log "✅ Database successfully restored from backup"
|
|
else
|
|
ERROR_OUTPUT=$( (PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$SELECTED_BACKUP") 2>&1 || true )
|
|
error "❌ Failed to restore database from backup"
|
|
error " PSQL Output: $ERROR_OUTPUT"
|
|
error " Backup file: $SELECTED_BACKUP"
|
|
error " Pre-rollback backup available at: $(basename "$FINAL_BACKUP_FILE")"
|
|
fi
|
|
|
|
# Step 6: Verify rollback
|
|
log "🔍 Step 6: Verifying rollback..."
|
|
|
|
# Test connectivity after restore
|
|
if test_postgres_connectivity; then
|
|
log "✅ Database connectivity verified after rollback"
|
|
|
|
# Get basic database stats
|
|
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
|
|
log "📊 Post-rollback database stats:"
|
|
log " Tables: $TABLE_COUNT"
|
|
|
|
if [ "$TABLE_COUNT" -gt 0 ]; then
|
|
log " Sample tables:"
|
|
SAMPLE_TABLES=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
|
|
SELECT tablename FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
ORDER BY tablename
|
|
LIMIT 5;
|
|
" 2>/dev/null | sed 's/^/ /')
|
|
echo "$SAMPLE_TABLES"
|
|
fi
|
|
else
|
|
error "❌ Database connectivity test failed after rollback"
|
|
error " The rollback may have completed but the database is not accessible."
|
|
error " Pre-rollback backup available at: $(basename "$FINAL_BACKUP_FILE")"
|
|
fi
|
|
|
|
# --- Step 7: Cleanup old backups (keep only 5 rollbacks max) ---
|
|
log "🧹 Step 7: Cleaning up old rollback backups..."
|
|
|
|
# Keep only the last 5 pre-rollback backups for this environment
|
|
ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_pre_rollback_backup_*.sql 2>/dev/null | tail -n +6 | xargs -r rm -f || true
|
|
|
|
log "✅ Kept last 5 pre-rollback backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$ENVIRONMENT/"
|
|
|
|
# Success Summary
|
|
log "🎉 Database rollback completed successfully for environment: $ENVIRONMENT!"
|
|
log "📁 Restored from backup: $BACKUP_FILENAME"
|
|
if [ -f "$FINAL_BACKUP_FILE" ]; then
|
|
log "📁 Pre-rollback backup: $(basename "$FINAL_BACKUP_FILE")"
|
|
fi
|
|
log "📝 Full Log file: $LOG_FILE"
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "📋 ROLLBACK SUMMARY"
|
|
echo "=========================================="
|
|
echo "Environment: $ENVIRONMENT"
|
|
echo "Timestamp: $TIMESTAMP"
|
|
echo "Status: ✅ SUCCESS"
|
|
echo "Restored from: $BACKUP_FILENAME"
|
|
if [ -f "$FINAL_BACKUP_FILE" ]; then
|
|
echo "Pre-rollback backup: $(basename "$FINAL_BACKUP_FILE")"
|
|
fi
|
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
|
echo "Log: $LOG_FILE"
|
|
echo "=========================================="
|