1#!/usr/bin/env bash
2#
3# Disaster Recovery Restore Script
4#
5# This script restores from backups created by dr_backup.sh
6#
7# Features:
8# - Interactive backup selection
9# - Integrity verification (checksums)
10# - Database restoration
11# - Configuration file restoration
12# - Dry-run mode
13# - Comprehensive logging
14
15set -euo pipefail
16
17#------------------------------------------------------------------------------
18# Configuration
19#------------------------------------------------------------------------------
20
21readonly SCRIPT_NAME="$(basename "$0")"
22readonly BACKUP_ROOT="/var/backups/dr"
23readonly LOG_DIR="${BACKUP_ROOT}/logs"
24readonly LOG_FILE="${LOG_DIR}/restore_$(date +%Y%m%d_%H%M%S).log"
25
26# Database configuration
27readonly DB_HOST="localhost"
28readonly DB_PORT="5432"
29readonly DB_USER="backup_user"
30
31# Restore settings
32readonly RESTORE_ROOT="/restore"
33
34#------------------------------------------------------------------------------
35# Functions
36#------------------------------------------------------------------------------
37
38usage() {
39 cat <<EOF
40Usage: $SCRIPT_NAME [OPTIONS]
41
42Restore from disaster recovery backups.
43
44OPTIONS:
45 -h, --help Show this help message
46 -d, --date DATE Restore from specific date (YYYYMMDD_HHMMSS)
47 -t, --type TYPE Restore type: all|filesystem|database|config
48 -n, --dry-run Show what would be done without executing
49 -l, --list List available backups
50 -v, --verify Verify backup integrity only
51
52EXAMPLES:
53 $SCRIPT_NAME --list # List available backups
54 $SCRIPT_NAME -d 20240215_120000 # Restore specific backup
55 $SCRIPT_NAME -t database # Restore databases only
56 $SCRIPT_NAME -n -d 20240215_120000 # Dry-run mode
57
58EOF
59}
60
61log() {
62 local level="$1"
63 shift
64 local message="$*"
65 local timestamp
66 timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
67
68 echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE"
69}
70
71die() {
72 log "ERROR" "$*"
73 exit 1
74}
75
76list_backups() {
77 log "INFO" "Available backups:"
78
79 echo ""
80 echo "Full Filesystem Backups:"
81 if [[ -d "${BACKUP_ROOT}/full" ]]; then
82 find "${BACKUP_ROOT}/full" -maxdepth 1 -type d | \
83 grep -E '[0-9]{8}_[0-9]{6}' | \
84 sort -r | \
85 sed 's/.*\// - /'
86 else
87 echo " No backups found"
88 fi
89
90 echo ""
91 echo "Database Backups:"
92 if [[ -d "${BACKUP_ROOT}/database" ]]; then
93 find "${BACKUP_ROOT}/database" -maxdepth 1 -type d | \
94 grep -E '[0-9]{8}_[0-9]{6}' | \
95 sort -r | \
96 sed 's/.*\// - /'
97 else
98 echo " No backups found"
99 fi
100
101 echo ""
102}
103
104verify_backup_integrity() {
105 local backup_date="$1"
106 local backup_path="${BACKUP_ROOT}/full/${backup_date}"
107
108 log "INFO" "Verifying backup integrity: $backup_date"
109
110 if [[ ! -f "${backup_path}/checksums.txt" ]]; then
111 log "WARN" "No checksum file found for backup: $backup_date"
112 return 1
113 fi
114
115 pushd "$backup_path" > /dev/null
116
117 if sha256sum -c checksums.txt >> "$LOG_FILE" 2>&1; then
118 log "INFO" "Backup integrity verified successfully"
119 popd > /dev/null
120 return 0
121 else
122 log "ERROR" "Backup integrity verification failed"
123 popd > /dev/null
124 return 1
125 fi
126}
127
128restore_filesystem() {
129 local backup_date="$1"
130 local dry_run="$2"
131 local backup_path="${BACKUP_ROOT}/full/${backup_date}"
132
133 log "INFO" "Restoring filesystem from: $backup_date"
134
135 if [[ ! -d "$backup_path" ]]; then
136 die "Backup not found: $backup_path"
137 fi
138
139 # Create restore directory
140 if [[ "$dry_run" != "true" ]]; then
141 mkdir -p "$RESTORE_ROOT"
142 fi
143
144 # Find tar archives
145 local archives
146 mapfile -t archives < <(find "$backup_path" -name "*.tar*" -type f)
147
148 if [[ ${#archives[@]} -eq 0 ]]; then
149 log "WARN" "No tar archives found in backup"
150 return 0
151 fi
152
153 for archive in "${archives[@]}"; do
154 log "INFO" "Processing archive: $(basename "$archive")"
155
156 if [[ "$dry_run" == "true" ]]; then
157 log "INFO" "[DRY-RUN] Would extract: $archive"
158 continue
159 fi
160
161 # Decrypt if needed
162 if [[ "$archive" == *.gpg ]]; then
163 log "INFO" "Decrypting archive"
164 local decrypted="${archive%.gpg}"
165 gpg --decrypt "$archive" > "$decrypted" || die "Decryption failed"
166 archive="$decrypted"
167 fi
168
169 # Decompress if needed
170 if [[ "$archive" == *.gz ]]; then
171 log "INFO" "Decompressing archive"
172 gunzip -k "$archive" || die "Decompression failed"
173 archive="${archive%.gz}"
174 fi
175
176 # Extract
177 log "INFO" "Extracting archive to: $RESTORE_ROOT"
178 tar -xpf "$archive" -C "$RESTORE_ROOT" || die "Extraction failed"
179 done
180
181 log "INFO" "Filesystem restore completed"
182}
183
184restore_databases() {
185 local backup_date="$1"
186 local dry_run="$2"
187 local db_backup_path="${BACKUP_ROOT}/database/${backup_date}"
188
189 log "INFO" "Restoring databases from: $backup_date"
190
191 if [[ ! -d "$db_backup_path" ]]; then
192 die "Database backup not found: $db_backup_path"
193 fi
194
195 if ! command -v pg_restore &>/dev/null; then
196 log "WARN" "pg_restore not found, skipping database restore"
197 return 0
198 fi
199
200 # Find database dumps
201 local dumps
202 mapfile -t dumps < <(find "$db_backup_path" -name "*.sql*" -type f)
203
204 if [[ ${#dumps[@]} -eq 0 ]]; then
205 log "WARN" "No database dumps found in backup"
206 return 0
207 fi
208
209 for dump in "${dumps[@]}"; do
210 local db_name
211 db_name="$(basename "$dump" | cut -d'_' -f1)"
212
213 log "INFO" "Restoring database: $db_name"
214
215 if [[ "$dry_run" == "true" ]]; then
216 log "INFO" "[DRY-RUN] Would restore database: $db_name from $dump"
217 continue
218 fi
219
220 # Decrypt if needed
221 if [[ "$dump" == *.gpg ]]; then
222 log "INFO" "Decrypting database dump"
223 local decrypted="${dump%.gpg}"
224 gpg --decrypt "$dump" > "$decrypted" || die "Decryption failed"
225 dump="$decrypted"
226 fi
227
228 # Decompress if needed
229 if [[ "$dump" == *.gz ]]; then
230 log "INFO" "Decompressing database dump"
231 gunzip -k "$dump" || die "Decompression failed"
232 dump="${dump%.gz}"
233 fi
234
235 # Restore database
236 PGPASSWORD="${DB_PASSWORD:-}" pg_restore \
237 -h "$DB_HOST" \
238 -p "$DB_PORT" \
239 -U "$DB_USER" \
240 -d "$db_name" \
241 -c \
242 "$dump" \
243 >> "$LOG_FILE" 2>&1 || log "WARN" "Database restore failed for $db_name"
244 done
245
246 log "INFO" "Database restore completed"
247}
248
249restore_config() {
250 local backup_date="$1"
251 local dry_run="$2"
252 local config_backup="${BACKUP_ROOT}/config/config_${backup_date}.tar.gz"
253
254 log "INFO" "Restoring configuration files"
255
256 if [[ ! -f "$config_backup" ]]; then
257 log "WARN" "Configuration backup not found: $config_backup"
258 return 0
259 fi
260
261 if [[ "$dry_run" == "true" ]]; then
262 log "INFO" "[DRY-RUN] Would restore config from: $config_backup"
263 return 0
264 fi
265
266 # Extract to temporary location first
267 local temp_dir
268 temp_dir="$(mktemp -d)"
269 trap 'rm -rf "$temp_dir"' EXIT
270
271 tar -xzf "$config_backup" -C "$temp_dir" || die "Config extraction failed"
272
273 log "INFO" "Configuration files extracted to: $temp_dir"
274 log "WARN" "Please manually review and copy config files as needed"
275
276 log "INFO" "Configuration restore completed"
277}
278
279#------------------------------------------------------------------------------
280# Main
281#------------------------------------------------------------------------------
282
283main() {
284 local backup_date=""
285 local restore_type="all"
286 local dry_run=false
287 local list_only=false
288 local verify_only=false
289
290 # Parse arguments
291 while [[ $# -gt 0 ]]; do
292 case "$1" in
293 -h|--help)
294 usage
295 exit 0
296 ;;
297 -d|--date)
298 backup_date="$2"
299 shift 2
300 ;;
301 -t|--type)
302 restore_type="$2"
303 shift 2
304 ;;
305 -n|--dry-run)
306 dry_run=true
307 shift
308 ;;
309 -l|--list)
310 list_only=true
311 shift
312 ;;
313 -v|--verify)
314 verify_only=true
315 shift
316 ;;
317 *)
318 echo "Unknown option: $1"
319 usage
320 exit 1
321 ;;
322 esac
323 done
324
325 # Setup logging
326 mkdir -p "$LOG_DIR"
327
328 if [[ "$list_only" == "true" ]]; then
329 list_backups
330 exit 0
331 fi
332
333 if [[ -z "$backup_date" ]]; then
334 die "Backup date required. Use --list to see available backups"
335 fi
336
337 log "INFO" "Starting disaster recovery restore"
338 log "INFO" "Backup date: $backup_date"
339 log "INFO" "Restore type: $restore_type"
340
341 if [[ "$verify_only" == "true" ]]; then
342 verify_backup_integrity "$backup_date"
343 exit 0
344 fi
345
346 # Verify integrity before restore
347 if ! verify_backup_integrity "$backup_date"; then
348 die "Backup integrity check failed. Aborting restore."
349 fi
350
351 case "$restore_type" in
352 all)
353 restore_filesystem "$backup_date" "$dry_run"
354 restore_databases "$backup_date" "$dry_run"
355 restore_config "$backup_date" "$dry_run"
356 ;;
357 filesystem)
358 restore_filesystem "$backup_date" "$dry_run"
359 ;;
360 database)
361 restore_databases "$backup_date" "$dry_run"
362 ;;
363 config)
364 restore_config "$backup_date" "$dry_run"
365 ;;
366 *)
367 die "Unknown restore type: $restore_type"
368 ;;
369 esac
370
371 log "INFO" "Restore completed successfully"
372}
373
374main "$@"