]>
Commit | Line | Data |
---|---|---|
02bbf9eb | 1 | #!/usr/bin/env python |
56d68fad | 2 | import os.path |
02bbf9eb | 3 | |
56d68fad | 4 | import json |
02bbf9eb RG |
5 | import requests |
6 | from dnsdisttests import DNSDistTest | |
7 | ||
56d68fad | 8 | class TestAPIBasics(DNSDistTest): |
02bbf9eb RG |
9 | |
10 | _webTimeout = 2.0 | |
11 | _webServerPort = 8083 | |
12 | _webServerBasicAuthPassword = 'secret' | |
13 | _webServerAPIKey = 'apisecret' | |
55afa518 RG |
14 | # paths accessible using the API key only |
15 | _apiOnlyPaths = ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics'] | |
16 | # paths accessible using an API key or basic auth | |
17 | _statsPaths = [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost'] | |
02bbf9eb RG |
18 | # paths accessible using basic auth only (list not exhaustive) |
19 | _basicOnlyPaths = ['/', '/index.html'] | |
20 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey'] | |
21 | _config_template = """ | |
56d68fad | 22 | setACL({"127.0.0.1/32", "::1/128"}) |
02bbf9eb RG |
23 | newServer{address="127.0.0.1:%s"} |
24 | webserver("127.0.0.1:%s", "%s", "%s") | |
25 | """ | |
26 | ||
27 | def testBasicAuth(self): | |
28 | """ | |
29 | API: Basic Authentication | |
30 | """ | |
55afa518 | 31 | for path in self._basicOnlyPaths + self._statsPaths: |
02bbf9eb RG |
32 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
33 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
34 | self.assertTrue(r) | |
35 | self.assertEquals(r.status_code, 200) | |
36 | ||
37 | def testXAPIKey(self): | |
38 | """ | |
39 | API: X-Api-Key | |
40 | """ | |
41 | headers = {'x-api-key': self._webServerAPIKey} | |
55afa518 | 42 | for path in self._apiOnlyPaths + self._statsPaths: |
02bbf9eb RG |
43 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
44 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
45 | self.assertTrue(r) | |
46 | self.assertEquals(r.status_code, 200) | |
47 | ||
48 | def testBasicAuthOnly(self): | |
49 | """ | |
50 | API: Basic Authentication Only | |
51 | """ | |
52 | headers = {'x-api-key': self._webServerAPIKey} | |
53 | for path in self._basicOnlyPaths: | |
54 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
55 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
56 | self.assertEquals(r.status_code, 401) | |
57 | ||
55afa518 RG |
58 | def testAPIKeyOnly(self): |
59 | """ | |
60 | API: API Key Only | |
61 | """ | |
62 | for path in self._apiOnlyPaths: | |
63 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
64 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
65 | self.assertEquals(r.status_code, 401) | |
66 | ||
02bbf9eb RG |
67 | def testServersLocalhost(self): |
68 | """ | |
69 | API: /api/v1/servers/localhost | |
70 | """ | |
71 | headers = {'x-api-key': self._webServerAPIKey} | |
72 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
73 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
74 | self.assertTrue(r) | |
75 | self.assertEquals(r.status_code, 200) | |
76 | self.assertTrue(r.json()) | |
77 | content = r.json() | |
78 | ||
79 | self.assertEquals(content['daemon_type'], 'dnsdist') | |
80 | ||
46e8b49e | 81 | for key in ['version', 'acl', 'local', 'rules', 'response-rules', 'servers', 'frontends']: |
02bbf9eb RG |
82 | self.assertIn(key, content) |
83 | ||
84 | for rule in content['rules']: | |
85 | for key in ['id', 'matches', 'rule', 'action']: | |
86 | self.assertIn(key, rule) | |
87 | for key in ['id', 'matches']: | |
88 | self.assertTrue(rule[key] >= 0) | |
46e8b49e RG |
89 | |
90 | for rule in content['response-rules']: | |
91 | for key in ['id', 'matches', 'rule', 'action']: | |
92 | self.assertIn(key, rule) | |
93 | for key in ['id', 'matches']: | |
94 | self.assertTrue(rule[key] >= 0) | |
02bbf9eb RG |
95 | |
96 | for server in content['servers']: | |
97 | for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit', | |
98 | 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order']: | |
99 | self.assertIn(key, server) | |
100 | ||
101 | for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds', | |
102 | 'qps', 'queries', 'order']: | |
103 | self.assertTrue(server[key] >= 0) | |
104 | ||
105 | self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN']) | |
106 | ||
107 | for frontend in content['frontends']: | |
108 | for key in ['id', 'address', 'udp', 'tcp', 'queries']: | |
109 | self.assertIn(key, frontend) | |
110 | ||
111 | for key in ['id', 'queries']: | |
112 | self.assertTrue(frontend[key] >= 0) | |
113 | ||
00566cbf PL |
114 | def testServersIDontExist(self): |
115 | """ | |
116 | API: /api/v1/servers/idontexist (should be 404) | |
117 | """ | |
118 | headers = {'x-api-key': self._webServerAPIKey} | |
119 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/idontexist' | |
120 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
121 | self.assertEquals(r.status_code, 404) | |
122 | ||
02bbf9eb RG |
123 | def testServersLocalhostConfig(self): |
124 | """ | |
125 | API: /api/v1/servers/localhost/config | |
126 | """ | |
127 | headers = {'x-api-key': self._webServerAPIKey} | |
128 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config' | |
129 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
130 | self.assertTrue(r) | |
131 | self.assertEquals(r.status_code, 200) | |
132 | self.assertTrue(r.json()) | |
133 | content = r.json() | |
134 | values = {} | |
135 | for entry in content: | |
136 | for key in ['type', 'name', 'value']: | |
137 | self.assertIn(key, entry) | |
138 | ||
139 | self.assertEquals(entry['type'], 'ConfigSetting') | |
140 | values[entry['name']] = entry['value'] | |
141 | ||
142 | for key in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4', | |
143 | 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy', | |
144 | 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout', | |
145 | 'truncate-tc', 'verbose', 'verbose-health-checks']: | |
146 | self.assertIn(key, values) | |
147 | ||
148 | for key in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout', | |
149 | 'tcp-send-timeout']: | |
150 | self.assertTrue(values[key] >= 0) | |
151 | ||
152 | self.assertTrue(values['ecs-source-prefix-v4'] >= 0 and values['ecs-source-prefix-v4'] <= 32) | |
153 | self.assertTrue(values['ecs-source-prefix-v6'] >= 0 and values['ecs-source-prefix-v6'] <= 128) | |
154 | ||
56d68fad RG |
155 | def testServersLocalhostConfigAllowFrom(self): |
156 | """ | |
157 | API: /api/v1/servers/localhost/config/allow-from | |
158 | """ | |
159 | headers = {'x-api-key': self._webServerAPIKey} | |
160 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
161 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
162 | self.assertTrue(r) | |
163 | self.assertEquals(r.status_code, 200) | |
164 | self.assertTrue(r.json()) | |
165 | content = r.json() | |
166 | for key in ['type', 'name', 'value']: | |
167 | self.assertIn(key, content) | |
168 | ||
169 | self.assertEquals(content['name'], 'allow-from') | |
170 | self.assertEquals(content['type'], 'ConfigSetting') | |
171 | self.assertEquals(content['value'], ["127.0.0.1/32", "::1/128"]) | |
172 | ||
173 | def testServersLocalhostConfigAllowFromPut(self): | |
174 | """ | |
175 | API: PUT /api/v1/servers/localhost/config/allow-from (should be refused) | |
176 | ||
177 | The API is read-only by default, so this should be refused | |
178 | """ | |
179 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
180 | payload = json.dumps({"name": "allow-from", | |
181 | "type": "ConfigSetting", | |
182 | "value": newACL}) | |
183 | headers = {'x-api-key': self._webServerAPIKey} | |
184 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
185 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
186 | self.assertFalse(r) | |
187 | self.assertEquals(r.status_code, 405) | |
188 | ||
02bbf9eb RG |
189 | def testServersLocalhostStatistics(self): |
190 | """ | |
191 | API: /api/v1/servers/localhost/statistics | |
192 | """ | |
193 | headers = {'x-api-key': self._webServerAPIKey} | |
194 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/statistics' | |
195 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
196 | self.assertTrue(r) | |
197 | self.assertEquals(r.status_code, 200) | |
198 | self.assertTrue(r.json()) | |
199 | content = r.json() | |
200 | values = {} | |
201 | for entry in content: | |
202 | self.assertIn('type', entry) | |
203 | self.assertIn('name', entry) | |
204 | self.assertIn('value', entry) | |
205 | self.assertEquals(entry['type'], 'StatisticItem') | |
206 | values[entry['name']] = entry['value'] | |
207 | ||
0c369ddc | 208 | expected = ['responses', 'servfail-responses', 'queries', 'acl-drops', |
dd46e5e3 | 209 | 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts', |
02bbf9eb RG |
210 | 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1', |
211 | 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000', | |
212 | 'latency-slow', 'latency-avg100', 'latency-avg1000', 'latency-avg10000', | |
213 | 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries', | |
214 | 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits', | |
215 | 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked', | |
216 | 'dyn-block-nmg-size'] | |
217 | ||
218 | for key in expected: | |
219 | self.assertIn(key, values) | |
220 | self.assertTrue(values[key] >= 0) | |
221 | ||
dd46e5e3 RG |
222 | for key in values: |
223 | self.assertIn(key, expected) | |
224 | ||
02bbf9eb RG |
225 | def testJsonstatStats(self): |
226 | """ | |
227 | API: /jsonstat?command=stats | |
228 | """ | |
229 | headers = {'x-api-key': self._webServerAPIKey} | |
230 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats' | |
231 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
232 | self.assertTrue(r) | |
233 | self.assertEquals(r.status_code, 200) | |
234 | self.assertTrue(r.json()) | |
235 | content = r.json() | |
236 | ||
0c369ddc | 237 | expected = ['responses', 'servfail-responses', 'queries', 'acl-drops', |
dd46e5e3 | 238 | 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts', |
02bbf9eb RG |
239 | 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1', |
240 | 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000', | |
241 | 'latency-slow', 'latency-avg100', 'latency-avg1000', 'latency-avg10000', | |
242 | 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries', | |
243 | 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits', | |
244 | 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked', | |
dd46e5e3 RG |
245 | 'dyn-block-nmg-size', 'packetcache-hits', 'packetcache-misses', 'over-capacity-drops', |
246 | 'too-old-drops'] | |
02bbf9eb RG |
247 | |
248 | for key in expected: | |
249 | self.assertIn(key, content) | |
250 | self.assertTrue(content[key] >= 0) | |
251 | ||
252 | def testJsonstatDynblocklist(self): | |
253 | """ | |
254 | API: /jsonstat?command=dynblocklist | |
255 | """ | |
256 | headers = {'x-api-key': self._webServerAPIKey} | |
257 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=dynblocklist' | |
258 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
259 | self.assertTrue(r) | |
260 | self.assertEquals(r.status_code, 200) | |
261 | ||
262 | content = r.json() | |
263 | ||
264 | if content: | |
265 | for key in ['reason', 'seconds', 'blocks']: | |
266 | self.assertIn(key, content) | |
267 | ||
268 | for key in ['blocks']: | |
269 | self.assertTrue(content[key] >= 0) | |
56d68fad | 270 | |
36927800 RG |
271 | class TestAPIServerDown(DNSDistTest): |
272 | ||
273 | _webTimeout = 2.0 | |
274 | _webServerPort = 8083 | |
275 | _webServerBasicAuthPassword = 'secret' | |
276 | _webServerAPIKey = 'apisecret' | |
277 | # paths accessible using the API key | |
278 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey'] | |
279 | _config_template = """ | |
280 | setACL({"127.0.0.1/32", "::1/128"}) | |
281 | newServer{address="127.0.0.1:%s"} | |
282 | getServer(0):setDown() | |
283 | webserver("127.0.0.1:%s", "%s", "%s") | |
284 | """ | |
285 | ||
286 | def testServerDownNoLatencyLocalhost(self): | |
287 | """ | |
288 | API: /api/v1/servers/localhost, no latency for a down server | |
289 | """ | |
290 | headers = {'x-api-key': self._webServerAPIKey} | |
291 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
292 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
293 | self.assertTrue(r) | |
294 | self.assertEquals(r.status_code, 200) | |
295 | self.assertTrue(r.json()) | |
296 | content = r.json() | |
297 | ||
298 | self.assertEquals(content['servers'][0]['latency'], None) | |
299 | ||
56d68fad RG |
300 | class TestAPIWritable(DNSDistTest): |
301 | ||
302 | _webTimeout = 2.0 | |
303 | _webServerPort = 8083 | |
304 | _webServerBasicAuthPassword = 'secret' | |
305 | _webServerAPIKey = 'apisecret' | |
306 | _APIWriteDir = '/tmp' | |
307 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey', '_APIWriteDir'] | |
308 | _config_template = """ | |
309 | setACL({"127.0.0.1/32", "::1/128"}) | |
310 | newServer{address="127.0.0.1:%s"} | |
311 | webserver("127.0.0.1:%s", "%s", "%s") | |
312 | setAPIWritable(true, "%s") | |
313 | """ | |
314 | ||
315 | def testSetACL(self): | |
316 | """ | |
317 | API: Set ACL | |
318 | """ | |
319 | headers = {'x-api-key': self._webServerAPIKey} | |
320 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
321 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
322 | self.assertTrue(r) | |
323 | self.assertEquals(r.status_code, 200) | |
324 | self.assertTrue(r.json()) | |
325 | content = r.json() | |
326 | self.assertEquals(content['value'], ["127.0.0.1/32", "::1/128"]) | |
327 | ||
328 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
329 | payload = json.dumps({"name": "allow-from", | |
330 | "type": "ConfigSetting", | |
331 | "value": newACL}) | |
332 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
333 | self.assertTrue(r) | |
334 | self.assertEquals(r.status_code, 200) | |
335 | self.assertTrue(r.json()) | |
336 | content = r.json() | |
337 | self.assertEquals(content['value'], newACL) | |
338 | ||
339 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
340 | self.assertTrue(r) | |
341 | self.assertEquals(r.status_code, 200) | |
342 | self.assertTrue(r.json()) | |
343 | content = r.json() | |
344 | self.assertEquals(content['value'], newACL) | |
345 | ||
346 | configFile = self._APIWriteDir + '/' + 'acl.conf' | |
347 | self.assertTrue(os.path.isfile(configFile)) | |
348 | fileContent = None | |
349 | with file(configFile) as f: | |
350 | fileContent = f.read() | |
351 | ||
352 | self.assertEquals(fileContent, """-- Generated by the REST API, DO NOT EDIT | |
353 | setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"}) | |
354 | """) |