]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/tests/test_host.py
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / tests / test_host.py
CommitLineData
9f95a23c 1import unittest
f67539c2 2from unittest import mock
9f95a23c 3
39ae355f 4from orchestrator import DaemonDescription, HostSpec
9f95a23c 5
9f95a23c 6from .. import mgr
a4b75251 7from ..controllers._version import APIVersion
f67539c2 8from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories
20effc67 9from ..tests import ControllerTestCase, patch_orch
f67539c2 10from ..tools import NotificationQueue, TaskManager
f67539c2
TL
11
12
9f95a23c
TL
13class 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':
aee94f69 50 return [hosts[0]]
a4b75251
TL
51 if sources == 'orchestrator':
52 return hosts[1:]
53 if sources == 'ceph, orchestrator':
aee94f69 54 return [hosts[2]]
a4b75251 55 return hosts
f6b5b4d7 56
aee94f69
TL
57 with patch_orch(True, hosts=hosts):
58 mock_get_hosts.side_effect = _get_hosts
59 self._get(self.URL_HOST, version=APIVersion(1, 1))
60 self.assertStatus(200)
61 self.assertJsonBody(hosts)
9f95a23c 62
aee94f69
TL
63 self._get('{}?sources=ceph'.format(self.URL_HOST), version=APIVersion(1, 1))
64 self.assertStatus(200)
65 self.assertJsonBody([hosts[0]])
9f95a23c 66
aee94f69
TL
67 self._get('{}?sources=orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1))
68 self.assertStatus(200)
69 self.assertJsonBody(hosts[1:])
9f95a23c 70
aee94f69
TL
71 self._get('{}?sources=ceph,orchestrator'.format(self.URL_HOST),
72 version=APIVersion(1, 1))
73 self.assertStatus(200)
74 self.assertJsonBody(hosts)
9f95a23c 75
a4b75251
TL
76 @mock.patch('dashboard.controllers.host.get_hosts')
77 def test_host_list_with_facts(self, mock_get_hosts):
78 hosts_without_facts = [{
79 'hostname': 'host-0',
80 'sources': {
81 'ceph': True,
82 'orchestrator': False
83 }
84 }, {
85 'hostname': 'host-1',
86 'sources': {
87 'ceph': False,
88 'orchestrator': True
89 }
90 }]
91
92 hosts_facts = [{
93 'hostname': 'host-0',
94 'cpu_count': 1,
95 'memory_total_kb': 1024
96 }, {
97 'hostname': 'host-1',
98 'cpu_count': 2,
99 'memory_total_kb': 1024
100 }]
101
102 hosts_with_facts = [{
103 'hostname': 'host-0',
104 'sources': {
105 'ceph': True,
106 'orchestrator': False
107 },
108 'cpu_count': 1,
aee94f69
TL
109 'memory_total_kb': 1024,
110 'services': [],
111 'service_instances': [{'type': 'mon', 'count': 1}]
a4b75251
TL
112 }, {
113 'hostname': 'host-1',
114 'sources': {
115 'ceph': False,
116 'orchestrator': True
117 },
118 'cpu_count': 2,
aee94f69
TL
119 'memory_total_kb': 1024,
120 'services': [],
121 'service_instances': [{'type': 'mon', 'count': 1}]
a4b75251
TL
122 }]
123 # test with orchestrator available
124 with patch_orch(True, hosts=hosts_without_facts) as fake_client:
125 mock_get_hosts.return_value = hosts_without_facts
aee94f69
TL
126
127 def get_facts_mock(hostname: str):
128 if hostname == 'host-0':
129 return [hosts_facts[0]]
130 return [hosts_facts[1]]
131 fake_client.hosts.get_facts.side_effect = get_facts_mock
a4b75251 132 # test with ?facts=true
aee94f69 133 self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
a4b75251
TL
134 self.assertStatus(200)
135 self.assertHeader('Content-Type',
aee94f69 136 APIVersion(1, 3).to_mime_type())
a4b75251
TL
137 self.assertJsonBody(hosts_with_facts)
138
139 # test with ?facts=false
aee94f69 140 self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 3))
a4b75251
TL
141 self.assertStatus(200)
142 self.assertHeader('Content-Type',
aee94f69 143 APIVersion(1, 3).to_mime_type())
a4b75251
TL
144 self.assertJsonBody(hosts_without_facts)
145
146 # test with orchestrator available but orch backend!=cephadm
147 with patch_orch(True, missing_features=['get_facts']) as fake_client:
148 mock_get_hosts.return_value = hosts_without_facts
149 # test with ?facts=true
aee94f69 150 self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
a4b75251
TL
151 self.assertStatus(400)
152
153 # test with no orchestrator available
154 with patch_orch(False):
155 mock_get_hosts.return_value = hosts_without_facts
156
157 # test with ?facts=true
aee94f69 158 self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 3))
a4b75251
TL
159 self.assertStatus(400)
160
161 # test with ?facts=false
aee94f69 162 self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 3))
a4b75251
TL
163 self.assertStatus(200)
164 self.assertHeader('Content-Type',
aee94f69 165 APIVersion(1, 3).to_mime_type())
a4b75251
TL
166 self.assertJsonBody(hosts_without_facts)
167
f67539c2 168 def test_get_1(self):
f6b5b4d7 169 mgr.list_servers.return_value = []
9f95a23c 170
f67539c2
TL
171 with patch_orch(False):
172 self._get('{}/node1'.format(self.URL_HOST))
173 self.assertStatus(404)
f6b5b4d7 174
f67539c2 175 def test_get_2(self):
39ae355f
TL
176 mgr.list_servers.return_value = [{
177 'hostname': 'node1',
178 'services': []
179 }]
f6b5b4d7 180
f67539c2
TL
181 with patch_orch(False):
182 self._get('{}/node1'.format(self.URL_HOST))
183 self.assertStatus(200)
184 self.assertIn('labels', self.json_body())
b3b6e05e
TL
185 self.assertIn('status', self.json_body())
186 self.assertIn('addr', self.json_body())
f6b5b4d7 187
f67539c2 188 def test_get_3(self):
f6b5b4d7
TL
189 mgr.list_servers.return_value = []
190
f67539c2
TL
191 with patch_orch(True, hosts=[HostSpec('node1')]):
192 self._get('{}/node1'.format(self.URL_HOST))
193 self.assertStatus(200)
194 self.assertIn('labels', self.json_body())
b3b6e05e
TL
195 self.assertIn('status', self.json_body())
196 self.assertIn('addr', self.json_body())
197
39ae355f
TL
198 def test_populate_service_instances(self):
199 mgr.list_servers.return_value = []
200
201 node1_daemons = [
202 DaemonDescription(
203 hostname='node1',
204 daemon_type='mon',
205 daemon_id='a'
206 ),
207 DaemonDescription(
208 hostname='node1',
209 daemon_type='mon',
210 daemon_id='b'
211 )
212 ]
213
214 node2_daemons = [
215 DaemonDescription(
216 hostname='node2',
217 daemon_type='mgr',
218 daemon_id='x'
219 ),
220 DaemonDescription(
221 hostname='node2',
222 daemon_type='mon',
223 daemon_id='c'
224 )
225 ]
226
227 node1_instances = [{
228 'type': 'mon',
229 'count': 2
230 }]
231
232 node2_instances = [{
233 'type': 'mgr',
234 'count': 1
235 }, {
236 'type': 'mon',
237 'count': 1
238 }]
239
240 # test with orchestrator available
241 with patch_orch(True,
242 hosts=[HostSpec('node1'), HostSpec('node2')]) as fake_client:
243 fake_client.services.list_daemons.return_value = node1_daemons
244 self._get('{}/node1'.format(self.URL_HOST))
245 self.assertStatus(200)
246 self.assertIn('service_instances', self.json_body())
247 self.assertEqual(self.json_body()['service_instances'], node1_instances)
248
249 fake_client.services.list_daemons.return_value = node2_daemons
250 self._get('{}/node2'.format(self.URL_HOST))
251 self.assertStatus(200)
252 self.assertIn('service_instances', self.json_body())
253 self.assertEqual(self.json_body()['service_instances'], node2_instances)
254
255 # test with no orchestrator available
256 with patch_orch(False):
257 mgr.list_servers.return_value = [{
258 'hostname': 'node1',
259 'services': [{
260 'type': 'mon',
261 'id': 'a'
262 }, {
263 'type': 'mgr',
264 'id': 'b'
265 }]
266 }]
267 self._get('{}/node1'.format(self.URL_HOST))
268 self.assertStatus(200)
269 self.assertIn('service_instances', self.json_body())
270 self.assertEqual(self.json_body()['service_instances'],
271 [{
272 'type': 'mon',
273 'count': 1
274 }, {
275 'type': 'mgr',
276 'count': 1
277 }])
278
b3b6e05e
TL
279 @mock.patch('dashboard.controllers.host.add_host')
280 def test_add_host(self, mock_add_host):
281 with patch_orch(True):
282 payload = {
283 'hostname': 'node0',
284 'addr': '192.0.2.0',
285 'labels': 'mon',
286 'status': 'maintenance'
287 }
a4b75251 288 self._post(self.URL_HOST, payload, version=APIVersion(0, 1))
b3b6e05e
TL
289 self.assertStatus(201)
290 mock_add_host.assert_called()
9f95a23c 291
f67539c2 292 def test_set_labels(self):
f6b5b4d7 293 mgr.list_servers.return_value = []
f67539c2 294 orch_hosts = [
f6b5b4d7
TL
295 HostSpec('node0', labels=['aaa', 'bbb'])
296 ]
f67539c2
TL
297 with patch_orch(True, hosts=orch_hosts) as fake_client:
298 fake_client.hosts.remove_label = mock.Mock()
299 fake_client.hosts.add_label = mock.Mock()
300
301 payload = {'update_labels': True, 'labels': ['bbb', 'ccc']}
a4b75251 302 self._put('{}/node0'.format(self.URL_HOST), payload, version=APIVersion(0, 1))
f67539c2 303 self.assertStatus(200)
b3b6e05e
TL
304 self.assertHeader('Content-Type',
305 'application/vnd.ceph.api.v0.1+json')
f67539c2
TL
306 fake_client.hosts.remove_label.assert_called_once_with('node0', 'aaa')
307 fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc')
308
309 # return 400 if type other than List[str]
a4b75251
TL
310 self._put('{}/node0'.format(self.URL_HOST),
311 {'update_labels': True, 'labels': 'ddd'},
312 version=APIVersion(0, 1))
f67539c2
TL
313 self.assertStatus(400)
314
315 def test_host_maintenance(self):
316 mgr.list_servers.return_value = []
317 orch_hosts = [
318 HostSpec('node0'),
319 HostSpec('node1')
320 ]
321 with patch_orch(True, hosts=orch_hosts):
322 # enter maintenance mode
a4b75251
TL
323 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
324 version=APIVersion(0, 1))
f67539c2 325 self.assertStatus(200)
b3b6e05e
TL
326 self.assertHeader('Content-Type',
327 'application/vnd.ceph.api.v0.1+json')
f67539c2
TL
328
329 # force enter maintenance mode
b3b6e05e 330 self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True},
a4b75251 331 version=APIVersion(0, 1))
f67539c2
TL
332 self.assertStatus(200)
333
334 # exit maintenance mode
a4b75251
TL
335 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
336 version=APIVersion(0, 1))
f67539c2 337 self.assertStatus(200)
a4b75251
TL
338 self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True},
339 version=APIVersion(0, 1))
f67539c2
TL
340 self.assertStatus(200)
341
342 # maintenance without orchestrator service
343 with patch_orch(False):
a4b75251
TL
344 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
345 version=APIVersion(0, 1))
f67539c2
TL
346 self.assertStatus(503)
347
348 @mock.patch('dashboard.controllers.host.time')
349 def test_identify_device(self, mock_time):
350 url = '{}/host-0/identify_device'.format(self.URL_HOST)
351 with patch_orch(True) as fake_client:
352 payload = {
353 'device': '/dev/sdz',
354 'duration': '1'
355 }
356 self._task_post(url, payload)
357 self.assertStatus(200)
358 mock_time.sleep.assert_called()
359 calls = [
360 mock.call('host-0', '/dev/sdz', 'ident', True),
361 mock.call('host-0', '/dev/sdz', 'ident', False),
362 ]
363 fake_client.blink_device_light.assert_has_calls(calls)
364
365 @mock.patch('dashboard.controllers.host.get_inventories')
366 def test_inventory(self, mock_get_inventories):
367 inventory_url = '{}/host-0/inventory'.format(self.URL_HOST)
368 with patch_orch(True):
369 tests = [
370 {
371 'url': inventory_url,
372 'inventories': [{'a': 'b'}],
373 'refresh': None,
374 'resp': {'a': 'b'}
375 },
376 {
377 'url': '{}?refresh=true'.format(inventory_url),
378 'inventories': [{'a': 'b'}],
379 'refresh': "true",
380 'resp': {'a': 'b'}
381 },
382 {
383 'url': inventory_url,
384 'inventories': [],
385 'refresh': None,
386 'resp': {}
387 },
388 ]
389 for test in tests:
390 mock_get_inventories.reset_mock()
391 mock_get_inventories.return_value = test['inventories']
392 self._get(test['url'])
393 mock_get_inventories.assert_called_once_with(['host-0'], test['refresh'])
394 self.assertEqual(self.json_body(), test['resp'])
395 self.assertStatus(200)
f6b5b4d7 396
f67539c2
TL
397 # list without orchestrator service
398 with patch_orch(False):
399 self._get(inventory_url)
400 self.assertStatus(503)
f6b5b4d7 401
20effc67
TL
402 def test_host_drain(self):
403 mgr.list_servers.return_value = []
404 orch_hosts = [
405 HostSpec('node0')
406 ]
407 with patch_orch(True, hosts=orch_hosts):
408 self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
409 version=APIVersion(0, 1))
410 self.assertStatus(200)
411 self.assertHeader('Content-Type',
412 'application/vnd.ceph.api.v0.1+json')
413
414 # maintenance without orchestrator service
415 with patch_orch(False):
416 self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
417 version=APIVersion(0, 1))
418 self.assertStatus(503)
419
f6b5b4d7
TL
420
421class HostUiControllerTest(ControllerTestCase):
422 URL_HOST = '/ui-api/host'
423
424 @classmethod
425 def setup_server(cls):
f6b5b4d7
TL
426 cls.setup_controllers([HostUi])
427
f67539c2
TL
428 def test_labels(self):
429 orch_hosts = [
f6b5b4d7
TL
430 HostSpec('node1', labels=['foo']),
431 HostSpec('node2', labels=['foo', 'bar'])
432 ]
f6b5b4d7 433
f67539c2
TL
434 with patch_orch(True, hosts=orch_hosts):
435 self._get('{}/labels'.format(self.URL_HOST))
436 self.assertStatus(200)
437 labels = self.json_body()
438 labels.sort()
439 self.assertListEqual(labels, ['bar', 'foo'])
440
441 @mock.patch('dashboard.controllers.host.get_inventories')
442 def test_inventory(self, mock_get_inventories):
443 inventory_url = '{}/inventory'.format(self.URL_HOST)
444 with patch_orch(True):
445 tests = [
446 {
447 'url': inventory_url,
448 'refresh': None
449 },
450 {
451 'url': '{}?refresh=true'.format(inventory_url),
452 'refresh': "true"
453 },
454 ]
455 for test in tests:
456 mock_get_inventories.reset_mock()
457 mock_get_inventories.return_value = [{'a': 'b'}]
458 self._get(test['url'])
459 mock_get_inventories.assert_called_once_with(None, test['refresh'])
460 self.assertEqual(self.json_body(), [{'a': 'b'}])
461 self.assertStatus(200)
462
463 # list without orchestrator service
464 with patch_orch(False):
465 self._get(inventory_url)
466 self.assertStatus(503)
f6b5b4d7
TL
467
468
469class TestHosts(unittest.TestCase):
f67539c2 470 def test_get_hosts(self):
f6b5b4d7 471 mgr.list_servers.return_value = [{
39ae355f
TL
472 'hostname': 'node1',
473 'services': []
f6b5b4d7 474 }, {
39ae355f
TL
475 'hostname': 'localhost',
476 'services': []
f6b5b4d7 477 }]
f67539c2 478 orch_hosts = [
f6b5b4d7
TL
479 HostSpec('node1', labels=['foo', 'bar']),
480 HostSpec('node2', labels=['bar'])
481 ]
9f95a23c 482
f67539c2
TL
483 with patch_orch(True, hosts=orch_hosts):
484 hosts = get_hosts()
aee94f69 485 self.assertEqual(len(hosts), 2)
f67539c2 486 checks = {
f67539c2
TL
487 'node1': {
488 'sources': {
aee94f69 489 'ceph': False,
f67539c2
TL
490 'orchestrator': True
491 },
aee94f69 492 'labels': ['foo', 'bar']
f67539c2
TL
493 },
494 'node2': {
495 'sources': {
496 'ceph': False,
497 'orchestrator': True
498 },
499 'labels': ['bar']
500 }
501 }
502 for host in hosts:
503 hostname = host['hostname']
504 self.assertDictEqual(host['sources'], checks[hostname]['sources'])
505 self.assertListEqual(host['labels'], checks[hostname]['labels'])
506
507 @mock.patch('dashboard.controllers.host.mgr.get')
508 def test_get_device_osd_map(self, mgr_get):
509 mgr_get.side_effect = lambda key: {
510 'osd_metadata': {
511 '0': {
512 'hostname': 'node0',
513 'devices': 'nvme0n1,sdb',
514 },
515 '1': {
516 'hostname': 'node0',
517 'devices': 'nvme0n1,sdc',
518 },
519 '2': {
520 'hostname': 'node1',
521 'devices': 'sda',
522 },
523 '3': {
524 'hostname': 'node2',
525 'devices': '',
526 }
527 }
528 }[key]
529
530 device_osd_map = get_device_osd_map()
531 mgr.get.assert_called_with('osd_metadata')
532 # sort OSD IDs to make assertDictEqual work
533 for devices in device_osd_map.values():
534 for host in devices.keys():
535 devices[host] = sorted(devices[host])
536 self.assertDictEqual(device_osd_map, {
537 'node0': {
538 'nvme0n1': [0, 1],
539 'sdb': [0],
540 'sdc': [1],
f6b5b4d7
TL
541 },
542 'node1': {
f67539c2
TL
543 'sda': [2]
544 }
545 })
546
547 @mock.patch('dashboard.controllers.host.str_to_bool')
548 @mock.patch('dashboard.controllers.host.get_device_osd_map')
549 def test_get_inventories(self, mock_get_device_osd_map, mock_str_to_bool):
550 mock_get_device_osd_map.return_value = {
551 'host-0': {
552 'nvme0n1': [1, 2],
553 'sdb': [1],
554 'sdc': [2]
f6b5b4d7 555 },
f67539c2
TL
556 'host-1': {
557 'sdb': [3]
f6b5b4d7 558 }
9f95a23c 559 }
f67539c2
TL
560 inventory = [
561 {
562 'name': 'host-0',
563 'addr': '1.2.3.4',
564 'devices': [
565 {'path': 'nvme0n1'},
566 {'path': '/dev/sdb'},
567 {'path': '/dev/sdc'},
568 ]
569 },
570 {
571 'name': 'host-1',
572 'addr': '1.2.3.5',
573 'devices': [
574 {'path': '/dev/sda'},
575 {'path': 'sdb'},
576 ]
577 }
578 ]
579
580 with patch_orch(True, inventory=inventory) as orch_client:
581 mock_str_to_bool.return_value = True
582
583 hosts = ['host-0', 'host-1']
584 inventories = get_inventories(hosts, 'true')
585 mock_str_to_bool.assert_called_with('true')
586 orch_client.inventory.list.assert_called_once_with(hosts=hosts, refresh=True)
587 self.assertEqual(len(inventories), 2)
588 host0 = inventories[0]
589 self.assertEqual(host0['name'], 'host-0')
590 self.assertEqual(host0['addr'], '1.2.3.4')
39ae355f
TL
591 # devices should be sorted by path name, so
592 # /dev/sdb, /dev/sdc, nvme0n1
593 self.assertEqual(host0['devices'][0]['osd_ids'], [1])
594 self.assertEqual(host0['devices'][1]['osd_ids'], [2])
595 self.assertEqual(host0['devices'][2]['osd_ids'], [1, 2])
f67539c2
TL
596 host1 = inventories[1]
597 self.assertEqual(host1['name'], 'host-1')
598 self.assertEqual(host1['addr'], '1.2.3.5')
39ae355f
TL
599 # devices should be sorted by path name, so
600 # /dev/sda, sdb
f67539c2
TL
601 self.assertEqual(host1['devices'][0]['osd_ids'], [])
602 self.assertEqual(host1['devices'][1]['osd_ids'], [3])