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 "$@"