]>
Commit | Line | Data |
---|---|---|
9f95a23c | 1 | import unittest |
f67539c2 | 2 | from unittest import mock |
9f95a23c | 3 | |
20effc67 | 4 | from orchestrator import HostSpec |
9f95a23c | 5 | |
9f95a23c | 6 | from .. import mgr |
a4b75251 | 7 | from ..controllers._version import APIVersion |
f67539c2 | 8 | from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories |
20effc67 | 9 | from ..tests import ControllerTestCase, patch_orch |
f67539c2 | 10 | from ..tools import NotificationQueue, TaskManager |
f67539c2 TL |
11 | |
12 | ||
9f95a23c TL |
13 | class HostControllerTest(ControllerTestCase): |
14 | URL_HOST = '/api/host' | |
15 | ||
16 | @classmethod | |
17 | def setup_server(cls): | |
f67539c2 TL |
18 | NotificationQueue.start_queue() |
19 | TaskManager.init() | |
9f95a23c TL |
20 | cls.setup_controllers([Host]) |
21 | ||
f67539c2 TL |
22 | @classmethod |
23 | def tearDownClass(cls): | |
24 | NotificationQueue.stop() | |
25 | ||
9f95a23c | 26 | @mock.patch('dashboard.controllers.host.get_hosts') |
a4b75251 | 27 | def test_host_list_with_sources(self, mock_get_hosts): |
f6b5b4d7 TL |
28 | hosts = [{ |
29 | 'hostname': 'host-0', | |
30 | 'sources': { | |
31 | 'ceph': True, | |
32 | 'orchestrator': False | |
9f95a23c | 33 | } |
f6b5b4d7 TL |
34 | }, { |
35 | 'hostname': 'host-1', | |
36 | 'sources': { | |
37 | 'ceph': False, | |
38 | 'orchestrator': True | |
39 | } | |
40 | }, { | |
41 | 'hostname': 'host-2', | |
42 | 'sources': { | |
43 | 'ceph': True, | |
44 | 'orchestrator': True | |
45 | } | |
46 | }] | |
9f95a23c | 47 | |
a4b75251 TL |
48 | def _get_hosts(sources=None): |
49 | if sources == 'ceph': | |
50 | return hosts[0] | |
51 | if sources == 'orchestrator': | |
52 | return hosts[1:] | |
53 | if sources == 'ceph, orchestrator': | |
54 | return hosts[2] | |
55 | return hosts | |
f6b5b4d7 | 56 | |
9f95a23c TL |
57 | mock_get_hosts.side_effect = _get_hosts |
58 | ||
a4b75251 | 59 | self._get(self.URL_HOST, version=APIVersion(1, 1)) |
9f95a23c TL |
60 | self.assertStatus(200) |
61 | self.assertJsonBody(hosts) | |
62 | ||
a4b75251 | 63 | self._get('{}?sources=ceph'.format(self.URL_HOST), version=APIVersion(1, 1)) |
9f95a23c | 64 | self.assertStatus(200) |
a4b75251 | 65 | self.assertJsonBody(hosts[0]) |
9f95a23c | 66 | |
a4b75251 | 67 | self._get('{}?sources=orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1)) |
9f95a23c TL |
68 | self.assertStatus(200) |
69 | self.assertJsonBody(hosts[1:]) | |
70 | ||
a4b75251 | 71 | self._get('{}?sources=ceph,orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1)) |
9f95a23c TL |
72 | self.assertStatus(200) |
73 | self.assertJsonBody(hosts) | |
74 | ||
a4b75251 TL |
75 | @mock.patch('dashboard.controllers.host.get_hosts') |
76 | def test_host_list_with_facts(self, mock_get_hosts): | |
77 | hosts_without_facts = [{ | |
78 | 'hostname': 'host-0', | |
79 | 'sources': { | |
80 | 'ceph': True, | |
81 | 'orchestrator': False | |
82 | } | |
83 | }, { | |
84 | 'hostname': 'host-1', | |
85 | 'sources': { | |
86 | 'ceph': False, | |
87 | 'orchestrator': True | |
88 | } | |
89 | }] | |
90 | ||
91 | hosts_facts = [{ | |
92 | 'hostname': 'host-0', | |
93 | 'cpu_count': 1, | |
94 | 'memory_total_kb': 1024 | |
95 | }, { | |
96 | 'hostname': 'host-1', | |
97 | 'cpu_count': 2, | |
98 | 'memory_total_kb': 1024 | |
99 | }] | |
100 | ||
101 | hosts_with_facts = [{ | |
102 | 'hostname': 'host-0', | |
103 | 'sources': { | |
104 | 'ceph': True, | |
105 | 'orchestrator': False | |
106 | }, | |
107 | 'cpu_count': 1, | |
108 | 'memory_total_kb': 1024 | |
109 | }, { | |
110 | 'hostname': 'host-1', | |
111 | 'sources': { | |
112 | 'ceph': False, | |
113 | 'orchestrator': True | |
114 | }, | |
115 | 'cpu_count': 2, | |
116 | 'memory_total_kb': 1024 | |
117 | }] | |
118 | # test with orchestrator available | |
119 | with patch_orch(True, hosts=hosts_without_facts) as fake_client: | |
120 | mock_get_hosts.return_value = hosts_without_facts | |
121 | fake_client.hosts.get_facts.return_value = hosts_facts | |
122 | # test with ?facts=true | |
123 | self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 1)) | |
124 | self.assertStatus(200) | |
125 | self.assertHeader('Content-Type', | |
126 | 'application/vnd.ceph.api.v1.1+json') | |
127 | self.assertJsonBody(hosts_with_facts) | |
128 | ||
129 | # test with ?facts=false | |
130 | self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 1)) | |
131 | self.assertStatus(200) | |
132 | self.assertHeader('Content-Type', | |
133 | 'application/vnd.ceph.api.v1.1+json') | |
134 | self.assertJsonBody(hosts_without_facts) | |
135 | ||
136 | # test with orchestrator available but orch backend!=cephadm | |
137 | with patch_orch(True, missing_features=['get_facts']) as fake_client: | |
138 | mock_get_hosts.return_value = hosts_without_facts | |
139 | # test with ?facts=true | |
140 | self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 1)) | |
141 | self.assertStatus(400) | |
142 | ||
143 | # test with no orchestrator available | |
144 | with patch_orch(False): | |
145 | mock_get_hosts.return_value = hosts_without_facts | |
146 | ||
147 | # test with ?facts=true | |
148 | self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 1)) | |
149 | self.assertStatus(400) | |
150 | ||
151 | # test with ?facts=false | |
152 | self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 1)) | |
153 | self.assertStatus(200) | |
154 | self.assertHeader('Content-Type', | |
155 | 'application/vnd.ceph.api.v1.1+json') | |
156 | self.assertJsonBody(hosts_without_facts) | |
157 | ||
f67539c2 | 158 | def test_get_1(self): |
f6b5b4d7 | 159 | mgr.list_servers.return_value = [] |
9f95a23c | 160 | |
f67539c2 TL |
161 | with patch_orch(False): |
162 | self._get('{}/node1'.format(self.URL_HOST)) | |
163 | self.assertStatus(404) | |
f6b5b4d7 | 164 | |
f67539c2 | 165 | def test_get_2(self): |
f6b5b4d7 TL |
166 | mgr.list_servers.return_value = [{'hostname': 'node1'}] |
167 | ||
f67539c2 TL |
168 | with patch_orch(False): |
169 | self._get('{}/node1'.format(self.URL_HOST)) | |
170 | self.assertStatus(200) | |
171 | self.assertIn('labels', self.json_body()) | |
b3b6e05e TL |
172 | self.assertIn('status', self.json_body()) |
173 | self.assertIn('addr', self.json_body()) | |
f6b5b4d7 | 174 | |
f67539c2 | 175 | def test_get_3(self): |
f6b5b4d7 TL |
176 | mgr.list_servers.return_value = [] |
177 | ||
f67539c2 TL |
178 | with patch_orch(True, hosts=[HostSpec('node1')]): |
179 | self._get('{}/node1'.format(self.URL_HOST)) | |
180 | self.assertStatus(200) | |
181 | self.assertIn('labels', self.json_body()) | |
b3b6e05e TL |
182 | self.assertIn('status', self.json_body()) |
183 | self.assertIn('addr', self.json_body()) | |
184 | ||
185 | @mock.patch('dashboard.controllers.host.add_host') | |
186 | def test_add_host(self, mock_add_host): | |
187 | with patch_orch(True): | |
188 | payload = { | |
189 | 'hostname': 'node0', | |
190 | 'addr': '192.0.2.0', | |
191 | 'labels': 'mon', | |
192 | 'status': 'maintenance' | |
193 | } | |
a4b75251 | 194 | self._post(self.URL_HOST, payload, version=APIVersion(0, 1)) |
b3b6e05e TL |
195 | self.assertStatus(201) |
196 | mock_add_host.assert_called() | |
9f95a23c | 197 | |
f67539c2 | 198 | def test_set_labels(self): |
f6b5b4d7 | 199 | mgr.list_servers.return_value = [] |
f67539c2 | 200 | orch_hosts = [ |
f6b5b4d7 TL |
201 | HostSpec('node0', labels=['aaa', 'bbb']) |
202 | ] | |
f67539c2 TL |
203 | with patch_orch(True, hosts=orch_hosts) as fake_client: |
204 | fake_client.hosts.remove_label = mock.Mock() | |
205 | fake_client.hosts.add_label = mock.Mock() | |
206 | ||
207 | payload = {'update_labels': True, 'labels': ['bbb', 'ccc']} | |
a4b75251 | 208 | self._put('{}/node0'.format(self.URL_HOST), payload, version=APIVersion(0, 1)) |
f67539c2 | 209 | self.assertStatus(200) |
b3b6e05e TL |
210 | self.assertHeader('Content-Type', |
211 | 'application/vnd.ceph.api.v0.1+json') | |
f67539c2 TL |
212 | fake_client.hosts.remove_label.assert_called_once_with('node0', 'aaa') |
213 | fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc') | |
214 | ||
215 | # return 400 if type other than List[str] | |
a4b75251 TL |
216 | self._put('{}/node0'.format(self.URL_HOST), |
217 | {'update_labels': True, 'labels': 'ddd'}, | |
218 | version=APIVersion(0, 1)) | |
f67539c2 TL |
219 | self.assertStatus(400) |
220 | ||
221 | def test_host_maintenance(self): | |
222 | mgr.list_servers.return_value = [] | |
223 | orch_hosts = [ | |
224 | HostSpec('node0'), | |
225 | HostSpec('node1') | |
226 | ] | |
227 | with patch_orch(True, hosts=orch_hosts): | |
228 | # enter maintenance mode | |
a4b75251 TL |
229 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, |
230 | version=APIVersion(0, 1)) | |
f67539c2 | 231 | self.assertStatus(200) |
b3b6e05e TL |
232 | self.assertHeader('Content-Type', |
233 | 'application/vnd.ceph.api.v0.1+json') | |
f67539c2 TL |
234 | |
235 | # force enter maintenance mode | |
b3b6e05e | 236 | self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True}, |
a4b75251 | 237 | version=APIVersion(0, 1)) |
f67539c2 TL |
238 | self.assertStatus(200) |
239 | ||
240 | # exit maintenance mode | |
a4b75251 TL |
241 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, |
242 | version=APIVersion(0, 1)) | |
f67539c2 | 243 | self.assertStatus(200) |
a4b75251 TL |
244 | self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}, |
245 | version=APIVersion(0, 1)) | |
f67539c2 TL |
246 | self.assertStatus(200) |
247 | ||
248 | # maintenance without orchestrator service | |
249 | with patch_orch(False): | |
a4b75251 TL |
250 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, |
251 | version=APIVersion(0, 1)) | |
f67539c2 TL |
252 | self.assertStatus(503) |
253 | ||
254 | @mock.patch('dashboard.controllers.host.time') | |
255 | def test_identify_device(self, mock_time): | |
256 | url = '{}/host-0/identify_device'.format(self.URL_HOST) | |
257 | with patch_orch(True) as fake_client: | |
258 | payload = { | |
259 | 'device': '/dev/sdz', | |
260 | 'duration': '1' | |
261 | } | |
262 | self._task_post(url, payload) | |
263 | self.assertStatus(200) | |
264 | mock_time.sleep.assert_called() | |
265 | calls = [ | |
266 | mock.call('host-0', '/dev/sdz', 'ident', True), | |
267 | mock.call('host-0', '/dev/sdz', 'ident', False), | |
268 | ] | |
269 | fake_client.blink_device_light.assert_has_calls(calls) | |
270 | ||
271 | @mock.patch('dashboard.controllers.host.get_inventories') | |
272 | def test_inventory(self, mock_get_inventories): | |
273 | inventory_url = '{}/host-0/inventory'.format(self.URL_HOST) | |
274 | with patch_orch(True): | |
275 | tests = [ | |
276 | { | |
277 | 'url': inventory_url, | |
278 | 'inventories': [{'a': 'b'}], | |
279 | 'refresh': None, | |
280 | 'resp': {'a': 'b'} | |
281 | }, | |
282 | { | |
283 | 'url': '{}?refresh=true'.format(inventory_url), | |
284 | 'inventories': [{'a': 'b'}], | |
285 | 'refresh': "true", | |
286 | 'resp': {'a': 'b'} | |
287 | }, | |
288 | { | |
289 | 'url': inventory_url, | |
290 | 'inventories': [], | |
291 | 'refresh': None, | |
292 | 'resp': {} | |
293 | }, | |
294 | ] | |
295 | for test in tests: | |
296 | mock_get_inventories.reset_mock() | |
297 | mock_get_inventories.return_value = test['inventories'] | |
298 | self._get(test['url']) | |
299 | mock_get_inventories.assert_called_once_with(['host-0'], test['refresh']) | |
300 | self.assertEqual(self.json_body(), test['resp']) | |
301 | self.assertStatus(200) | |
f6b5b4d7 | 302 | |
f67539c2 TL |
303 | # list without orchestrator service |
304 | with patch_orch(False): | |
305 | self._get(inventory_url) | |
306 | self.assertStatus(503) | |
f6b5b4d7 | 307 | |
20effc67 TL |
308 | def test_host_drain(self): |
309 | mgr.list_servers.return_value = [] | |
310 | orch_hosts = [ | |
311 | HostSpec('node0') | |
312 | ] | |
313 | with patch_orch(True, hosts=orch_hosts): | |
314 | self._put('{}/node0'.format(self.URL_HOST), {'drain': True}, | |
315 | version=APIVersion(0, 1)) | |
316 | self.assertStatus(200) | |
317 | self.assertHeader('Content-Type', | |
318 | 'application/vnd.ceph.api.v0.1+json') | |
319 | ||
320 | # maintenance without orchestrator service | |
321 | with patch_orch(False): | |
322 | self._put('{}/node0'.format(self.URL_HOST), {'drain': True}, | |
323 | version=APIVersion(0, 1)) | |
324 | self.assertStatus(503) | |
325 | ||
f6b5b4d7 TL |
326 | |
327 | class HostUiControllerTest(ControllerTestCase): | |
328 | URL_HOST = '/ui-api/host' | |
329 | ||
330 | @classmethod | |
331 | def setup_server(cls): | |
f6b5b4d7 TL |
332 | cls.setup_controllers([HostUi]) |
333 | ||
f67539c2 TL |
334 | def test_labels(self): |
335 | orch_hosts = [ | |
f6b5b4d7 TL |
336 | HostSpec('node1', labels=['foo']), |
337 | HostSpec('node2', labels=['foo', 'bar']) | |
338 | ] | |
f6b5b4d7 | 339 | |
f67539c2 TL |
340 | with patch_orch(True, hosts=orch_hosts): |
341 | self._get('{}/labels'.format(self.URL_HOST)) | |
342 | self.assertStatus(200) | |
343 | labels = self.json_body() | |
344 | labels.sort() | |
345 | self.assertListEqual(labels, ['bar', 'foo']) | |
346 | ||
347 | @mock.patch('dashboard.controllers.host.get_inventories') | |
348 | def test_inventory(self, mock_get_inventories): | |
349 | inventory_url = '{}/inventory'.format(self.URL_HOST) | |
350 | with patch_orch(True): | |
351 | tests = [ | |
352 | { | |
353 | 'url': inventory_url, | |
354 | 'refresh': None | |
355 | }, | |
356 | { | |
357 | 'url': '{}?refresh=true'.format(inventory_url), | |
358 | 'refresh': "true" | |
359 | }, | |
360 | ] | |
361 | for test in tests: | |
362 | mock_get_inventories.reset_mock() | |
363 | mock_get_inventories.return_value = [{'a': 'b'}] | |
364 | self._get(test['url']) | |
365 | mock_get_inventories.assert_called_once_with(None, test['refresh']) | |
366 | self.assertEqual(self.json_body(), [{'a': 'b'}]) | |
367 | self.assertStatus(200) | |
368 | ||
369 | # list without orchestrator service | |
370 | with patch_orch(False): | |
371 | self._get(inventory_url) | |
372 | self.assertStatus(503) | |
f6b5b4d7 TL |
373 | |
374 | ||
375 | class TestHosts(unittest.TestCase): | |
f67539c2 | 376 | def test_get_hosts(self): |
f6b5b4d7 TL |
377 | mgr.list_servers.return_value = [{ |
378 | 'hostname': 'node1' | |
379 | }, { | |
380 | 'hostname': 'localhost' | |
381 | }] | |
f67539c2 | 382 | orch_hosts = [ |
f6b5b4d7 TL |
383 | HostSpec('node1', labels=['foo', 'bar']), |
384 | HostSpec('node2', labels=['bar']) | |
385 | ] | |
9f95a23c | 386 | |
f67539c2 TL |
387 | with patch_orch(True, hosts=orch_hosts): |
388 | hosts = get_hosts() | |
389 | self.assertEqual(len(hosts), 3) | |
390 | checks = { | |
391 | 'localhost': { | |
392 | 'sources': { | |
393 | 'ceph': True, | |
394 | 'orchestrator': False | |
395 | }, | |
396 | 'labels': [] | |
f6b5b4d7 | 397 | }, |
f67539c2 TL |
398 | 'node1': { |
399 | 'sources': { | |
400 | 'ceph': True, | |
401 | 'orchestrator': True | |
402 | }, | |
403 | 'labels': ['bar', 'foo'] | |
404 | }, | |
405 | 'node2': { | |
406 | 'sources': { | |
407 | 'ceph': False, | |
408 | 'orchestrator': True | |
409 | }, | |
410 | 'labels': ['bar'] | |
411 | } | |
412 | } | |
413 | for host in hosts: | |
414 | hostname = host['hostname'] | |
415 | self.assertDictEqual(host['sources'], checks[hostname]['sources']) | |
416 | self.assertListEqual(host['labels'], checks[hostname]['labels']) | |
417 | ||
418 | @mock.patch('dashboard.controllers.host.mgr.get') | |
419 | def test_get_device_osd_map(self, mgr_get): | |
420 | mgr_get.side_effect = lambda key: { | |
421 | 'osd_metadata': { | |
422 | '0': { | |
423 | 'hostname': 'node0', | |
424 | 'devices': 'nvme0n1,sdb', | |
425 | }, | |
426 | '1': { | |
427 | 'hostname': 'node0', | |
428 | 'devices': 'nvme0n1,sdc', | |
429 | }, | |
430 | '2': { | |
431 | 'hostname': 'node1', | |
432 | 'devices': 'sda', | |
433 | }, | |
434 | '3': { | |
435 | 'hostname': 'node2', | |
436 | 'devices': '', | |
437 | } | |
438 | } | |
439 | }[key] | |
440 | ||
441 | device_osd_map = get_device_osd_map() | |
442 | mgr.get.assert_called_with('osd_metadata') | |
443 | # sort OSD IDs to make assertDictEqual work | |
444 | for devices in device_osd_map.values(): | |
445 | for host in devices.keys(): | |
446 | devices[host] = sorted(devices[host]) | |
447 | self.assertDictEqual(device_osd_map, { | |
448 | 'node0': { | |
449 | 'nvme0n1': [0, 1], | |
450 | 'sdb': [0], | |
451 | 'sdc': [1], | |
f6b5b4d7 TL |
452 | }, |
453 | 'node1': { | |
f67539c2 TL |
454 | 'sda': [2] |
455 | } | |
456 | }) | |
457 | ||
458 | @mock.patch('dashboard.controllers.host.str_to_bool') | |
459 | @mock.patch('dashboard.controllers.host.get_device_osd_map') | |
460 | def test_get_inventories(self, mock_get_device_osd_map, mock_str_to_bool): | |
461 | mock_get_device_osd_map.return_value = { | |
462 | 'host-0': { | |
463 | 'nvme0n1': [1, 2], | |
464 | 'sdb': [1], | |
465 | 'sdc': [2] | |
f6b5b4d7 | 466 | }, |
f67539c2 TL |
467 | 'host-1': { |
468 | 'sdb': [3] | |
f6b5b4d7 | 469 | } |
9f95a23c | 470 | } |
f67539c2 TL |
471 | inventory = [ |
472 | { | |
473 | 'name': 'host-0', | |
474 | 'addr': '1.2.3.4', | |
475 | 'devices': [ | |
476 | {'path': 'nvme0n1'}, | |
477 | {'path': '/dev/sdb'}, | |
478 | {'path': '/dev/sdc'}, | |
479 | ] | |
480 | }, | |
481 | { | |
482 | 'name': 'host-1', | |
483 | 'addr': '1.2.3.5', | |
484 | 'devices': [ | |
485 | {'path': '/dev/sda'}, | |
486 | {'path': 'sdb'}, | |
487 | ] | |
488 | } | |
489 | ] | |
490 | ||
491 | with patch_orch(True, inventory=inventory) as orch_client: | |
492 | mock_str_to_bool.return_value = True | |
493 | ||
494 | hosts = ['host-0', 'host-1'] | |
495 | inventories = get_inventories(hosts, 'true') | |
496 | mock_str_to_bool.assert_called_with('true') | |
497 | orch_client.inventory.list.assert_called_once_with(hosts=hosts, refresh=True) | |
498 | self.assertEqual(len(inventories), 2) | |
499 | host0 = inventories[0] | |
500 | self.assertEqual(host0['name'], 'host-0') | |
501 | self.assertEqual(host0['addr'], '1.2.3.4') | |
502 | self.assertEqual(host0['devices'][0]['osd_ids'], [1, 2]) | |
503 | self.assertEqual(host0['devices'][1]['osd_ids'], [1]) | |
504 | self.assertEqual(host0['devices'][2]['osd_ids'], [2]) | |
505 | host1 = inventories[1] | |
506 | self.assertEqual(host1['name'], 'host-1') | |
507 | self.assertEqual(host1['addr'], '1.2.3.5') | |
508 | self.assertEqual(host1['devices'][0]['osd_ids'], []) | |
509 | self.assertEqual(host1['devices'][1]['osd_ids'], [3]) |