#!/bin/bash # filepath: repmgr-split-brain-recovery.sh set -e NAMESPACE="freeleaps-prod" STATEFULSET="freeleaps-prod-gitea-postgresql-ha-postgresql" HEADLESS_SVC="${STATEFULSET}-headless.${NAMESPACE}.svc.freeleaps.cluster" REPMGR_USER="repmgr" REPMGR_PASSWORD="WGZ47gbUTLvo" POSTGRES_PASSWORD="X9H2*9M2ZWYmuZ" REPMGR_DB="repmgr" POSTGRES_USER="postgres" BACKUP_DIR="/tmp/pg_backup_$(date +%Y%m%d_%H%M%S)" LOCAL_BACKUP_DIR="./pg_backups_$(date +%Y%m%d_%H%M%S)" echo "===== PostgreSQL Repmgr Split-Brain Recovery =====" echo "This script will attempt to fix the repmgr split-brain issue" echo "" # Create local backup directory mkdir -p $LOCAL_BACKUP_DIR # Function to run commands in a pod run_in_pod() { local pod=$1 local cmd=$2 kubectl exec -n $NAMESPACE $pod -- bash -c "$cmd" } # Function to get PostgreSQL WAL position get_wal_position() { local pod=$1 run_in_pod $pod "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT pg_current_wal_lsn();\"" } # Function to check if node is primary is_primary() { local pod=$1 local result=$(run_in_pod $pod "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT pg_is_in_recovery();\"") if [[ $result == *"f"* ]]; then return 0 # is primary else return 1 # is standby fi } # Function to backup databases from a pod backup_databases() { local pod=$1 local backup_path="$BACKUP_DIR/$pod" echo "Creating backup directory in the pod..." run_in_pod $pod "mkdir -p $backup_path" echo "Getting list of databases..." local databases=$(run_in_pod $pod "PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -t -c \"SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'postgres')\" | tr -d ' '") echo "Backing up databases: $databases" for db in $databases; do echo "Backing up database: $db" run_in_pod $pod "PGPASSWORD=$POSTGRES_PASSWORD pg_dump -U $POSTGRES_USER -Fc $db > $backup_path/${db}.dump" done # Also backup global objects (roles, tablespaces) echo "Backing up global objects..." run_in_pod $pod "PGPASSWORD=$POSTGRES_PASSWORD pg_dumpall -U $POSTGRES_USER --globals-only > $backup_path/globals.sql" # Backup PostgreSQL configuration echo "Backing up PostgreSQL configuration..." run_in_pod $pod "cp /bitnami/postgresql/conf/postgresql.conf $backup_path/ 2>/dev/null || true" run_in_pod $pod "cp /bitnami/postgresql/conf/pg_hba.conf $backup_path/ 2>/dev/null || true" # Copy repmgr configuration echo "Backing up repmgr configuration..." run_in_pod $pod "cp /etc/repmgr.conf $backup_path/ 2>/dev/null || true" # Tar the backup files echo "Creating archive of the backup..." run_in_pod $pod "tar -czf ${backup_path}.tar.gz -C $(dirname $backup_path) $(basename $backup_path)" # Copy backup to local machine echo "Copying backup to local machine..." kubectl cp $NAMESPACE/$pod:${backup_path}.tar.gz $LOCAL_BACKUP_DIR/${pod}_backup.tar.gz # Cleanup backup in the pod echo "Cleaning up backup files in the pod..." run_in_pod $pod "rm -rf $backup_path ${backup_path}.tar.gz" } echo "Step 0: Checking current status of the cluster..." for i in 0 1 2; do POD="${STATEFULSET}-${i}" echo -n "Node ${i} ($POD): " # Check if node is running as primary if is_primary $POD; then PRIMARY_STATE="running as primary" echo "$PRIMARY_STATE" else echo "running as standby" fi # Get WAL position WAL_POS=$(get_wal_position $POD 2>/dev/null || echo "N/A") if [ "$WAL_POS" != "N/A" ]; then echo " - WAL position: $WAL_POS" # Store WAL positions for comparison declare "WAL_POS_${i}=$WAL_POS" fi done echo "" echo "Step 1: Backing up all databases from each node..." for i in 0 1 2; do POD="${STATEFULSET}-${i}" echo "Backing up data from node $i ($POD)..." backup_databases $POD done echo "All backups completed and stored in: $LOCAL_BACKUP_DIR" echo "" echo "Determining most advanced node based on WAL position..." # Get the primary nodes from each pod - there might be more than one in split-brain for i in 0 1 2; do POD="${STATEFULSET}-${i}" # Get node information NODE_INFO=$(run_in_pod $POD "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT node_id, node_name, type, active FROM repmgr.nodes WHERE node_name = '$POD';\"" 2>/dev/null || echo "") if [ -n "$NODE_INFO" ]; then echo "Node ${i} info: $NODE_INFO" # Store if this node thinks it's a primary if [[ $NODE_INFO == *"primary"* ]]; then echo "Node ${i} is configured as a primary" declare "NODE_${i}_IS_PRIMARY=true" else declare "NODE_${i}_IS_PRIMARY=false" fi # Check if node is actually running as primary using pg_is_in_recovery() if is_primary $POD; then echo "Node ${i} is running as primary (pg_is_in_recovery=false)" declare "NODE_${i}_RUNNING_AS_PRIMARY=true" else declare "NODE_${i}_RUNNING_AS_PRIMARY=false" fi else echo "Could not get info for node ${i}" declare "NODE_${i}_IS_PRIMARY=false" declare "NODE_${i}_RUNNING_AS_PRIMARY=false" fi done echo "" echo "Analyzing WAL positions to determine the most advanced node..." # Compare WAL positions if [ -n "${WAL_POS_0}" ] && [ -n "${WAL_POS_1}" ] && [ -n "${WAL_POS_2}" ]; then # We have all WAL positions, find the most advanced if run_in_pod ${STATEFULSET}-0 "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT '${WAL_POS_0}' > '${WAL_POS_1}' AND '${WAL_POS_0}' > '${WAL_POS_2}';\"" | grep -q 't'; then NEW_PRIMARY=0 elif run_in_pod ${STATEFULSET}-0 "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT '${WAL_POS_1}' > '${WAL_POS_2}';\"" | grep -q 't'; then NEW_PRIMARY=1 else NEW_PRIMARY=2 fi elif [ -n "${WAL_POS_0}" ] && [ -n "${WAL_POS_1}" ]; then # Only nodes 0 and 1 have WAL positions if run_in_pod ${STATEFULSET}-0 "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT '${WAL_POS_0}' > '${WAL_POS_1}';\"" | grep -q 't'; then NEW_PRIMARY=0 else NEW_PRIMARY=1 fi elif [ -n "${WAL_POS_0}" ] && [ -n "${WAL_POS_2}" ]; then # Only nodes 0 and 2 have WAL positions if run_in_pod ${STATEFULSET}-0 "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT '${WAL_POS_0}' > '${WAL_POS_2}';\"" | grep -q 't'; then NEW_PRIMARY=0 else NEW_PRIMARY=2 fi elif [ -n "${WAL_POS_1}" ] && [ -n "${WAL_POS_2}" ]; then # Only nodes 1 and 2 have WAL positions if run_in_pod ${STATEFULSET}-1 "PGPASSWORD=$REPMGR_PASSWORD psql -U $REPMGR_USER -d $REPMGR_DB -t -c \"SELECT '${WAL_POS_1}' > '${WAL_POS_2}';\"" | grep -q 't'; then NEW_PRIMARY=1 else NEW_PRIMARY=2 fi elif [ -n "${WAL_POS_0}" ]; then NEW_PRIMARY=0 elif [ -n "${WAL_POS_1}" ]; then NEW_PRIMARY=1 elif [ -n "${WAL_POS_2}" ]; then NEW_PRIMARY=2 else echo "Could not determine most advanced node. Using node 0 as default primary." NEW_PRIMARY=0 fi echo "Selected node ${NEW_PRIMARY} as the new primary based on WAL position." # Fix the bad substitution by using proper indirection eval WAL_POS_VALUE=\$WAL_POS_${NEW_PRIMARY} if [ -n "$WAL_POS_VALUE" ]; then echo "WAL position: $WAL_POS_VALUE" fi echo "" # Confirm with user read -p "Backups completed. Do you want to proceed with fixing the split-brain issue? (y/n): " CONFIRM if [[ "$CONFIRM" != "y" ]]; then echo "Operation cancelled. Backups are still available at $LOCAL_BACKUP_DIR" exit 1 fi echo "" echo "Step 2: Registering node ${NEW_PRIMARY} as primary..." PRIMARY_POD="${STATEFULSET}-${NEW_PRIMARY}" # Create a temporary script to run repmgr commands run_in_pod $PRIMARY_POD "cat > /tmp/register_primary.sh << EOF #!/bin/bash export PGUSER='$REPMGR_USER' export PGPASSWORD='$REPMGR_PASSWORD' export PGDATABASE='$REPMGR_DB' export PATH=\$PATH:/opt/bitnami/repmgr/bin:/opt/bitnami/postgresql/bin # Try to find repmgr repmgr_bin=\$(find /opt/bitnami -name repmgr -type f | head -1) if [ -z \"\$repmgr_bin\" ]; then echo \"Could not find repmgr binary\" exit 1 fi \$repmgr_bin -f /etc/repmgr.conf primary register --force EOF chmod +x /tmp/register_primary.sh" # Run the script directly run_in_pod $PRIMARY_POD "bash /tmp/register_primary.sh" # Stop PostgreSQL on other nodes for i in 0 1 2; do if [ $i -ne $NEW_PRIMARY ]; then STANDBY_POD="${STATEFULSET}-${i}" echo "Step 3: Stopping PostgreSQL on standby node ${i}..." run_in_pod $STANDBY_POD "/opt/bitnami/scripts/postgresql-repmgr/stop.sh" echo "Step 4: Cloning primary data to standby node ${i}..." # Create a temporary script for cloning the standby that doesn't rely on specific user run_in_pod $STANDBY_POD "cat > /tmp/clone_standby.sh << EOF #!/bin/bash export PGUSER='$REPMGR_USER' export PGPASSWORD='$REPMGR_PASSWORD' export PGDATABASE='$REPMGR_DB' export PATH=\$PATH:/opt/bitnami/repmgr/bin:/opt/bitnami/postgresql/bin # Remove existing data rm -rf /bitnami/postgresql/data/* # Try to find repmgr repmgr_bin=\$(find /opt/bitnami -name repmgr -type f | head -1) if [ -z \"\$repmgr_bin\" ]; then echo \"Could not find repmgr binary\" exit 1 fi \$repmgr_bin -h ${PRIMARY_POD}.${HEADLESS_SVC} -p 5432 standby clone --force EOF chmod +x /tmp/clone_standby.sh" # Run the clone script directly run_in_pod $STANDBY_POD "bash /tmp/clone_standby.sh" echo "Step 5: Starting PostgreSQL on standby node ${i}..." run_in_pod $STANDBY_POD "/opt/bitnami/scripts/postgresql-repmgr/start.sh" echo "Step 6: Registering node ${i} as standby..." # Create a temporary script for registering the standby run_in_pod $STANDBY_POD "cat > /tmp/register_standby.sh << EOF #!/bin/bash export PGUSER='$REPMGR_USER' export PGPASSWORD='$REPMGR_PASSWORD' export PGDATABASE='$REPMGR_DB' export PATH=\$PATH:/opt/bitnami/repmgr/bin:/opt/bitnami/postgresql/bin # Try to find repmgr repmgr_bin=\$(find /opt/bitnami -name repmgr -type f | head -1) if [ -z \"\$repmgr_bin\" ]; then echo \"Could not find repmgr binary\" exit 1 fi \$repmgr_bin -f /etc/repmgr.conf standby register --force EOF chmod +x /tmp/register_standby.sh" # Run the register script directly run_in_pod $STANDBY_POD "bash /tmp/register_standby.sh" fi done echo "" echo "Step 7: Checking final cluster status..." # Create a temporary script for checking cluster status run_in_pod $PRIMARY_POD "cat > /tmp/cluster_status.sh << EOF #!/bin/bash export PGUSER='$REPMGR_USER' export PGPASSWORD='$REPMGR_PASSWORD' export PGDATABASE='$REPMGR_DB' export PATH=\$PATH:/opt/bitnami/repmgr/bin:/opt/bitnami/postgresql/bin # Try to find repmgr repmgr_bin=\$(find /opt/bitnami -name repmgr -type f | head -1) if [ -z \"\$repmgr_bin\" ]; then echo \"Could not find repmgr binary\" exit 1 fi \$repmgr_bin -f /etc/repmgr.conf cluster show EOF chmod +x /tmp/cluster_status.sh" # Run the cluster status script directly FINAL_STATUS=$(run_in_pod $PRIMARY_POD "bash /tmp/cluster_status.sh") echo "$FINAL_STATUS" # Clean up temporary scripts for i in 0 1 2; do POD="${STATEFULSET}-${i}" run_in_pod $POD "rm -f /tmp/register_primary.sh /tmp/clone_standby.sh /tmp/register_standby.sh /tmp/cluster_status.sh" || true done echo "" echo "Split-brain recovery completed." echo "Your database backups are available at: $LOCAL_BACKUP_DIR" echo "Please verify that the cluster is now in a consistent state."