8 from dnsdisttests
import DNSDistTest
, pickAvailablePort
10 class HealthCheckTest(DNSDistTest
):
11 _consoleKey
= DNSDistTest
.generateConsoleKey()
12 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
14 _webServerPort
= pickAvailablePort()
15 _webServerAPIKey
= 'apisecret'
16 _webServerAPIKeyHashed
= '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso='
17 _config_params
= ['_consoleKeyB64', '_consolePort', '_webServerPort', '_webServerAPIKeyHashed', '_testServerPort']
18 _config_template
= """
20 controlSocket("127.0.0.1:%d")
21 webserver("127.0.0.1:%s")
22 setWebserverConfig({apiKey="%s"})
23 newServer{address="127.0.0.1:%d"}
26 def getBackendStatus(self
):
27 return self
.sendConsoleCommand("if getServer(0):isUp() then return 'up' else return 'down' end").strip("\n")
29 def getBackendMetric(self
, backendID
, metricName
):
30 headers
= {'x-api-key': self
._webServerAPIKey
}
31 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
32 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
34 self
.assertEqual(r
.status_code
, 200)
35 self
.assertTrue(r
.json())
37 self
.assertIn('servers', content
)
38 servers
= content
['servers']
39 server
= servers
[backendID
]
40 return int(server
[metricName
])
42 class TestDefaultHealthCheck(HealthCheckTest
):
43 # this test suite uses a different responder port
44 # because we need fresh counters
45 _testServerPort
= pickAvailablePort()
47 def testDefault(self
):
51 before
= TestDefaultHealthCheck
._healthCheckCounter
53 self
.assertGreater(TestDefaultHealthCheck
._healthCheckCounter
, before
)
54 self
.assertEqual(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
55 self
.assertEqual(self
.getBackendStatus(), 'up')
57 self
.sendConsoleCommand("getServer(0):setUp()")
58 self
.assertEqual(self
.getBackendStatus(), 'up')
60 before
= TestDefaultHealthCheck
._healthCheckCounter
62 self
.assertEqual(TestDefaultHealthCheck
._healthCheckCounter
, before
)
64 self
.sendConsoleCommand("getServer(0):setDown()")
65 self
.assertEqual(self
.getBackendStatus(), 'down')
67 before
= TestDefaultHealthCheck
._healthCheckCounter
69 self
.assertEqual(TestDefaultHealthCheck
._healthCheckCounter
, before
)
71 self
.sendConsoleCommand("getServer(0):setAuto()")
72 # we get back the previous state, which was up
73 self
.assertEqual(self
.getBackendStatus(), 'up')
75 before
= TestDefaultHealthCheck
._healthCheckCounter
77 self
.assertGreater(TestDefaultHealthCheck
._healthCheckCounter
, before
)
78 self
.assertEqual(self
.getBackendStatus(), 'up')
80 self
.sendConsoleCommand("getServer(0):setDown()")
81 self
.assertEqual(self
.getBackendStatus(), 'down')
82 self
.sendConsoleCommand("getServer(0):setAuto(false)")
84 before
= TestDefaultHealthCheck
._healthCheckCounter
86 self
.assertGreater(TestDefaultHealthCheck
._healthCheckCounter
, before
)
87 self
.assertEqual(self
.getBackendStatus(), 'up')
88 self
.assertEqual(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
90 class TestHealthCheckForcedUP(HealthCheckTest
):
91 # this test suite uses a different responder port
92 # because we need fresh counters
93 _testServerPort
= pickAvailablePort()
95 _config_template
= """
97 controlSocket("127.0.0.1:%d")
98 webserver("127.0.0.1:%s")
99 setWebserverConfig({apiKey="%s"})
100 srv = newServer{address="127.0.0.1:%d"}
104 def testForcedUp(self
):
106 HealthChecks: Forced UP
108 before
= TestHealthCheckForcedUP
._healthCheckCounter
110 self
.assertEqual(TestHealthCheckForcedUP
._healthCheckCounter
, before
)
111 self
.assertEqual(self
.getBackendStatus(), 'up')
112 self
.assertEqual(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
114 class TestHealthCheckForcedDown(HealthCheckTest
):
115 # this test suite uses a different responder port
116 # because we need fresh counters
117 _testServerPort
= pickAvailablePort()
119 _config_template
= """
121 controlSocket("127.0.0.1:%d")
122 webserver("127.0.0.1:%s")
123 setWebserverConfig({apiKey="%s"})
124 srv = newServer{address="127.0.0.1:%d"}
128 def testForcedDown(self
):
130 HealthChecks: Forced Down
132 before
= TestHealthCheckForcedDown
._healthCheckCounter
134 self
.assertEqual(TestHealthCheckForcedDown
._healthCheckCounter
, before
)
135 self
.assertEqual(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
137 class TestHealthCheckCustomName(HealthCheckTest
):
138 # this test suite uses a different responder port
139 # because it uses a different health check name
140 _testServerPort
= pickAvailablePort()
142 _healthCheckName
= 'powerdns.com.'
143 _config_params
= ['_consoleKeyB64', '_consolePort', '_webServerPort', '_webServerAPIKeyHashed', '_testServerPort', '_healthCheckName']
144 _config_template
= """
146 controlSocket("127.0.0.1:%d")
147 webserver("127.0.0.1:%s")
148 setWebserverConfig({apiKey="%s"})
149 srv = newServer{address="127.0.0.1:%d", checkName='%s'}
154 HealthChecks: Custom name
156 before
= TestHealthCheckCustomName
._healthCheckCounter
158 self
.assertGreater(TestHealthCheckCustomName
._healthCheckCounter
, before
)
159 self
.assertEqual(self
.getBackendStatus(), 'up')
160 self
.assertEqual(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
162 class TestHealthCheckCustomNameNoAnswer(HealthCheckTest
):
163 # this test suite uses a different responder port
164 # because it uses a different health check configuration
165 _testServerPort
= pickAvailablePort()
167 _answerUnexpected
= False
168 _config_template
= """
170 controlSocket("127.0.0.1:%d")
171 webserver("127.0.0.1:%s")
172 setWebserverConfig({apiKey="%s"})
173 srv = newServer{address="127.0.0.1:%d", checkName='powerdns.com.'}
178 HealthChecks: Custom name not expected by the responder
180 before
= TestHealthCheckCustomNameNoAnswer
._healthCheckCounter
182 self
.assertEqual(TestHealthCheckCustomNameNoAnswer
._healthCheckCounter
, before
)
183 self
.assertEqual(self
.getBackendStatus(), 'down')
184 self
.assertGreater(self
.getBackendMetric(0, 'healthCheckFailures'), 0)
185 self
.assertGreater(self
.getBackendMetric(0, 'healthCheckFailuresTimeout'), 0)
187 class TestHealthCheckCustomFunction(HealthCheckTest
):
188 # this test suite uses a different responder port
189 # because it uses a different health check configuration
190 _testServerPort
= pickAvailablePort()
191 _answerUnexpected
= False
193 _healthCheckName
= 'powerdns.com.'
194 _config_template
= """
196 controlSocket("127.0.0.1:%d")
197 webserver("127.0.0.1:%s")
198 setWebserverConfig({apiKey="%s"})
200 function myHealthCheckFunction(qname, qtype, qclass, dh)
203 return newDNSName('powerdns.com.'), DNSQType.AAAA, qclass
206 srv = newServer{address="127.0.0.1:%d", checkName='powerdns.org.', checkFunction=myHealthCheckFunction}
211 HealthChecks: Custom function
213 before
= TestHealthCheckCustomFunction
._healthCheckCounter
215 self
.assertGreater(TestHealthCheckCustomFunction
._healthCheckCounter
, before
)
216 self
.assertEqual(self
.getBackendStatus(), 'up')
218 _do53HealthCheckQueries
= 0
219 _dotHealthCheckQueries
= 0
220 _dohHealthCheckQueries
= 0
222 class TestLazyHealthChecks(HealthCheckTest
):
223 _extraStartupSleep
= 1
224 _do53Port
= pickAvailablePort()
225 _dotPort
= pickAvailablePort()
226 _dohPort
= pickAvailablePort()
228 _consoleKey
= DNSDistTest
.generateConsoleKey()
229 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
230 _config_params
= ['_consoleKeyB64', '_consolePort', '_do53Port', '_dotPort', '_dohPort']
231 _config_template
= """
233 controlSocket("127.0.0.1:%d")
235 newServer{address="127.0.0.1:%s", healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=1, lazyHealthCheckThreshold=10, lazyHealthCheckSampleSize=100, lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOrServFail', pool=''}
237 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'}
238 addAction('dot.lazy.test.powerdns.com.', PoolAction('dot'))
240 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'}
241 addAction('doh.lazy.test.powerdns.com.', PoolAction('doh'))
246 def HandleDNSQuery(request
):
247 response
= dns
.message
.make_response(request
)
248 if str(request
.question
[0].name
).startswith('server-failure'):
249 response
.set_rcode(dns
.rcode
.SERVFAIL
)
250 return response
.to_wire()
253 def Do53Callback(cls
, request
):
254 global _do53HealthCheckQueries
255 if str(request
.question
[0].name
).startswith('a.root-servers.net'):
256 _do53HealthCheckQueries
= _do53HealthCheckQueries
+ 1
257 response
= dns
.message
.make_response(request
)
258 return response
.to_wire()
259 return cls
.HandleDNSQuery(request
)
262 def DoTCallback(cls
, request
):
263 global _dotHealthCheckQueries
264 if str(request
.question
[0].name
).startswith('a.root-servers.net'):
265 _dotHealthCheckQueries
= _dotHealthCheckQueries
+ 1
266 response
= dns
.message
.make_response(request
)
267 return response
.to_wire()
268 return cls
.HandleDNSQuery(request
)
271 def DoHCallback(cls
, request
, requestHeaders
, fromQueue
, toQueue
):
272 global _dohHealthCheckQueries
273 if str(request
.question
[0].name
).startswith('a.root-servers.net'):
274 _dohHealthCheckQueries
= _dohHealthCheckQueries
+ 1
275 response
= dns
.message
.make_response(request
)
276 return 200, response
.to_wire()
277 return 200, cls
.HandleDNSQuery(request
)
280 def startResponders(cls
):
281 tlsContext
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
282 tlsContext
.load_cert_chain('server.chain', 'server.key')
284 Do53Responder
= threading
.Thread(name
='Do53 Lazy Responder', target
=cls
.UDPResponder
, args
=[cls
._do
53Port
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, cls
.Do53Callback
])
285 Do53Responder
.daemon
= True
286 Do53Responder
.start()
288 Do53TCPResponder
= threading
.Thread(name
='Do53 TCP Lazy Responder', target
=cls
.TCPResponder
, args
=[cls
._do
53Port
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.Do53Callback
])
289 Do53TCPResponder
.daemon
= True
290 Do53TCPResponder
.start()
292 DoTResponder
= threading
.Thread(name
='DoT Lazy Responder', target
=cls
.TCPResponder
, args
=[cls
._dotPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.DoTCallback
, tlsContext
])
293 DoTResponder
.daemon
= True
296 DoHResponder
= threading
.Thread(name
='DoH Lazy Responder', target
=cls
.DOHResponder
, args
=[cls
._dohPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.DoHCallback
, tlsContext
])
297 DoHResponder
.daemon
= True
300 def testDo53Lazy(self
):
302 Lazy Healthchecks: Do53
304 # there is one initial query on startup
305 self
.assertEqual(_do53HealthCheckQueries
, 1)
307 self
.assertEqual(_do53HealthCheckQueries
, 1)
309 name
= 'do53.lazy.test.powerdns.com.'
310 query
= dns
.message
.make_query(name
, 'A', 'IN')
311 response
= dns
.message
.make_response(query
)
312 failedQuery
= dns
.message
.make_query('server-failure.do53.lazy.test.powerdns.com.', 'A', 'IN')
313 failedResponse
= dns
.message
.make_response(failedQuery
)
314 failedResponse
.set_rcode(dns
.rcode
.SERVFAIL
)
316 # send a few valid queries
318 for method
in ("sendUDPQuery", "sendTCPQuery"):
319 sender
= getattr(self
, method
)
320 (_
, receivedResponse
) = sender(query
, response
=None, useQueue
=False)
321 self
.assertEqual(receivedResponse
, response
)
323 self
.assertEqual(_do53HealthCheckQueries
, 1)
325 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
327 (_
, receivedResponse
) = self
.sendUDPQuery(failedQuery
, response
=None, useQueue
=False)
328 self
.assertEqual(receivedResponse
, failedResponse
)
331 self
.assertEqual(_do53HealthCheckQueries
, 2)
332 self
.assertEqual(self
.getBackendStatus(), 'up')
334 def testDoTLazy(self
):
336 Lazy Healthchecks: DoT
338 # there is one initial query on startup
339 self
.assertEqual(_dotHealthCheckQueries
, 1)
341 self
.assertEqual(_dotHealthCheckQueries
, 1)
343 name
= 'dot.lazy.test.powerdns.com.'
344 query
= dns
.message
.make_query(name
, 'A', 'IN')
345 response
= dns
.message
.make_response(query
)
346 failedQuery
= dns
.message
.make_query('server-failure.dot.lazy.test.powerdns.com.', 'A', 'IN')
347 failedResponse
= dns
.message
.make_response(failedQuery
)
348 failedResponse
.set_rcode(dns
.rcode
.SERVFAIL
)
350 # send a few valid queries
352 for method
in ("sendUDPQuery", "sendTCPQuery"):
353 sender
= getattr(self
, method
)
354 (_
, receivedResponse
) = sender(query
, response
=None, useQueue
=False)
355 self
.assertEqual(receivedResponse
, response
)
357 self
.assertEqual(_dotHealthCheckQueries
, 1)
359 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
361 (_
, receivedResponse
) = self
.sendUDPQuery(failedQuery
, response
=None, useQueue
=False)
362 self
.assertEqual(receivedResponse
, failedResponse
)
365 self
.assertEqual(_dotHealthCheckQueries
, 2)
366 self
.assertEqual(self
.getBackendStatus(), 'up')
368 def testDoHLazy(self
):
370 Lazy Healthchecks: DoH
372 # there is one initial query on startup
373 self
.assertEqual(_dohHealthCheckQueries
, 1)
375 self
.assertEqual(_dohHealthCheckQueries
, 1)
377 name
= 'doh.lazy.test.powerdns.com.'
378 query
= dns
.message
.make_query(name
, 'A', 'IN')
379 response
= dns
.message
.make_response(query
)
380 failedQuery
= dns
.message
.make_query('server-failure.doh.lazy.test.powerdns.com.', 'A', 'IN')
381 failedResponse
= dns
.message
.make_response(failedQuery
)
382 failedResponse
.set_rcode(dns
.rcode
.SERVFAIL
)
384 # send a few valid queries
386 for method
in ("sendUDPQuery", "sendTCPQuery"):
387 sender
= getattr(self
, method
)
388 (_
, receivedResponse
) = sender(query
, response
=None, useQueue
=False)
389 self
.assertEqual(receivedResponse
, response
)
391 self
.assertEqual(_dohHealthCheckQueries
, 1)
393 # we need at least 10 samples, and 10 percent of them failing, so two failing queries should be enough
395 (_
, receivedResponse
) = self
.sendUDPQuery(failedQuery
, response
=None, useQueue
=False)
396 self
.assertEqual(receivedResponse
, failedResponse
)
399 self
.assertEqual(_dohHealthCheckQueries
, 2)
400 self
.assertEqual(self
.getBackendStatus(), 'up')