]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import contextlib |
9f95a23c | 2 | import unittest |
f67539c2 TL |
3 | from typing import List, Optional |
4 | from unittest import mock | |
9f95a23c | 5 | |
f67539c2 | 6 | from orchestrator import HostSpec, InventoryHost |
9f95a23c | 7 | |
9f95a23c | 8 | from .. import mgr |
f67539c2 TL |
9 | from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories |
10 | from ..tools import NotificationQueue, TaskManager | |
11 | from . import ControllerTestCase # pylint: disable=no-name-in-module | |
12 | ||
13 | ||
14 | @contextlib.contextmanager | |
15 | def patch_orch(available: bool, hosts: Optional[List[HostSpec]] = None, | |
16 | inventory: Optional[List[dict]] = None): | |
17 | with mock.patch('dashboard.controllers.orchestrator.OrchClient.instance') as instance: | |
18 | fake_client = mock.Mock() | |
19 | fake_client.available.return_value = available | |
20 | fake_client.get_missing_features.return_value = [] | |
21 | ||
22 | if hosts is not None: | |
23 | fake_client.hosts.list.return_value = hosts | |
24 | ||
25 | if inventory is not None: | |
26 | def _list_inventory(hosts=None, refresh=False): # pylint: disable=unused-argument | |
27 | inv_hosts = [] | |
28 | for inv_host in inventory: | |
29 | if hosts is None or inv_host['name'] in hosts: | |
30 | inv_hosts.append(InventoryHost.from_json(inv_host)) | |
31 | return inv_hosts | |
32 | fake_client.inventory.list.side_effect = _list_inventory | |
33 | ||
34 | instance.return_value = fake_client | |
35 | yield fake_client | |
9f95a23c TL |
36 | |
37 | ||
38 | class HostControllerTest(ControllerTestCase): | |
39 | URL_HOST = '/api/host' | |
40 | ||
41 | @classmethod | |
42 | def setup_server(cls): | |
f67539c2 TL |
43 | NotificationQueue.start_queue() |
44 | TaskManager.init() | |
9f95a23c TL |
45 | # pylint: disable=protected-access |
46 | Host._cp_config['tools.authenticate.on'] = False | |
47 | cls.setup_controllers([Host]) | |
48 | ||
f67539c2 TL |
49 | @classmethod |
50 | def tearDownClass(cls): | |
51 | NotificationQueue.stop() | |
52 | ||
9f95a23c TL |
53 | @mock.patch('dashboard.controllers.host.get_hosts') |
54 | def test_host_list(self, mock_get_hosts): | |
f6b5b4d7 TL |
55 | hosts = [{ |
56 | 'hostname': 'host-0', | |
57 | 'sources': { | |
58 | 'ceph': True, | |
59 | 'orchestrator': False | |
9f95a23c | 60 | } |
f6b5b4d7 TL |
61 | }, { |
62 | 'hostname': 'host-1', | |
63 | 'sources': { | |
64 | 'ceph': False, | |
65 | 'orchestrator': True | |
66 | } | |
67 | }, { | |
68 | 'hostname': 'host-2', | |
69 | 'sources': { | |
70 | 'ceph': True, | |
71 | 'orchestrator': True | |
72 | } | |
73 | }] | |
9f95a23c TL |
74 | |
75 | def _get_hosts(from_ceph=True, from_orchestrator=True): | |
76 | _hosts = [] | |
77 | if from_ceph: | |
78 | _hosts.append(hosts[0]) | |
79 | if from_orchestrator: | |
80 | _hosts.append(hosts[1]) | |
81 | _hosts.append(hosts[2]) | |
82 | return _hosts | |
f6b5b4d7 | 83 | |
9f95a23c TL |
84 | mock_get_hosts.side_effect = _get_hosts |
85 | ||
86 | self._get(self.URL_HOST) | |
87 | self.assertStatus(200) | |
88 | self.assertJsonBody(hosts) | |
89 | ||
90 | self._get('{}?sources=ceph'.format(self.URL_HOST)) | |
91 | self.assertStatus(200) | |
92 | self.assertJsonBody([hosts[0]]) | |
93 | ||
94 | self._get('{}?sources=orchestrator'.format(self.URL_HOST)) | |
95 | self.assertStatus(200) | |
96 | self.assertJsonBody(hosts[1:]) | |
97 | ||
98 | self._get('{}?sources=ceph,orchestrator'.format(self.URL_HOST)) | |
99 | self.assertStatus(200) | |
100 | self.assertJsonBody(hosts) | |
101 | ||
f67539c2 | 102 | def test_get_1(self): |
f6b5b4d7 | 103 | mgr.list_servers.return_value = [] |
9f95a23c | 104 | |
f67539c2 TL |
105 | with patch_orch(False): |
106 | self._get('{}/node1'.format(self.URL_HOST)) | |
107 | self.assertStatus(404) | |
f6b5b4d7 | 108 | |
f67539c2 | 109 | def test_get_2(self): |
f6b5b4d7 TL |
110 | mgr.list_servers.return_value = [{'hostname': 'node1'}] |
111 | ||
f67539c2 TL |
112 | with patch_orch(False): |
113 | self._get('{}/node1'.format(self.URL_HOST)) | |
114 | self.assertStatus(200) | |
115 | self.assertIn('labels', self.json_body()) | |
f6b5b4d7 | 116 | |
f67539c2 | 117 | def test_get_3(self): |
f6b5b4d7 TL |
118 | mgr.list_servers.return_value = [] |
119 | ||
f67539c2 TL |
120 | with patch_orch(True, hosts=[HostSpec('node1')]): |
121 | self._get('{}/node1'.format(self.URL_HOST)) | |
122 | self.assertStatus(200) | |
123 | self.assertIn('labels', self.json_body()) | |
9f95a23c | 124 | |
f67539c2 | 125 | def test_set_labels(self): |
f6b5b4d7 | 126 | mgr.list_servers.return_value = [] |
f67539c2 | 127 | orch_hosts = [ |
f6b5b4d7 TL |
128 | HostSpec('node0', labels=['aaa', 'bbb']) |
129 | ] | |
f67539c2 TL |
130 | with patch_orch(True, hosts=orch_hosts) as fake_client: |
131 | fake_client.hosts.remove_label = mock.Mock() | |
132 | fake_client.hosts.add_label = mock.Mock() | |
133 | ||
134 | payload = {'update_labels': True, 'labels': ['bbb', 'ccc']} | |
135 | self._put('{}/node0'.format(self.URL_HOST), payload) | |
136 | self.assertStatus(200) | |
137 | fake_client.hosts.remove_label.assert_called_once_with('node0', 'aaa') | |
138 | fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc') | |
139 | ||
140 | # return 400 if type other than List[str] | |
141 | self._put('{}/node0'.format(self.URL_HOST), {'update_labels': True, | |
142 | 'labels': 'ddd'}) | |
143 | self.assertStatus(400) | |
144 | ||
145 | def test_host_maintenance(self): | |
146 | mgr.list_servers.return_value = [] | |
147 | orch_hosts = [ | |
148 | HostSpec('node0'), | |
149 | HostSpec('node1') | |
150 | ] | |
151 | with patch_orch(True, hosts=orch_hosts): | |
152 | # enter maintenance mode | |
153 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) | |
154 | self.assertStatus(200) | |
155 | ||
156 | # force enter maintenance mode | |
157 | self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True}) | |
158 | self.assertStatus(200) | |
159 | ||
160 | # exit maintenance mode | |
161 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) | |
162 | self.assertStatus(200) | |
163 | self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}) | |
164 | self.assertStatus(200) | |
165 | ||
166 | # maintenance without orchestrator service | |
167 | with patch_orch(False): | |
168 | self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) | |
169 | self.assertStatus(503) | |
170 | ||
171 | @mock.patch('dashboard.controllers.host.time') | |
172 | def test_identify_device(self, mock_time): | |
173 | url = '{}/host-0/identify_device'.format(self.URL_HOST) | |
174 | with patch_orch(True) as fake_client: | |
175 | payload = { | |
176 | 'device': '/dev/sdz', | |
177 | 'duration': '1' | |
178 | } | |
179 | self._task_post(url, payload) | |
180 | self.assertStatus(200) | |
181 | mock_time.sleep.assert_called() | |
182 | calls = [ | |
183 | mock.call('host-0', '/dev/sdz', 'ident', True), | |
184 | mock.call('host-0', '/dev/sdz', 'ident', False), | |
185 | ] | |
186 | fake_client.blink_device_light.assert_has_calls(calls) | |
187 | ||
188 | @mock.patch('dashboard.controllers.host.get_inventories') | |
189 | def test_inventory(self, mock_get_inventories): | |
190 | inventory_url = '{}/host-0/inventory'.format(self.URL_HOST) | |
191 | with patch_orch(True): | |
192 | tests = [ | |
193 | { | |
194 | 'url': inventory_url, | |
195 | 'inventories': [{'a': 'b'}], | |
196 | 'refresh': None, | |
197 | 'resp': {'a': 'b'} | |
198 | }, | |
199 | { | |
200 | 'url': '{}?refresh=true'.format(inventory_url), | |
201 | 'inventories': [{'a': 'b'}], | |
202 | 'refresh': "true", | |
203 | 'resp': {'a': 'b'} | |
204 | }, | |
205 | { | |
206 | 'url': inventory_url, | |
207 | 'inventories': [], | |
208 | 'refresh': None, | |
209 | 'resp': {} | |
210 | }, | |
211 | ] | |
212 | for test in tests: | |
213 | mock_get_inventories.reset_mock() | |
214 | mock_get_inventories.return_value = test['inventories'] | |
215 | self._get(test['url']) | |
216 | mock_get_inventories.assert_called_once_with(['host-0'], test['refresh']) | |
217 | self.assertEqual(self.json_body(), test['resp']) | |
218 | self.assertStatus(200) | |
f6b5b4d7 | 219 | |
f67539c2 TL |
220 | # list without orchestrator service |
221 | with patch_orch(False): | |
222 | self._get(inventory_url) | |
223 | self.assertStatus(503) | |
f6b5b4d7 TL |
224 | |
225 | ||
226 | class HostUiControllerTest(ControllerTestCase): | |
227 | URL_HOST = '/ui-api/host' | |
228 | ||
229 | @classmethod | |
230 | def setup_server(cls): | |
231 | # pylint: disable=protected-access | |
232 | HostUi._cp_config['tools.authenticate.on'] = False | |
233 | cls.setup_controllers([HostUi]) | |
234 | ||
f67539c2 TL |
235 | def test_labels(self): |
236 | orch_hosts = [ | |
f6b5b4d7 TL |
237 | HostSpec('node1', labels=['foo']), |
238 | HostSpec('node2', labels=['foo', 'bar']) | |
239 | ] | |
f6b5b4d7 | 240 | |
f67539c2 TL |
241 | with patch_orch(True, hosts=orch_hosts): |
242 | self._get('{}/labels'.format(self.URL_HOST)) | |
243 | self.assertStatus(200) | |
244 | labels = self.json_body() | |
245 | labels.sort() | |
246 | self.assertListEqual(labels, ['bar', 'foo']) | |
247 | ||
248 | @mock.patch('dashboard.controllers.host.get_inventories') | |
249 | def test_inventory(self, mock_get_inventories): | |
250 | inventory_url = '{}/inventory'.format(self.URL_HOST) | |
251 | with patch_orch(True): | |
252 | tests = [ | |
253 | { | |
254 | 'url': inventory_url, | |
255 | 'refresh': None | |
256 | }, | |
257 | { | |
258 | 'url': '{}?refresh=true'.format(inventory_url), | |
259 | 'refresh': "true" | |
260 | }, | |
261 | ] | |
262 | for test in tests: | |
263 | mock_get_inventories.reset_mock() | |
264 | mock_get_inventories.return_value = [{'a': 'b'}] | |
265 | self._get(test['url']) | |
266 | mock_get_inventories.assert_called_once_with(None, test['refresh']) | |
267 | self.assertEqual(self.json_body(), [{'a': 'b'}]) | |
268 | self.assertStatus(200) | |
269 | ||
270 | # list without orchestrator service | |
271 | with patch_orch(False): | |
272 | self._get(inventory_url) | |
273 | self.assertStatus(503) | |
f6b5b4d7 TL |
274 | |
275 | ||
276 | class TestHosts(unittest.TestCase): | |
f67539c2 | 277 | def test_get_hosts(self): |
f6b5b4d7 TL |
278 | mgr.list_servers.return_value = [{ |
279 | 'hostname': 'node1' | |
280 | }, { | |
281 | 'hostname': 'localhost' | |
282 | }] | |
f67539c2 | 283 | orch_hosts = [ |
f6b5b4d7 TL |
284 | HostSpec('node1', labels=['foo', 'bar']), |
285 | HostSpec('node2', labels=['bar']) | |
286 | ] | |
9f95a23c | 287 | |
f67539c2 TL |
288 | with patch_orch(True, hosts=orch_hosts): |
289 | hosts = get_hosts() | |
290 | self.assertEqual(len(hosts), 3) | |
291 | checks = { | |
292 | 'localhost': { | |
293 | 'sources': { | |
294 | 'ceph': True, | |
295 | 'orchestrator': False | |
296 | }, | |
297 | 'labels': [] | |
f6b5b4d7 | 298 | }, |
f67539c2 TL |
299 | 'node1': { |
300 | 'sources': { | |
301 | 'ceph': True, | |
302 | 'orchestrator': True | |
303 | }, | |
304 | 'labels': ['bar', 'foo'] | |
305 | }, | |
306 | 'node2': { | |
307 | 'sources': { | |
308 | 'ceph': False, | |
309 | 'orchestrator': True | |
310 | }, | |
311 | 'labels': ['bar'] | |
312 | } | |
313 | } | |
314 | for host in hosts: | |
315 | hostname = host['hostname'] | |
316 | self.assertDictEqual(host['sources'], checks[hostname]['sources']) | |
317 | self.assertListEqual(host['labels'], checks[hostname]['labels']) | |
318 | ||
319 | @mock.patch('dashboard.controllers.host.mgr.get') | |
320 | def test_get_device_osd_map(self, mgr_get): | |
321 | mgr_get.side_effect = lambda key: { | |
322 | 'osd_metadata': { | |
323 | '0': { | |
324 | 'hostname': 'node0', | |
325 | 'devices': 'nvme0n1,sdb', | |
326 | }, | |
327 | '1': { | |
328 | 'hostname': 'node0', | |
329 | 'devices': 'nvme0n1,sdc', | |
330 | }, | |
331 | '2': { | |
332 | 'hostname': 'node1', | |
333 | 'devices': 'sda', | |
334 | }, | |
335 | '3': { | |
336 | 'hostname': 'node2', | |
337 | 'devices': '', | |
338 | } | |
339 | } | |
340 | }[key] | |
341 | ||
342 | device_osd_map = get_device_osd_map() | |
343 | mgr.get.assert_called_with('osd_metadata') | |
344 | # sort OSD IDs to make assertDictEqual work | |
345 | for devices in device_osd_map.values(): | |
346 | for host in devices.keys(): | |
347 | devices[host] = sorted(devices[host]) | |
348 | self.assertDictEqual(device_osd_map, { | |
349 | 'node0': { | |
350 | 'nvme0n1': [0, 1], | |
351 | 'sdb': [0], | |
352 | 'sdc': [1], | |
f6b5b4d7 TL |
353 | }, |
354 | 'node1': { | |
f67539c2 TL |
355 | 'sda': [2] |
356 | } | |
357 | }) | |
358 | ||
359 | @mock.patch('dashboard.controllers.host.str_to_bool') | |
360 | @mock.patch('dashboard.controllers.host.get_device_osd_map') | |
361 | def test_get_inventories(self, mock_get_device_osd_map, mock_str_to_bool): | |
362 | mock_get_device_osd_map.return_value = { | |
363 | 'host-0': { | |
364 | 'nvme0n1': [1, 2], | |
365 | 'sdb': [1], | |
366 | 'sdc': [2] | |
f6b5b4d7 | 367 | }, |
f67539c2 TL |
368 | 'host-1': { |
369 | 'sdb': [3] | |
f6b5b4d7 | 370 | } |
9f95a23c | 371 | } |
f67539c2 TL |
372 | inventory = [ |
373 | { | |
374 | 'name': 'host-0', | |
375 | 'addr': '1.2.3.4', | |
376 | 'devices': [ | |
377 | {'path': 'nvme0n1'}, | |
378 | {'path': '/dev/sdb'}, | |
379 | {'path': '/dev/sdc'}, | |
380 | ] | |
381 | }, | |
382 | { | |
383 | 'name': 'host-1', | |
384 | 'addr': '1.2.3.5', | |
385 | 'devices': [ | |
386 | {'path': '/dev/sda'}, | |
387 | {'path': 'sdb'}, | |
388 | ] | |
389 | } | |
390 | ] | |
391 | ||
392 | with patch_orch(True, inventory=inventory) as orch_client: | |
393 | mock_str_to_bool.return_value = True | |
394 | ||
395 | hosts = ['host-0', 'host-1'] | |
396 | inventories = get_inventories(hosts, 'true') | |
397 | mock_str_to_bool.assert_called_with('true') | |
398 | orch_client.inventory.list.assert_called_once_with(hosts=hosts, refresh=True) | |
399 | self.assertEqual(len(inventories), 2) | |
400 | host0 = inventories[0] | |
401 | self.assertEqual(host0['name'], 'host-0') | |
402 | self.assertEqual(host0['addr'], '1.2.3.4') | |
403 | self.assertEqual(host0['devices'][0]['osd_ids'], [1, 2]) | |
404 | self.assertEqual(host0['devices'][1]['osd_ids'], [1]) | |
405 | self.assertEqual(host0['devices'][2]['osd_ids'], [2]) | |
406 | host1 = inventories[1] | |
407 | self.assertEqual(host1['name'], 'host-1') | |
408 | self.assertEqual(host1['addr'], '1.2.3.5') | |
409 | self.assertEqual(host1['devices'][0]['osd_ids'], []) | |
410 | self.assertEqual(host1['devices'][1]['osd_ids'], [3]) |