]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/tests/test_host.py
0f55ce52247c117fea36972700a8daace03f60be
[ceph.git] / ceph / src / pybind / mgr / dashboard / tests / test_host.py
1 import unittest
2 from unittest import mock
3
4 from orchestrator import HostSpec
5
6 from .. import mgr
7 from ..controllers._version import APIVersion
8 from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories
9 from ..tests import ControllerTestCase, patch_orch
10 from ..tools import NotificationQueue, TaskManager
11
12
13 class HostControllerTest(ControllerTestCase):
14 URL_HOST = '/api/host'
15
16 @classmethod
17 def setup_server(cls):
18 NotificationQueue.start_queue()
19 TaskManager.init()
20 cls.setup_controllers([Host])
21
22 @classmethod
23 def tearDownClass(cls):
24 NotificationQueue.stop()
25
26 @mock.patch('dashboard.controllers.host.get_hosts')
27 def test_host_list_with_sources(self, mock_get_hosts):
28 hosts = [{
29 'hostname': 'host-0',
30 'sources': {
31 'ceph': True,
32 'orchestrator': False
33 }
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 }]
47
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
56
57 mock_get_hosts.side_effect = _get_hosts
58
59 self._get(self.URL_HOST, version=APIVersion(1, 1))
60 self.assertStatus(200)
61 self.assertJsonBody(hosts)
62
63 self._get('{}?sources=ceph'.format(self.URL_HOST), version=APIVersion(1, 1))
64 self.assertStatus(200)
65 self.assertJsonBody(hosts[0])
66
67 self._get('{}?sources=orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1))
68 self.assertStatus(200)
69 self.assertJsonBody(hosts[1:])
70
71 self._get('{}?sources=ceph,orchestrator'.format(self.URL_HOST), version=APIVersion(1, 1))
72 self.assertStatus(200)
73 self.assertJsonBody(hosts)
74
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
158 def test_get_1(self):
159 mgr.list_servers.return_value = []
160
161 with patch_orch(False):
162 self._get('{}/node1'.format(self.URL_HOST))
163 self.assertStatus(404)
164
165 def test_get_2(self):
166 mgr.list_servers.return_value = [{'hostname': 'node1'}]
167
168 with patch_orch(False):
169 self._get('{}/node1'.format(self.URL_HOST))
170 self.assertStatus(200)
171 self.assertIn('labels', self.json_body())
172 self.assertIn('status', self.json_body())
173 self.assertIn('addr', self.json_body())
174
175 def test_get_3(self):
176 mgr.list_servers.return_value = []
177
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())
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 }
194 self._post(self.URL_HOST, payload, version=APIVersion(0, 1))
195 self.assertStatus(201)
196 mock_add_host.assert_called()
197
198 def test_set_labels(self):
199 mgr.list_servers.return_value = []
200 orch_hosts = [
201 HostSpec('node0', labels=['aaa', 'bbb'])
202 ]
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']}
208 self._put('{}/node0'.format(self.URL_HOST), payload, version=APIVersion(0, 1))
209 self.assertStatus(200)
210 self.assertHeader('Content-Type',
211 'application/vnd.ceph.api.v0.1+json')
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]
216 self._put('{}/node0'.format(self.URL_HOST),
217 {'update_labels': True, 'labels': 'ddd'},
218 version=APIVersion(0, 1))
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
229 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
230 version=APIVersion(0, 1))
231 self.assertStatus(200)
232 self.assertHeader('Content-Type',
233 'application/vnd.ceph.api.v0.1+json')
234
235 # force enter maintenance mode
236 self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True},
237 version=APIVersion(0, 1))
238 self.assertStatus(200)
239
240 # exit maintenance mode
241 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
242 version=APIVersion(0, 1))
243 self.assertStatus(200)
244 self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True},
245 version=APIVersion(0, 1))
246 self.assertStatus(200)
247
248 # maintenance without orchestrator service
249 with patch_orch(False):
250 self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True},
251 version=APIVersion(0, 1))
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)
302
303 # list without orchestrator service
304 with patch_orch(False):
305 self._get(inventory_url)
306 self.assertStatus(503)
307
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
326
327 class HostUiControllerTest(ControllerTestCase):
328 URL_HOST = '/ui-api/host'
329
330 @classmethod
331 def setup_server(cls):
332 cls.setup_controllers([HostUi])
333
334 def test_labels(self):
335 orch_hosts = [
336 HostSpec('node1', labels=['foo']),
337 HostSpec('node2', labels=['foo', 'bar'])
338 ]
339
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)
373
374
375 class TestHosts(unittest.TestCase):
376 def test_get_hosts(self):
377 mgr.list_servers.return_value = [{
378 'hostname': 'node1'
379 }, {
380 'hostname': 'localhost'
381 }]
382 orch_hosts = [
383 HostSpec('node1', labels=['foo', 'bar']),
384 HostSpec('node2', labels=['bar'])
385 ]
386
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': []
397 },
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],
452 },
453 'node1': {
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]
466 },
467 'host-1': {
468 'sdb': [3]
469 }
470 }
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])