/
/
/
1#!/bin/bash
2# ==============================================================================
3# DNS Stack Backup Script
4# ==============================================================================
5#
6# Description: Creates comprehensive backups of Pi-hole and Unbound configurations
7# Usage: ./dns-stack-backup.sh [full|pihole-only|unbound-only|rotate]
8#
9# This script is automatically generated by Ansible - DO NOT EDIT MANUALLY
10# Template: dns-stack-backup.sh.j2
11#
12# ==============================================================================
13
14set -euo pipefail
15
16# Configuration
17PIHOLE_CONFIG_DIR="{{ docker_base_path }}/pihole/config"
18PIHOLE_DNSMASQ_DIR="{{ docker_base_path }}/pihole/dnsmasq.d"
19UNBOUND_CONFIG_DIR="{{ docker_base_path }}/unbound/config"
20BACKUP_DIR="{{ docker_base_path }}/backups/dns-stack"
21RETENTION_DAYS={{ backup_retention_days | default(7) }}
22LOG_FILE="/var/log/dns-stack-backup.log"
23TIMESTAMP=$(date +%Y%m%d-%H%M%S)
24
25# Ensure directories exist
26mkdir -p "${BACKUP_DIR}"
27
28# Logging function
29log() {
30 echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "${LOG_FILE}"
31}
32
33# Error handling function
34error_exit() {
35 log "ERROR: $1"
36 exit 1
37}
38
39# Validate configuration directories
40validate_config() {
41 local dirs=("${PIHOLE_CONFIG_DIR}" "${PIHOLE_DNSMASQ_DIR}" "${UNBOUND_CONFIG_DIR}")
42
43 for dir in "${dirs[@]}"; do
44 if [[ ! -d "${dir}" ]]; then
45 log "WARNING: Configuration directory not found: ${dir}"
46 fi
47 done
48}
49
50# Create full backup (Pi-hole + Unbound)
51create_full_backup() {
52 local backup_file="${BACKUP_DIR}/dns-stack-full-${TIMESTAMP}.tar.gz"
53
54 log "Creating full DNS stack backup"
55 validate_config
56
57 # Create backup excluding large/unnecessary files
58 tar -czf "${backup_file}" \
59 -C "{{ docker_base_path }}" \
60 --exclude="*.db-*" \
61 --exclude="*.log" \
62 --exclude="cache" \
63 --exclude="tmp" \
64 pihole/config \
65 pihole/dnsmasq.d \
66 unbound/config \
67 2>/dev/null || true
68
69 if [[ -f "${backup_file}" ]]; then
70 log "Full backup created: ${backup_file}"
71 echo "Backup size: $(du -h "${backup_file}" | cut -f1)"
72 echo "Backup contents: $(tar -tzf "${backup_file}" | wc -l) files"
73 else
74 error_exit "Failed to create full backup"
75 fi
76}
77
78# Create Pi-hole only backup
79create_pihole_backup() {
80 local backup_file="${BACKUP_DIR}/pihole-only-${TIMESTAMP}.tar.gz"
81
82 log "Creating Pi-hole only backup"
83
84 if [[ ! -d "${PIHOLE_CONFIG_DIR}" ]]; then
85 error_exit "Pi-hole configuration directory not found"
86 fi
87
88 # Backup Pi-hole config excluding large files
89 tar -czf "${backup_file}" \
90 -C "{{ docker_base_path }}" \
91 --exclude="*.db-*" \
92 --exclude="*.log" \
93 --exclude="lists" \
94 pihole/config \
95 pihole/dnsmasq.d \
96 2>/dev/null || true
97
98 if [[ -f "${backup_file}" ]]; then
99 log "Pi-hole backup created: ${backup_file}"
100 echo "Backup size: $(du -h "${backup_file}" | cut -f1)"
101 else
102 error_exit "Failed to create Pi-hole backup"
103 fi
104}
105
106# Create Unbound only backup
107create_unbound_backup() {
108 local backup_file="${BACKUP_DIR}/unbound-only-${TIMESTAMP}.tar.gz"
109
110 log "Creating Unbound only backup"
111
112 if [[ ! -d "${UNBOUND_CONFIG_DIR}" ]]; then
113 error_exit "Unbound configuration directory not found"
114 fi
115
116 # Backup Unbound config
117 tar -czf "${backup_file}" -C "${UNBOUND_CONFIG_DIR}" . 2>/dev/null || true
118
119 if [[ -f "${backup_file}" ]]; then
120 log "Unbound backup created: ${backup_file}"
121 echo "Backup size: $(du -h "${backup_file}" | cut -f1)"
122 else
123 error_exit "Failed to create Unbound backup"
124 fi
125}
126
127# Rotate old backups
128rotate_backups() {
129 log "Rotating backups older than ${RETENTION_DAYS} days"
130
131 local files_removed=0
132 local space_freed=0
133
134 # Find and remove old backup files
135 find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +${RETENTION_DAYS} -print0 | while IFS= read -r -d '' file; do
136 local file_size=$(du -b "${file}" | cut -f1)
137 rm -f "${file}"
138 files_removed=$((files_removed + 1))
139 space_freed=$((space_freed + file_size))
140 log "Removed old backup: ${file}"
141 done
142
143 if [[ ${files_removed} -gt 0 ]]; then
144 log "Rotation complete: ${files_removed} files removed, $(numfmt --to=iec ${space_freed}) freed"
145 else
146 log "No old backups found for rotation"
147 fi
148}
149
150# Verify backup integrity
151verify_backup() {
152 local backup_file="${1}"
153
154 if [[ ! -f "${backup_file}" ]]; then
155 error_exit "Backup file not found: ${backup_file}"
156 fi
157
158 log "Verifying backup integrity: ${backup_file}"
159
160 # Test tar archive
161 if tar -tzf "${backup_file}" >/dev/null 2>&1; then
162 local file_count=$(tar -tzf "${backup_file}" | wc -l)
163 log "Backup verification successful - ${file_count} files"
164 else
165 error_exit "Backup verification failed - archive may be corrupt"
166 fi
167}
168
169# List available backups
170list_backups() {
171 log "Available DNS stack backups:"
172
173 if ls "${BACKUP_DIR}"/*.tar.gz 1>/dev/null 2>&1; then
174 echo "Backup files in ${BACKUP_DIR}:"
175 ls -la "${BACKUP_DIR}"/*.tar.gz
176 echo ""
177 echo "Total backup size: $(du -sh "${BACKUP_DIR}" | cut -f1)"
178 echo "Number of backups: $(ls "${BACKUP_DIR}"/*.tar.gz 2>/dev/null | wc -l)"
179 else
180 echo "No backup files found in ${BACKUP_DIR}"
181 fi
182}
183
184# Export Pi-hole teleporter data (additional backup)
185export_teleporter() {
186 local teleporter_file="${BACKUP_DIR}/pihole-teleporter-${TIMESTAMP}.tar.gz"
187
188 log "Exporting Pi-hole teleporter data"
189
190 # Use pihole command to export data
191 if docker exec "{{ pihole_container_name }}" pihole -a -t "${teleporter_file}" 2>/dev/null; then
192 log "Teleporter export completed: ${teleporter_file}"
193 else
194 log "WARNING: Pi-hole teleporter export failed - using manual backup"
195 fi
196}
197
198# Show usage
199usage() {
200 cat << EOF
201DNS Stack Backup Script
202
203Usage: $0 [command]
204
205Commands:
206 full Create full backup (Pi-hole + Unbound)
207 pihole-only Create Pi-hole only backup
208 unbound-only Create Unbound only backup
209 rotate Remove backups older than ${RETENTION_DAYS} days
210 list List available backups
211 verify [file] Verify backup file integrity
212 teleporter Export Pi-hole teleporter data
213 help Show this help message
214
215Examples:
216 $0 full
217 $0 pihole-only
218 $0 unbound-only
219 $0 rotate
220 $0 list
221 $0 verify /backups/dns-stack/dns-stack-full-20231201-120000.tar.gz
222 $0 teleporter
223
224Backup retention: ${RETENTION_DAYS} days
225Backup directory: ${BACKUP_DIR}
226EOF
227}
228
229# Main execution
230main() {
231 local command="${1:-help}"
232
233 case "${command}" in
234 full)
235 create_full_backup
236 ;;
237 pihole-only)
238 create_pihole_backup
239 ;;
240 unbound-only)
241 create_unbound_backup
242 ;;
243 rotate)
244 rotate_backups
245 ;;
246 list)
247 list_backups
248 ;;
249 verify)
250 verify_backup "${2:-}"
251 ;;
252 teleporter)
253 export_teleporter
254 ;;
255 help|*)
256 usage
257 ;;
258 esac
259}
260
261# Run main function with all arguments
262main "$@"