/
/
/
1#!/bin/bash
2# ==============================================================================
3# DNS Stack Restart Script
4# ==============================================================================
5#
6# Description: Safe restart script for Pi-hole + Unbound DNS stack
7# Usage: ./dns-stack-restart.sh [full|pihole|unbound|graceful|force]
8#
9# This script is automatically generated by Ansible - DO NOT EDIT MANUALLY
10# Template: dns-stack-restart.sh.j2
11#
12# ==============================================================================
13
14set -euo pipefail
15
16# Configuration
17PIHOLE_CONTAINER="{{ pihole_container_name }}"
18UNBOUND_CONTAINER="{{ unbound_container_name }}"
19COMPOSE_FILE="{{ docker_base_path }}/dns-stack/docker-compose.yml"
20LOG_FILE="/var/log/dns-stack-restart.log"
21WAIT_TIMEOUT=60
22HEALTH_CHECK_TIMEOUT=120
23
24# Logging function
25log() {
26 echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "${LOG_FILE}"
27}
28
29# Error handling function
30error_exit() {
31 log "ERROR: $1"
32 exit 1
33}
34
35# Check if container exists
36container_exists() {
37 docker inspect "$1" >/dev/null 2>&1
38}
39
40# Check if container is running
41container_running() {
42 docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null | grep -q "true"
43}
44
45# Wait for container to become healthy
46wait_for_healthy() {
47 local container="$1"
48 local timeout="$2"
49 local start_time=$(date +%s)
50
51 log "Waiting for ${container} to become healthy (timeout: ${timeout}s)"
52
53 while [[ $(($(date +%s) - start_time)) -lt timeout ]]; do
54 if docker inspect -f '{{.State.Health.Status}}' "${container}" 2>/dev/null | grep -q "healthy"; then
55 log "${container} is healthy"
56 return 0
57 fi
58 sleep 5
59 done
60
61 log "WARNING: ${container} did not become healthy within ${timeout} seconds"
62 return 1
63}
64
65# Test DNS functionality
66test_dns() {
67 log "Testing DNS functionality after restart"
68
69 if dig +short +time=2 +tries=2 @localhost google.com >/dev/null 2>&1; then
70 log "DNS test successful"
71 return 0
72 else
73 log "WARNING: DNS test failed immediately after restart"
74 return 1
75 fi
76}
77
78# Full DNS stack restart
79restart_full() {
80 log "Starting full DNS stack restart"
81
82 # Stop services in reverse dependency order
83 if container_running "${PIHOLE_CONTAINER}"; then
84 log "Stopping Pi-hole"
85 docker stop "${PIHOLE_CONTAINER}" 2>/dev/null || true
86 fi
87
88 if container_running "${UNBOUND_CONTAINER}"; then
89 log "Stopping Unbound"
90 docker stop "${UNBOUND_CONTAINER}" 2>/dev/null || true
91 fi
92
93 # Start services in dependency order
94 log "Starting Unbound"
95 docker start "${UNBOUND_CONTAINER}" 2>/dev/null || \
96 docker-compose -f "${COMPOSE_FILE}" up -d "${UNBOUND_CONTAINER}" 2>/dev/null || true
97
98 wait_for_healthy "${UNBOUND_CONTAINER}" ${WAIT_TIMEOUT} || true
99
100 log "Starting Pi-hole"
101 docker start "${PIHOLE_CONTAINER}" 2>/dev/null || \
102 docker-compose -f "${COMPOSE_FILE}" up -d "${PIHOLE_CONTAINER}" 2>/dev/null || true
103
104 wait_for_healthy "${PIHOLE_CONTAINER}" ${WAIT_TIMEOUT} || true
105
106 # Final health check
107 sleep 10
108 test_dns || true
109
110 log "Full DNS stack restart completed"
111}
112
113# Pi-hole only restart
114restart_pihole() {
115 log "Starting Pi-hole only restart"
116
117 if container_running "${PIHOLE_CONTAINER}"; then
118 docker restart "${PIHOLE_CONTAINER}" 2>/dev/null || true
119 else
120 docker start "${PIHOLE_CONTAINER}" 2>/dev/null || \
121 docker-compose -f "${COMPOSE_FILE}" up -d "${PIHOLE_CONTAINER}" 2>/dev/null || true
122 fi
123
124 wait_for_healthy "${PIHOLE_CONTAINER}" ${WAIT_TIMEOUT} || true
125
126 # Test DNS after Pi-hole restart
127 sleep 5
128 test_dns || true
129
130 log "Pi-hole restart completed"
131}
132
133# Unbound only restart
134restart_unbound() {
135 log "Starting Unbound only restart"
136
137 if container_running "${UNBOUND_CONTAINER}"; then
138 docker restart "${UNBOUND_CONTAINER}" 2>/dev/null || true
139 else
140 docker start "${UNBOUND_CONTAINER}" 2>/dev/null || \
141 docker-compose -f "${COMPOSE_FILE}" up -d "${UNBOUND_CONTAINER}" 2>/dev/null || true
142 fi
143
144 wait_for_healthy "${UNBOUND_CONTAINER}" ${WAIT_TIMEOUT} || true
145
146 log "Unbound restart completed"
147}
148
149# Graceful restart using docker-compose
150graceful_restart() {
151 log "Starting graceful DNS stack restart using docker-compose"
152
153 if [[ -f "${COMPOSE_FILE}" ]]; then
154 docker-compose -f "${COMPOSE_FILE}" down --timeout 30 2>/dev/null || true
155 sleep 5
156 docker-compose -f "${COMPOSE_FILE}" up -d 2>/dev/null || true
157
158 # Wait for services to become healthy
159 wait_for_healthy "${UNBOUND_CONTAINER}" ${HEALTH_CHECK_TIMEOUT} || true
160 wait_for_healthy "${PIHOLE_CONTAINER}" ${HEALTH_CHECK_TIMEOUT} || true
161
162 # Final DNS test
163 sleep 10
164 test_dns || true
165
166 log "Graceful restart completed"
167 else
168 error_exit "Docker compose file not found: ${COMPOSE_FILE}"
169 fi
170}
171
172# Force restart (stop, remove, then start)
173force_restart() {
174 log "Starting force restart of DNS stack"
175
176 # Stop and remove containers
177 docker stop "${PIHOLE_CONTAINER}" "${UNBOUND_CONTAINER}" 2>/dev/null || true
178 docker rm "${PIHOLE_CONTAINER}" "${UNBOUND_CONTAINER}" 2>/dev/null || true
179
180 # Recreate using docker-compose
181 if [[ -f "${COMPOSE_FILE}" ]]; then
182 docker-compose -f "${COMPOSE_FILE}" up -d --force-recreate 2>/dev/null || true
183
184 # Wait for services to become healthy
185 wait_for_healthy "${UNBOUND_CONTAINER}" ${HEALTH_CHECK_TIMEOUT} || true
186 wait_for_healthy "${PIHOLE_CONTAINER}" ${HEALTH_CHECK_TIMEOUT} || true
187
188 # Final DNS test
189 sleep 15
190 test_dns || true
191
192 log "Force restart completed"
193 else
194 error_exit "Docker compose file not found: ${COMPOSE_FILE}"
195 fi
196}
197
198# Show current status
199show_status() {
200 log "Current DNS stack status:"
201
202 for container in "${PIHOLE_CONTAINER}" "${UNBOUND_CONTAINER}"; do
203 if container_exists "${container}"; then
204 local status=$(docker inspect -f '{{.State.Status}}' "${container}" 2>/dev/null || echo "unknown")
205 local health=$(docker inspect -f '{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "no healthcheck")
206 local restarts=$(docker inspect -f '{{.RestartCount}}' "${container}" 2>/dev/null || echo "0")
207
208 log "${container}: status=${status}, health=${health}, restarts=${restarts}"
209 else
210 log "${container}: does not exist"
211 fi
212 done
213}
214
215# Show usage
216usage() {
217 cat << EOF
218DNS Stack Restart Script
219
220Usage: $0 [mode]
221
222Modes:
223 full Full restart (stop all, start in order)
224 pihole Restart Pi-hole only
225 unbound Restart Unbound only
226 graceful Graceful restart using docker-compose
227 force Force restart (recreate containers)
228 status Show current status
229 help Show this help message
230
231Options:
232 This script ensures proper dependency ordering and health checks
233 during restarts to maintain DNS service availability
234
235Examples:
236 $0 full
237 $0 pihole
238 $0 graceful
239 $0 force
240 $0 status
241EOF
242}
243
244# Main execution
245main() {
246 local mode="${1:-help}"
247
248 case "${mode}" in
249 full)
250 restart_full
251 ;;
252 pihole)
253 restart_pihole
254 ;;
255 unbound)
256 restart_unbound
257 ;;
258 graceful)
259 graceful_restart
260 ;;
261 force)
262 force_restart
263 ;;
264 status)
265 show_status
266 ;;
267 help|*)
268 usage
269 ;;
270 esac
271}
272
273# Run main function with all arguments
274main "$@"