]>
Commit | Line | Data |
---|---|---|
98650fde RG |
1 | #!/usr/bin/env python |
2 | import base64 | |
412643ce | 3 | import threading |
98650fde | 4 | import time |
412643ce | 5 | import ssl |
98650fde RG |
6 | import dns |
7 | from dnsdisttests import DNSDistTest | |
8 | ||
9 | class 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 | ||
22 | class 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 | |
68 | class 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 | |
89 | class 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 | |
109 | class 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 | |
131 | class 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 | |
152 | class 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 | ||
185 | class 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') |