]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/test_HealthChecks.py
dnsdist: Implement exponential back-off for the 'lazy' mode
[thirdparty/pdns.git] / regression-tests.dnsdist / test_HealthChecks.py
CommitLineData
98650fde
RG
1#!/usr/bin/env python
2import base64
412643ce 3import threading
98650fde 4import time
412643ce 5import ssl
98650fde
RG
6import dns
7from dnsdisttests import DNSDistTest
8
9class HealthCheckTest(DNSDistTest):
10 _consoleKey = DNSDistTest.generateConsoleKey()
11 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
12 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort']
13 _config_template = """
14 setKey("%s")
15 controlSocket("127.0.0.1:%d")
16 newServer{address="127.0.0.1:%d"}
17 """
18
19 def getBackendStatus(self):
20 return self.sendConsoleCommand("if getServer(0):isUp() then return 'up' else return 'down' end").strip("\n")
21
22class TestDefaultHealthCheck(HealthCheckTest):
23 # this test suite uses a different responder port
24 # because we need fresh counters
25 _testServerPort = 5380
26
27 def testDefault(self):
28 """
29 HealthChecks: Default
30 """
31 before = TestDefaultHealthCheck._healthCheckCounter
e4fb91e8 32 time.sleep(1.5)
98650fde 33 self.assertGreater(TestDefaultHealthCheck._healthCheckCounter, before)
4bfebc93 34 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
35
36 self.sendConsoleCommand("getServer(0):setUp()")
4bfebc93 37 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
38
39 before = TestDefaultHealthCheck._healthCheckCounter
e4fb91e8 40 time.sleep(1.5)
4bfebc93 41 self.assertEqual(TestDefaultHealthCheck._healthCheckCounter, before)
98650fde
RG
42
43 self.sendConsoleCommand("getServer(0):setDown()")
4bfebc93 44 self.assertEqual(self.getBackendStatus(), 'down')
98650fde
RG
45
46 before = TestDefaultHealthCheck._healthCheckCounter
e4fb91e8 47 time.sleep(1.5)
4bfebc93 48 self.assertEqual(TestDefaultHealthCheck._healthCheckCounter, before)
98650fde
RG
49
50 self.sendConsoleCommand("getServer(0):setAuto()")
51 # we get back the previous state, which was up
4bfebc93 52 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
53
54 before = TestDefaultHealthCheck._healthCheckCounter
e4fb91e8 55 time.sleep(1.5)
98650fde 56 self.assertGreater(TestDefaultHealthCheck._healthCheckCounter, before)
4bfebc93 57 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
58
59 self.sendConsoleCommand("getServer(0):setDown()")
4bfebc93 60 self.assertEqual(self.getBackendStatus(), 'down')
98650fde 61 self.sendConsoleCommand("getServer(0):setAuto(false)")
98650fde
RG
62
63 before = TestDefaultHealthCheck._healthCheckCounter
e4fb91e8 64 time.sleep(1.5)
98650fde 65 self.assertGreater(TestDefaultHealthCheck._healthCheckCounter, before)
4bfebc93 66 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
67
68class TestHealthCheckForcedUP(HealthCheckTest):
69 # this test suite uses a different responder port
70 # because we need fresh counters
71 _testServerPort = 5381
72
73 _config_template = """
74 setKey("%s")
75 controlSocket("127.0.0.1:%d")
76 srv = newServer{address="127.0.0.1:%d"}
77 srv:setUp()
78 """
79
80 def testForcedUp(self):
81 """
82 HealthChecks: Forced UP
83 """
84 before = TestHealthCheckForcedUP._healthCheckCounter
e4fb91e8 85 time.sleep(1.5)
4bfebc93
CH
86 self.assertEqual(TestHealthCheckForcedUP._healthCheckCounter, before)
87 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
88
89class TestHealthCheckForcedDown(HealthCheckTest):
90 # this test suite uses a different responder port
91 # because we need fresh counters
92 _testServerPort = 5382
93
94 _config_template = """
95 setKey("%s")
96 controlSocket("127.0.0.1:%d")
97 srv = newServer{address="127.0.0.1:%d"}
98 srv:setDown()
99 """
100
101 def testForcedDown(self):
102 """
103 HealthChecks: Forced Down
104 """
105 before = TestHealthCheckForcedDown._healthCheckCounter
e4fb91e8 106 time.sleep(1.5)
4bfebc93 107 self.assertEqual(TestHealthCheckForcedDown._healthCheckCounter, before)
98650fde
RG
108
109class TestHealthCheckCustomName(HealthCheckTest):
110 # this test suite uses a different responder port
111 # because it uses a different health check name
112 _testServerPort = 5383
113
114 _healthCheckName = 'powerdns.com.'
115 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_healthCheckName']
116 _config_template = """
117 setKey("%s")
118 controlSocket("127.0.0.1:%d")
119 srv = newServer{address="127.0.0.1:%d", checkName='%s'}
120 """
121
122 def testAuto(self):
123 """
124 HealthChecks: Custom name
125 """
126 before = TestHealthCheckCustomName._healthCheckCounter
e4fb91e8 127 time.sleep(1.5)
98650fde 128 self.assertGreater(TestHealthCheckCustomName._healthCheckCounter, before)
4bfebc93 129 self.assertEqual(self.getBackendStatus(), 'up')
98650fde
RG
130
131class TestHealthCheckCustomNameNoAnswer(HealthCheckTest):
132 # this test suite uses a different responder port
133 # because it uses a different health check configuration
134 _testServerPort = 5384
135
e44df0f1 136 _answerUnexpected = False
98650fde
RG
137 _config_template = """
138 setKey("%s")
139 controlSocket("127.0.0.1:%d")
140 srv = newServer{address="127.0.0.1:%d", checkName='powerdns.com.'}
141 """
142
143 def testAuto(self):
144 """
145 HealthChecks: Custom name not expected by the responder
146 """
147 before = TestHealthCheckCustomNameNoAnswer._healthCheckCounter
e4fb91e8 148 time.sleep(1.5)
4bfebc93
CH
149 self.assertEqual(TestHealthCheckCustomNameNoAnswer._healthCheckCounter, before)
150 self.assertEqual(self.getBackendStatus(), 'down')
98650fde
RG
151
152class TestHealthCheckCustomFunction(HealthCheckTest):
153 # this test suite uses a different responder port
154 # because it uses a different health check configuration
155 _testServerPort = 5385
e44df0f1 156 _answerUnexpected = False
98650fde
RG
157
158 _healthCheckName = 'powerdns.com.'
159 _config_template = """
160 setKey("%s")
161 controlSocket("127.0.0.1:%d")
162
163 function myHealthCheckFunction(qname, qtype, qclass, dh)
164 dh:setCD(true)
165
d3ec24f9 166 return newDNSName('powerdns.com.'), DNSQType.AAAA, qclass
98650fde
RG
167 end
168
169 srv = newServer{address="127.0.0.1:%d", checkName='powerdns.org.', checkFunction=myHealthCheckFunction}
170 """
171
172 def testAuto(self):
173 """
174 HealthChecks: Custom function
175 """
176 before = TestHealthCheckCustomFunction._healthCheckCounter
e4fb91e8 177 time.sleep(1.5)
98650fde 178 self.assertGreater(TestHealthCheckCustomFunction._healthCheckCounter, before)
4bfebc93 179 self.assertEqual(self.getBackendStatus(), 'up')
412643ce
RG
180
181_do53HealthCheckQueries = 0
182_dotHealthCheckQueries = 0
183_dohHealthCheckQueries = 0
184
185class TestLazyHealthChecks(HealthCheckTest):
186 _do53Port = 10700
187 _dotPort = 10701
188 _dohPort = 10702
189
190 _consoleKey = DNSDistTest.generateConsoleKey()
191 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
192 _config_params = ['_consoleKeyB64', '_consolePort', '_do53Port', '_dotPort', '_dohPort']
193 _config_template = """
194 setKey("%s")
195 controlSocket("127.0.0.1:%d")
196
197 newServer{address="127.0.0.1:%s", healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=1, lazyHealthCheckThreshold=10, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOrServFail', pool=''}
198
199 newServer{address="127.0.0.1:%s", tls='openssl', caStore='ca.pem', healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=1, lazyHealthCheckThreshold=10, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOrServFail', pool='dot'}
200 addAction('dot.lazy.test.powerdns.com.', PoolAction('dot'))
201
202 newServer{address="127.0.0.1:%s", tls='openssl', dohPath='/dns-query', caStore='ca.pem', healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=1, lazyHealthCheckThreshold=10, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOrServFail', pool='doh'}
203 addAction('doh.lazy.test.powerdns.com.', PoolAction('doh'))
204 """
205 _verboseMode = True
206
207 @staticmethod
208 def HandleDNSQuery(request):
209 response = dns.message.make_response(request)
210 if str(request.question[0].name).startswith('server-failure'):
211 response.set_rcode(dns.rcode.SERVFAIL)
212 return response.to_wire()
213
214 @classmethod
215 def Do53Callback(cls, request):
216 global _do53HealthCheckQueries
217 if str(request.question[0].name).startswith('a.root-servers.net'):
218 _do53HealthCheckQueries = _do53HealthCheckQueries + 1
219 response = dns.message.make_response(request)
220 return response.to_wire()
221 return cls.HandleDNSQuery(request)
222
223 @classmethod
224 def DoTCallback(cls, request):
225 global _dotHealthCheckQueries
226 if str(request.question[0].name).startswith('a.root-servers.net'):
227 _dotHealthCheckQueries = _dotHealthCheckQueries + 1
228 response = dns.message.make_response(request)
229 return response.to_wire()
230 return cls.HandleDNSQuery(request)
231
232 @classmethod
233 def DoHCallback(cls, request, requestHeaders, fromQueue, toQueue):
234 global _dohHealthCheckQueries
235 if str(request.question[0].name).startswith('a.root-servers.net'):
236 _dohHealthCheckQueries = _dohHealthCheckQueries + 1
237 response = dns.message.make_response(request)
238 return 200, response.to_wire()
239 return 200, cls.HandleDNSQuery(request)
240
241 @classmethod
242 def startResponders(cls):
243 tlsContext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
244 tlsContext.load_cert_chain('server.chain', 'server.key')
245
246 Do53Responder = threading.Thread(name='Do53 Lazy Responder', target=cls.UDPResponder, args=[cls._do53Port, cls._toResponderQueue, cls._fromResponderQueue, False, cls.Do53Callback])
247 Do53Responder.setDaemon(True)
248 Do53Responder.start()
249
250 Do53TCPResponder = threading.Thread(name='Do53 TCP Lazy Responder', target=cls.TCPResponder, args=[cls._do53Port, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.Do53Callback])
251 Do53TCPResponder.setDaemon(True)
252 Do53TCPResponder.start()
253
254 DoTResponder = threading.Thread(name='DoT Lazy Responder', target=cls.TCPResponder, args=[cls._dotPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.DoTCallback, tlsContext])
255 DoTResponder.setDaemon(True)
256 DoTResponder.start()
257
258 DoHResponder = threading.Thread(name='DoH Lazy Responder', target=cls.DOHResponder, args=[cls._dohPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.DoHCallback, tlsContext])
259 DoHResponder.setDaemon(True)
260 DoHResponder.start()
261
262 def testDo53Lazy(self):
263 """
264 Lazy Healthchecks: Do53
265 """
93b395a5
RG
266 # there is one initial query on startup
267 self.assertEqual(_do53HealthCheckQueries, 1)
412643ce 268 time.sleep(1)
93b395a5 269 self.assertEqual(_do53HealthCheckQueries, 1)
412643ce
RG
270
271 name = 'do53.lazy.test.powerdns.com.'
272 query = dns.message.make_query(name, 'A', 'IN')
273 response = dns.message.make_response(query)
274 failedQuery = dns.message.make_query('server-failure.do53.lazy.test.powerdns.com.', 'A', 'IN')
275 failedResponse = dns.message.make_response(failedQuery)
276 failedResponse.set_rcode(dns.rcode.SERVFAIL)
277
278 # send a few valid queries
279 for _ in range(5):
280 for method in ("sendUDPQuery", "sendTCPQuery"):
281 sender = getattr(self, method)
282 (_, receivedResponse) = sender(query, response=None, useQueue=False)
283 self.assertEqual(receivedResponse, response)
284
93b395a5 285 self.assertEqual(_do53HealthCheckQueries, 1)
412643ce
RG
286
287 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
288 for _ in range(2):
289 (_, receivedResponse) = self.sendUDPQuery(failedQuery, response=None, useQueue=False)
290 self.assertEqual(receivedResponse, failedResponse)
291
292 time.sleep(1.5)
93b395a5 293 self.assertEqual(_do53HealthCheckQueries, 2)
412643ce
RG
294 self.assertEqual(self.getBackendStatus(), 'up')
295
296 def testDoTLazy(self):
297 """
298 Lazy Healthchecks: DoT
299 """
93b395a5
RG
300 # there is one initial query on startup
301 self.assertEqual(_dotHealthCheckQueries, 1)
412643ce 302 time.sleep(1)
93b395a5 303 self.assertEqual(_dotHealthCheckQueries, 1)
412643ce
RG
304
305 name = 'dot.lazy.test.powerdns.com.'
306 query = dns.message.make_query(name, 'A', 'IN')
307 response = dns.message.make_response(query)
308 failedQuery = dns.message.make_query('server-failure.dot.lazy.test.powerdns.com.', 'A', 'IN')
309 failedResponse = dns.message.make_response(failedQuery)
310 failedResponse.set_rcode(dns.rcode.SERVFAIL)
311
312 # send a few valid queries
313 for _ in range(5):
314 for method in ("sendUDPQuery", "sendTCPQuery"):
315 sender = getattr(self, method)
316 (_, receivedResponse) = sender(query, response=None, useQueue=False)
317 self.assertEqual(receivedResponse, response)
318
93b395a5 319 self.assertEqual(_dotHealthCheckQueries, 1)
412643ce
RG
320
321 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
322 for _ in range(2):
323 (_, receivedResponse) = self.sendUDPQuery(failedQuery, response=None, useQueue=False)
324 self.assertEqual(receivedResponse, failedResponse)
325
326 time.sleep(1.5)
93b395a5 327 self.assertEqual(_dotHealthCheckQueries, 2)
412643ce
RG
328 self.assertEqual(self.getBackendStatus(), 'up')
329
330 def testDoHLazy(self):
331 """
332 Lazy Healthchecks: DoH
333 """
93b395a5
RG
334 # there is one initial query on startup
335 self.assertEqual(_dohHealthCheckQueries, 1)
412643ce 336 time.sleep(1)
93b395a5 337 self.assertEqual(_dohHealthCheckQueries, 1)
412643ce
RG
338
339 name = 'doh.lazy.test.powerdns.com.'
340 query = dns.message.make_query(name, 'A', 'IN')
341 response = dns.message.make_response(query)
342 failedQuery = dns.message.make_query('server-failure.doh.lazy.test.powerdns.com.', 'A', 'IN')
343 failedResponse = dns.message.make_response(failedQuery)
344 failedResponse.set_rcode(dns.rcode.SERVFAIL)
345
346 # send a few valid queries
347 for _ in range(5):
348 for method in ("sendUDPQuery", "sendTCPQuery"):
349 sender = getattr(self, method)
350 (_, receivedResponse) = sender(query, response=None, useQueue=False)
351 self.assertEqual(receivedResponse, response)
352
93b395a5 353 self.assertEqual(_dohHealthCheckQueries, 1)
412643ce
RG
354
355 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
356 for _ in range(2):
357 (_, receivedResponse) = self.sendUDPQuery(failedQuery, response=None, useQueue=False)
358 self.assertEqual(receivedResponse, failedResponse)
359
360 time.sleep(1.5)
93b395a5 361 self.assertEqual(_dohHealthCheckQueries, 2)
412643ce 362 self.assertEqual(self.getBackendStatus(), 'up')