dr_backup.sh

Download
bash 368 lines 9.6 KB
  1#!/usr/bin/env bash
  2#
  3# Disaster Recovery Backup Script
  4#
  5# This script performs automated backups of:
  6# - Full system files (configurable directories)
  7# - PostgreSQL databases
  8# - Configuration files
  9# - Incremental backups (optional)
 10#
 11# Features:
 12# - Compression (gzip) and encryption (gpg)
 13# - Remote transfer (rsync/scp)
 14# - Backup rotation
 15# - Email notifications
 16# - Comprehensive logging
 17
 18set -euo pipefail
 19
 20#------------------------------------------------------------------------------
 21# Configuration
 22#------------------------------------------------------------------------------
 23
 24readonly SCRIPT_NAME="$(basename "$0")"
 25readonly BACKUP_ROOT="/var/backups/dr"
 26readonly LOG_DIR="${BACKUP_ROOT}/logs"
 27readonly LOG_FILE="${LOG_DIR}/backup_$(date +%Y%m%d_%H%M%S).log"
 28
 29# Backup sources
 30readonly BACKUP_DIRS=(
 31    "/etc"
 32    "/var/www"
 33    "/opt/applications"
 34)
 35
 36# Database configuration
 37readonly DB_HOST="localhost"
 38readonly DB_PORT="5432"
 39readonly DB_USER="backup_user"
 40readonly DB_NAMES=(
 41    "production_db"
 42    "analytics_db"
 43)
 44
 45# Backup settings
 46readonly INCREMENTAL=false  # Set to true for incremental backups
 47readonly COMPRESS=true
 48readonly ENCRYPT=true
 49readonly GPG_RECIPIENT="admin@example.com"
 50
 51# Remote backup settings
 52readonly REMOTE_BACKUP=true
 53readonly REMOTE_HOST="backup-server.example.com"
 54readonly REMOTE_USER="backup"
 55readonly REMOTE_PATH="/backups/dr"
 56
 57# Retention settings
 58readonly KEEP_DAILY=7
 59readonly KEEP_WEEKLY=4
 60readonly KEEP_MONTHLY=6
 61
 62# Email notification
 63readonly EMAIL_NOTIFY=true
 64readonly EMAIL_TO="admin@example.com"
 65
 66#------------------------------------------------------------------------------
 67# Functions
 68#------------------------------------------------------------------------------
 69
 70usage() {
 71    cat <<EOF
 72Usage: $SCRIPT_NAME [OPTIONS]
 73
 74Automated disaster recovery backup script.
 75
 76OPTIONS:
 77    -h, --help              Show this help message
 78    -n, --no-remote         Skip remote backup transfer
 79    -i, --incremental       Perform incremental backup
 80    -d, --dry-run           Show what would be done without executing
 81
 82EXAMPLES:
 83    $SCRIPT_NAME                # Full backup with all features
 84    $SCRIPT_NAME -n             # Backup locally only
 85    $SCRIPT_NAME -i             # Incremental backup
 86
 87EOF
 88}
 89
 90log() {
 91    local level="$1"
 92    shift
 93    local message="$*"
 94    local timestamp
 95    timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
 96
 97    echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE"
 98}
 99
100die() {
101    log "ERROR" "$*"
102    send_notification "FAILURE" "$*"
103    exit 1
104}
105
106send_notification() {
107    if [[ "$EMAIL_NOTIFY" != "true" ]]; then
108        return 0
109    fi
110
111    local status="$1"
112    local message="$2"
113    local subject="DR Backup ${status}: $(hostname)"
114
115    if command -v mail &>/dev/null; then
116        echo "$message" | mail -s "$subject" "$EMAIL_TO"
117    else
118        log "WARN" "mail command not found, skipping email notification"
119    fi
120}
121
122setup_directories() {
123    log "INFO" "Setting up backup directories"
124
125    mkdir -p "$BACKUP_ROOT"/{full,incremental,database,config,logs}
126    mkdir -p "$LOG_DIR"
127
128    # Set secure permissions
129    chmod 700 "$BACKUP_ROOT"
130    chmod 600 "$LOG_FILE"
131}
132
133backup_filesystem() {
134    log "INFO" "Starting filesystem backup"
135
136    local timestamp
137    timestamp="$(date +%Y%m%d_%H%M%S)"
138    local backup_dir="${BACKUP_ROOT}/full/${timestamp}"
139    local archive_name="filesystem_${timestamp}.tar"
140
141    mkdir -p "$backup_dir"
142
143    for dir in "${BACKUP_DIRS[@]}"; do
144        if [[ ! -d "$dir" ]]; then
145            log "WARN" "Directory does not exist: $dir"
146            continue
147        fi
148
149        log "INFO" "Backing up: $dir"
150
151        local dir_name
152        dir_name="$(basename "$dir")"
153
154        if [[ "$INCREMENTAL" == "true" ]]; then
155            # Incremental backup using rsync
156            rsync -av --link-dest="${BACKUP_ROOT}/full/latest" \
157                "$dir/" "${backup_dir}/${dir_name}/" \
158                >> "$LOG_FILE" 2>&1 || log "WARN" "rsync failed for $dir"
159        else
160            # Full tar backup
161            tar -cpf "${backup_dir}/${archive_name}" \
162                -C "$(dirname "$dir")" "$(basename "$dir")" \
163                >> "$LOG_FILE" 2>&1 || log "WARN" "tar failed for $dir"
164        fi
165    done
166
167    # Compress if enabled
168    if [[ "$COMPRESS" == "true" ]]; then
169        log "INFO" "Compressing filesystem backup"
170        gzip -9 "${backup_dir}/${archive_name}" || log "WARN" "Compression failed"
171        archive_name="${archive_name}.gz"
172    fi
173
174    # Encrypt if enabled
175    if [[ "$ENCRYPT" == "true" ]]; then
176        log "INFO" "Encrypting filesystem backup"
177        gpg --encrypt --recipient "$GPG_RECIPIENT" \
178            "${backup_dir}/${archive_name}" \
179            && rm -f "${backup_dir}/${archive_name}" \
180            || log "WARN" "Encryption failed"
181    fi
182
183    # Create/update latest symlink
184    ln -sfn "$backup_dir" "${BACKUP_ROOT}/full/latest"
185
186    log "INFO" "Filesystem backup completed: $backup_dir"
187}
188
189backup_databases() {
190    log "INFO" "Starting database backup"
191
192    if ! command -v pg_dump &>/dev/null; then
193        log "WARN" "pg_dump not found, skipping database backup"
194        return 0
195    fi
196
197    local timestamp
198    timestamp="$(date +%Y%m%d_%H%M%S)"
199    local db_backup_dir="${BACKUP_ROOT}/database/${timestamp}"
200
201    mkdir -p "$db_backup_dir"
202
203    for db in "${DB_NAMES[@]}"; do
204        log "INFO" "Backing up database: $db"
205
206        local dump_file="${db_backup_dir}/${db}_${timestamp}.sql"
207
208        PGPASSWORD="${DB_PASSWORD:-}" pg_dump \
209            -h "$DB_HOST" \
210            -p "$DB_PORT" \
211            -U "$DB_USER" \
212            -F c \
213            -f "$dump_file" \
214            "$db" \
215            >> "$LOG_FILE" 2>&1 || log "WARN" "Database dump failed for $db"
216
217        # Compress
218        if [[ "$COMPRESS" == "true" ]]; then
219            gzip -9 "$dump_file" || log "WARN" "Compression failed for $db"
220        fi
221
222        # Encrypt
223        if [[ "$ENCRYPT" == "true" ]]; then
224            local encrypted_file="${dump_file}.gz.gpg"
225            gpg --encrypt --recipient "$GPG_RECIPIENT" "${dump_file}.gz" \
226                && rm -f "${dump_file}.gz" \
227                || log "WARN" "Encryption failed for $db"
228        fi
229    done
230
231    log "INFO" "Database backup completed: $db_backup_dir"
232}
233
234backup_config() {
235    log "INFO" "Backing up configuration files"
236
237    local timestamp
238    timestamp="$(date +%Y%m%d_%H%M%S)"
239    local config_backup="${BACKUP_ROOT}/config/config_${timestamp}.tar.gz"
240
241    # Backup specific config files
242    tar -czf "$config_backup" \
243        /etc/fstab \
244        /etc/hosts \
245        /etc/ssh/sshd_config \
246        /etc/systemd/system/*.service \
247        2>/dev/null || log "WARN" "Some config files could not be backed up"
248
249    log "INFO" "Configuration backup completed: $config_backup"
250}
251
252transfer_to_remote() {
253    if [[ "$REMOTE_BACKUP" != "true" ]]; then
254        log "INFO" "Remote backup disabled, skipping transfer"
255        return 0
256    fi
257
258    log "INFO" "Transferring backups to remote server"
259
260    # Use rsync for efficient transfer
261    rsync -avz --delete \
262        -e "ssh -o StrictHostKeyChecking=no" \
263        "${BACKUP_ROOT}/" \
264        "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/" \
265        >> "$LOG_FILE" 2>&1 || log "WARN" "Remote transfer failed"
266
267    log "INFO" "Remote transfer completed"
268}
269
270rotate_backups() {
271    log "INFO" "Rotating old backups"
272
273    # Rotate daily backups
274    find "${BACKUP_ROOT}/full" -maxdepth 1 -type d -mtime "+${KEEP_DAILY}" \
275        -exec rm -rf {} \; 2>/dev/null || true
276
277    find "${BACKUP_ROOT}/database" -maxdepth 1 -type d -mtime "+${KEEP_DAILY}" \
278        -exec rm -rf {} \; 2>/dev/null || true
279
280    # Rotate logs older than 30 days
281    find "$LOG_DIR" -type f -name "*.log" -mtime +30 \
282        -exec rm -f {} \; 2>/dev/null || true
283
284    log "INFO" "Backup rotation completed"
285}
286
287verify_backup() {
288    log "INFO" "Verifying backup integrity"
289
290    local latest_backup="${BACKUP_ROOT}/full/latest"
291
292    if [[ ! -d "$latest_backup" ]]; then
293        log "WARN" "No backup found to verify"
294        return 0
295    fi
296
297    # Create checksums
298    find "$latest_backup" -type f -exec sha256sum {} \; \
299        > "${latest_backup}/checksums.txt" 2>/dev/null
300
301    log "INFO" "Backup verification completed"
302}
303
304#------------------------------------------------------------------------------
305# Main
306#------------------------------------------------------------------------------
307
308main() {
309    local skip_remote=false
310    local incremental=false
311    local dry_run=false
312
313    # Parse arguments
314    while [[ $# -gt 0 ]]; do
315        case "$1" in
316            -h|--help)
317                usage
318                exit 0
319                ;;
320            -n|--no-remote)
321                skip_remote=true
322                shift
323                ;;
324            -i|--incremental)
325                incremental=true
326                shift
327                ;;
328            -d|--dry-run)
329                dry_run=true
330                shift
331                ;;
332            *)
333                echo "Unknown option: $1"
334                usage
335                exit 1
336                ;;
337        esac
338    done
339
340    if [[ "$dry_run" == "true" ]]; then
341        log "INFO" "DRY RUN MODE - No actions will be performed"
342        exit 0
343    fi
344
345    log "INFO" "Starting disaster recovery backup"
346    log "INFO" "Hostname: $(hostname)"
347    log "INFO" "Timestamp: $(date)"
348
349    setup_directories
350    backup_filesystem
351    backup_databases
352    backup_config
353    verify_backup
354    rotate_backups
355
356    if [[ "$skip_remote" != "true" ]]; then
357        transfer_to_remote
358    fi
359
360    local end_time
361    end_time="$(date)"
362    log "INFO" "Backup completed successfully at $end_time"
363
364    send_notification "SUCCESS" "Disaster recovery backup completed successfully"
365}
366
367main "$@"