/
/
/
1#!/bin/bash
2# ==============================================================================
3# Connectivity Services Backup Script
4# ==============================================================================
5#
6# Description: Comprehensive backup of all connectivity Docker services
7# Usage: ./connectivity-backup.sh [full|config|data|verify|restore|cleanup]
8#
9# This script is automatically generated by Ansible - DO NOT EDIT MANUALLY
10# Template: connectivity-backup.sh.j2
11#
12# ==============================================================================
13
14set -euo pipefail
15
16# Configuration
17DOCKER_BASE_PATH="{{ docker_base_path }}"
18BACKUP_ROOT="${DOCKER_BASE_PATH}/backups"
19BACKUP_DIR="${BACKUP_ROOT}/connectivity-$(date +%Y%m%d-%H%M%S)"
20LOG_FILE="/var/log/connectivity-backup.log"
21RETENTION_DAYS={{ backup_retention_days | default(7) }}
22COMPRESSION_LEVEL={{ backup_compression_level | default(6) }}
23
24# Service configuration
25SERVICES=(
26 "{{ unbound_service_name }}"
27 "{{ pihole_service_name }}"
28 "{{ nginx_proxy_db_service_name }}"
29 "{{ nginx_proxy_service_name }}"
30 "{{ wireguard_service_name }}"
31)
32
33# Critical data directories
34CRITICAL_DIRS=(
35 "${DOCKER_BASE_PATH}/unbound/config"
36 "${DOCKER_BASE_PATH}/unbound/data"
37 "${DOCKER_BASE_PATH}/pihole/config"
38 "${DOCKER_BASE_PATH}/pihole/data"
39 "${DOCKER_BASE_PATH}/nginx-proxy/data"
40 "${DOCKER_BASE_PATH}/nginx-proxy/letsencrypt"
41 "${DOCKER_BASE_PATH}/wireguard/config"
42 "${DOCKER_BASE_PATH}/wireguard/data"
43)
44
45# Logging function
46log() {
47 echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "${LOG_FILE}"
48}
49
50# Error handling function
51error_exit() {
52 log "ERROR: $1"
53 exit 1
54}
55
56# Check if container exists
57container_exists() {
58 docker inspect "$1" >/dev/null 2>&1
59}
60
61# Check if container is running
62container_running() {
63 docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null | grep -q "true"
64}
65
66# Validate backup environment
67validate_environment() {
68 log "Validating backup environment"
69
70 # Check Docker daemon
71 if ! docker info >/dev/null 2>&1; then
72 error_exit "Docker daemon is not running"
73 fi
74
75 # Check disk space
76 local available_space=$(df "${DOCKER_BASE_PATH}" | awk 'NR==2 {print $4}')
77 local min_space=$((1024 * 1024 * 1024)) # 1GB minimum
78
79 if [[ ${available_space} -lt ${min_space} ]]; then
80 error_exit "Insufficient disk space for backup (available: ${available_space}, needed: ${min_space})"
81 fi
82
83 # Create backup directory
84 mkdir -p "${BACKUP_DIR}"
85
86 log "Environment validation completed"
87}
88
89# Backup service configuration
90backup_configuration() {
91 local service="$1"
92 local service_dir="${DOCKER_BASE_PATH}/${service}"
93
94 log "Backing up configuration for ${service}"
95
96 if [[ ! -d "${service_dir}" ]]; then
97 log "WARNING: Service directory not found: ${service_dir}"
98 return 1
99 fi
100
101 # Backup compose file and environment
102 local config_files=(
103 "${service_dir}/docker-compose.yml"
104 "${service_dir}/.env"
105 "${service_dir}/config"
106 )
107
108 local config_backup="${BACKUP_DIR}/${service}-config.tar.gz"
109
110 # Find existing config files
111 local files_to_backup=()
112 for file in "${config_files[@]}"; do
113 if [[ -e "${file}" ]]; then
114 files_to_backup+=("${file}")
115 fi
116 done
117
118 if [[ ${#files_to_backup[@]} -eq 0 ]]; then
119 log "WARNING: No configuration files found for ${service}"
120 return 1
121 fi
122
123 # Create compressed backup
124 tar -czf "${config_backup}" -C "${DOCKER_BASE_PATH}" \
125 "${service}/docker-compose.yml" \
126 "${service}/.env" \
127 "${service}/config" \
128 2>/dev/null || true
129
130 if [[ -f "${config_backup}" ]]; then
131 local size=$(du -h "${config_backup}" | cut -f1)
132 log "Configuration backup created: ${config_backup} (${size})"
133 echo "${config_backup}"
134 else
135 log "WARNING: Failed to create configuration backup for ${service}"
136 echo ""
137 fi
138}
139
140# Backup service data
141backup_data() {
142 local service="$1"
143 local service_dir="${DOCKER_BASE_PATH}/${service}"
144
145 log "Backing up data for ${service}"
146
147 if [[ ! -d "${service_dir}" ]]; then
148 log "WARNING: Service directory not found: ${service_dir}"
149 return 1
150 fi
151
152 # Check if service has data directory
153 if [[ ! -d "${service_dir}/data" ]]; then
154 log "INFO: No data directory found for ${service}"
155 return 1
156 fi
157
158 local data_backup="${BACKUP_DIR}/${service}-data.tar.gz"
159
160 # Create compressed backup of data directory
161 tar -czf "${data_backup}" -C "${DOCKER_BASE_PATH}" "${service}/data" 2>/dev/null || true
162
163 if [[ -f "${data_backup}" ]]; then
164 local size=$(du -h "${data_backup}" | cut -f1)
165 log "Data backup created: ${data_backup} (${size})"
166 echo "${data_backup}"
167 else
168 log "WARNING: Failed to create data backup for ${service}"
169 echo ""
170 fi
171}
172
173# Backup Docker volumes
174backup_volumes() {
175 local service="$1"
176
177 log "Backing up Docker volumes for ${service}"
178
179 if ! container_exists "${service}"; then
180 log "WARNING: Container does not exist: ${service}"
181 return 1
182 fi
183
184 local volume_backup="${BACKUP_DIR}/${service}-volumes.tar.gz"
185
186 # Get volume information from container
187 local volumes=$(docker inspect -f '{{range .Mounts}}{{if .Name}}{{.Name}}{{end}}{{end}}' "${service}" 2>/dev/null)
188
189 if [[ -z "${volumes}" ]]; then
190 log "INFO: No named volumes found for ${service}"
191 return 1
192 fi
193
194 # Backup each volume
195 local volume_files=()
196 for volume in ${volumes}; do
197 if docker volume inspect "${volume}" >/dev/null 2>&1; then
198 volume_files+=("${volume}")
199 fi
200 done
201
202 if [[ ${#volume_files[@]} -eq 0 ]]; then
203 log "INFO: No accessible volumes found for ${service}"
204 return 1
205 fi
206
207 # Create volume backup
208 tar -czf "${volume_backup}" -C "/var/lib/docker/volumes" "${volume_files[@]}" 2>/dev/null || true
209
210 if [[ -f "${volume_backup}" ]]; then
211 local size=$(du -h "${volume_backup}" | cut -f1)
212 log "Volume backup created: ${volume_backup} (${size})"
213 echo "${volume_backup}"
214 else
215 log "WARNING: Failed to create volume backup for ${service}"
216 echo ""
217 fi
218}
219
220# Create full backup of all services
221full_backup() {
222 log "Starting full backup of all connectivity services"
223
224 validate_environment
225
226 local backup_manifest="${BACKUP_DIR}/backup-manifest.json"
227 local backup_files=()
228 local total_size=0
229
230 # Create manifest header
231 cat > "${backup_manifest}" << EOF
232{
233 "backup_date": "$(date +%Y-%m-%dT%H:%M:%S%z)",
234 "hostname": "$(hostname)",
235 "ip_address": "{{ ansible_default_ipv4.address }}",
236 "services": [
237EOF
238
239 # Backup each service
240 for service in "${SERVICES[@]}"; do
241 log "Backing up service: ${service}"
242
243 local service_info="{}"
244 local config_backup=""
245 local data_backup=""
246 local volume_backup=""
247
248 # Backup configuration
249 config_backup=$(backup_configuration "${service}")
250
251 # Backup data
252 data_backup=$(backup_data "${service}")
253
254 # Backup volumes
255 volume_backup=$(backup_volumes "${service}")
256
257 # Add to manifest
258 service_info=$(jq -n \
259 --arg service "${service}" \
260 --arg config "${config_backup}" \
261 --arg data "${data_backup}" \
262 --arg volumes "${volume_backup}" \
263 '{service: $service, config_backup: $config, data_backup: $data, volume_backup: $volumes}')
264
265 # Add to backup files list
266 if [[ -n "${config_backup}" ]]; then backup_files+=("${config_backup}"); fi
267 if [[ -n "${data_backup}" ]]; then backup_files+=("${data_backup}"); fi
268 if [[ -n "${volume_backup}" ]]; then backup_files+=("${volume_backup}"); fi
269
270 # Add to manifest
271 if [[ "${service}" != "{{ unbound_service_name }}" ]]; then
272 echo " ," >> "${backup_manifest}"
273 fi
274 echo " ${service_info}" >> "${backup_manifest}"
275 done
276
277 # Complete manifest
278 cat >> "${backup_manifest}" << EOF
279 ],
280 "total_files": ${#backup_files[@]},
281 "backup_size": "$(du -sh "${BACKUP_DIR}" | cut -f1)"
282}
283EOF
284
285 # Create final backup archive
286 local final_backup="${BACKUP_ROOT}/connectivity-full-$(date +%Y%m%d-%H%M%S).tar.gz"
287
288 log "Creating final backup archive"
289 tar -czf "${final_backup}" -C "${BACKUP_DIR}" . 2>/dev/null || true
290
291 if [[ -f "${final_backup}" ]]; then
292 local final_size=$(du -h "${final_backup}" | cut -f1)
293 log "Full backup completed: ${final_backup} (${final_size})"
294
295 # Cleanup temporary files
296 rm -rf "${BACKUP_DIR}"
297
298 echo "Backup location: ${final_backup}"
299 echo "Backup size: ${final_size}"
300 echo "Total files: ${#backup_files[@]}"
301 else
302 error_exit "Failed to create final backup archive"
303 fi
304}
305
306# Verify backup integrity
307verify_backup() {
308 local backup_file="${1:-}"
309
310 if [[ -z "${backup_file}" ]]; then
311 # Find latest backup
312 backup_file=$(ls -t "${BACKUP_ROOT}/connectivity-full-*.tar.gz" 2>/dev/null | head -1)
313
314 if [[ -z "${backup_file}" ]]; then
315 error_exit "No backup files found"
316 fi
317 fi
318
319 if [[ ! -f "${backup_file}" ]]; then
320 error_exit "Backup file not found: ${backup_file}"
321 fi
322
323 log "Verifying backup: ${backup_file}"
324
325 # Check if backup is valid tar archive
326 if ! tar -tzf "${backup_file}" >/dev/null 2>&1; then
327 error_exit "Backup file is corrupt or invalid"
328 fi
329
330 # Check for manifest file
331 if ! tar -tzf "${backup_file}" | grep -q "backup-manifest.json"; then
332 error_exit "Backup manifest missing"
333 fi
334
335 # Extract manifest for verification
336 local temp_dir=$(mktemp -d)
337 tar -xzf "${backup_file}" -C "${temp_dir}" "backup-manifest.json" 2>/dev/null || true
338
339 if [[ ! -f "${temp_dir}/backup-manifest.json" ]]; then
340 rm -rf "${temp_dir}"
341 error_exit "Failed to extract backup manifest"
342 fi
343
344 # Verify manifest structure
345 if ! jq -e . "${temp_dir}/backup-manifest.json" >/dev/null 2>&1; then
346 rm -rf "${temp_dir}"
347 error_exit "Backup manifest is invalid JSON"
348 fi
349
350 local service_count=$(jq '.services | length' "${temp_dir}/backup-manifest.json")
351 local file_count=$(jq '.total_files' "${temp_dir}/backup-manifest.json")
352
353 rm -rf "${temp_dir}"
354
355 log "Backup verification successful"
356 log "Services backed up: ${service_count}"
357 log "Total files: ${file_count}"
358 log "Backup size: $(du -h "${backup_file}" | cut -f1)"
359
360 echo "Backup verified: ${backup_file}"
361}
362
363# Cleanup old backups
364cleanup_backups() {
365 local retention_days="${1:-${RETENTION_DAYS}}"
366
367 log "Cleaning up backups older than ${retention_days} days"
368
369 local backups_removed=$(find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" -mtime +${retention_days} -delete -print | wc -l)
370
371 log "Cleanup completed: ${backups_removed} old backup(s) removed"
372
373 # Also cleanup any temporary directories
374 find "${BACKUP_ROOT}" -name "connectivity-*" -type d -mtime +1 -exec rm -rf {} + 2>/dev/null || true
375}
376
377# Show backup statistics
378show_stats() {
379 log "Backup Statistics"
380 echo "=============================================================================="
381
382 local total_backups=$(find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" | wc -l)
383 local total_size=$(find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" -exec du -ch {} + | grep total$ | cut -f1)
384 local oldest_backup=$(find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" -printf "%T@ %p\n" | sort -n | head -1 | cut -d' ' -f2-)
385 local newest_backup=$(find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" -printf "%T@ %p\n" | sort -nr | head -1 | cut -d' ' -f2-)
386
387 echo "Total backups: ${total_backups}"
388 echo "Total size: ${total_size}"
389 echo "Oldest backup: ${oldest_backup:-None}"
390 echo "Newest backup: ${newest_backup:-None}"
391 echo "Retention policy: ${RETENTION_DAYS} days"
392 echo ""
393
394 # Show individual backup sizes
395 echo "Backup files:"
396 find "${BACKUP_ROOT}" -name "connectivity-*.tar.gz" -exec du -h {} + | sort -hr
397
398 echo "=============================================================================="
399}
400
401# Show usage
402usage() {
403 cat << EOF
404Connectivity Services Backup Script
405
406Usage: $0 [command] [options]
407
408Commands:
409 full Create full backup of all services (default)
410 config Backup only configuration files
411 data Backup only data directories
412 verify [FILE] Verify backup integrity
413 cleanup [DAYS] Cleanup old backups (default: ${RETENTION_DAYS} days)
414 stats Show backup statistics
415 help Show this help message
416
417Options:
418 FILE Specific backup file to verify
419 DAYS Retention period in days
420
421Examples:
422 $0 full
423 $0 config
424 $0 data
425 $0 verify
426 $0 verify /path/to/backup.tar.gz
427 $0 cleanup 14
428 $0 stats
429 $0 help
430
431Backup Location: ${BACKUP_ROOT}
432Retention: ${RETENTION_DAYS} days
433Compression: level ${COMPRESSION_LEVEL}
434EOF
435}
436
437# Main execution
438main() {
439 local command="${1:-full}"
440 local option="${2:-}"
441
442 # Create backup root directory
443 mkdir -p "${BACKUP_ROOT}"
444
445 case "${command}" in
446 full)
447 full_backup
448 ;;
449 config)
450 # Backup only configuration
451 validate_environment
452 for service in "${SERVICES[@]}"; do
453 backup_configuration "${service}"
454 done
455 ;;
456 data)
457 # Backup only data
458 validate_environment
459 for service in "${SERVICES[@]}"; do
460 backup_data "${service}"
461 done
462 ;;
463 verify)
464 verify_backup "${option}"
465 ;;
466 cleanup)
467 cleanup_backups "${option}"
468 ;;
469 stats)
470 show_stats
471 ;;
472 help|*)
473 usage
474 ;;
475 esac
476}
477
478# Run main function with all arguments
479main "$@"