]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
f6b5b4d7 | 3 | |
9f95a23c | 4 | import copy |
f67539c2 TL |
5 | import os |
6 | import time | |
7 | from typing import Dict, List, Optional | |
f6b5b4d7 TL |
8 | |
9 | import cherrypy | |
9f95a23c TL |
10 | from mgr_util import merge_dicts |
11 | from orchestrator import HostSpec | |
f67539c2 | 12 | |
11fdf7f2 | 13 | from .. import mgr |
9f95a23c | 14 | from ..exceptions import DashboardException |
11fdf7f2 | 15 | from ..security import Scope |
9f95a23c TL |
16 | from ..services.ceph_service import CephService |
17 | from ..services.exception import handle_orchestrator_error | |
f67539c2 TL |
18 | from ..services.orchestrator import OrchClient, OrchFeature |
19 | from ..tools import TaskManager, str_to_bool | |
20 | from . import ApiController, BaseController, ControllerDoc, Endpoint, \ | |
21 | EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \ | |
22 | UpdatePermission, allow_empty_body | |
23 | from .orchestrator import raise_if_no_orchestrator | |
24 | ||
25 | LIST_HOST_SCHEMA = { | |
26 | "hostname": (str, "Hostname"), | |
27 | "services": ([{ | |
28 | "type": (str, "type of service"), | |
29 | "id": (str, "Service Id"), | |
30 | }], "Services related to the host"), | |
31 | "ceph_version": (str, "Ceph version"), | |
32 | "addr": (str, "Host address"), | |
33 | "labels": ([str], "Labels related to the host"), | |
34 | "service_type": (str, ""), | |
35 | "sources": ({ | |
36 | "ceph": (bool, ""), | |
37 | "orchestrator": (bool, "") | |
38 | }, "Host Sources"), | |
39 | "status": (str, "") | |
40 | } | |
41 | ||
42 | INVENTORY_SCHEMA = { | |
43 | "name": (str, "Hostname"), | |
44 | "addr": (str, "Host address"), | |
45 | "devices": ([{ | |
46 | "rejected_reasons": ([str], ""), | |
47 | "available": (bool, "If the device can be provisioned to an OSD"), | |
48 | "path": (str, "Device path"), | |
49 | "sys_api": ({ | |
50 | "removable": (str, ""), | |
51 | "ro": (str, ""), | |
52 | "vendor": (str, ""), | |
53 | "model": (str, ""), | |
54 | "rev": (str, ""), | |
55 | "sas_address": (str, ""), | |
56 | "sas_device_handle": (str, ""), | |
57 | "support_discard": (str, ""), | |
58 | "rotational": (str, ""), | |
59 | "nr_requests": (str, ""), | |
60 | "scheduler_mode": (str, ""), | |
61 | "partitions": ({ | |
62 | "partition_name": ({ | |
63 | "start": (str, ""), | |
64 | "sectors": (str, ""), | |
65 | "sectorsize": (int, ""), | |
66 | "size": (int, ""), | |
67 | "human_readable_size": (str, ""), | |
68 | "holders": ([str], "") | |
69 | }, "") | |
70 | }, ""), | |
71 | "sectors": (int, ""), | |
72 | "sectorsize": (str, ""), | |
73 | "size": (int, ""), | |
74 | "human_readable_size": (str, ""), | |
75 | "path": (str, ""), | |
76 | "locked": (int, "") | |
77 | }, ""), | |
78 | "lvs": ([{ | |
79 | "name": (str, ""), | |
80 | "osd_id": (str, ""), | |
81 | "cluster_name": (str, ""), | |
82 | "type": (str, ""), | |
83 | "osd_fsid": (str, ""), | |
84 | "cluster_fsid": (str, ""), | |
85 | "osdspec_affinity": (str, ""), | |
86 | "block_uuid": (str, ""), | |
87 | }], ""), | |
88 | "human_readable_type": (str, "Device type. ssd or hdd"), | |
89 | "device_id": (str, "Device's udev ID"), | |
90 | "lsm_data": ({ | |
91 | "serialNum": (str, ""), | |
92 | "transport": (str, ""), | |
93 | "mediaType": (str, ""), | |
94 | "rpm": (str, ""), | |
95 | "linkSpeed": (str, ""), | |
96 | "health": (str, ""), | |
97 | "ledSupport": ({ | |
98 | "IDENTsupport": (str, ""), | |
99 | "IDENTstatus": (str, ""), | |
100 | "FAILsupport": (str, ""), | |
101 | "FAILstatus": (str, ""), | |
102 | }, ""), | |
103 | "errors": ([str], "") | |
104 | }, ""), | |
105 | "osd_ids": ([int], "Device OSD IDs") | |
106 | }], "Host devices"), | |
107 | "labels": ([str], "Host labels") | |
108 | } | |
9f95a23c TL |
109 | |
110 | ||
111 | def host_task(name, metadata, wait_for=10.0): | |
112 | return Task("host/{}".format(name), metadata, wait_for) | |
113 | ||
114 | ||
115 | def merge_hosts_by_hostname(ceph_hosts, orch_hosts): | |
116 | # type: (List[dict], List[HostSpec]) -> List[dict] | |
f6b5b4d7 TL |
117 | """ |
118 | Merge Ceph hosts with orchestrator hosts by hostnames. | |
9f95a23c TL |
119 | |
120 | :param ceph_hosts: hosts returned from mgr | |
121 | :type ceph_hosts: list of dict | |
122 | :param orch_hosts: hosts returned from ochestrator | |
123 | :type orch_hosts: list of HostSpec | |
124 | :return list of dict | |
125 | """ | |
e306af50 | 126 | hosts = copy.deepcopy(ceph_hosts) |
f6b5b4d7 TL |
127 | orch_hosts_map = {host.hostname: host.to_json() for host in orch_hosts} |
128 | ||
129 | # Sort labels. | |
130 | for hostname in orch_hosts_map: | |
131 | orch_hosts_map[hostname]['labels'].sort() | |
132 | ||
133 | # Hosts in both Ceph and Orchestrator. | |
e306af50 TL |
134 | for host in hosts: |
135 | hostname = host['hostname'] | |
136 | if hostname in orch_hosts_map: | |
f6b5b4d7 | 137 | host.update(orch_hosts_map[hostname]) |
e306af50 TL |
138 | host['sources']['orchestrator'] = True |
139 | orch_hosts_map.pop(hostname) | |
9f95a23c | 140 | |
f6b5b4d7 | 141 | # Hosts only in Orchestrator. |
e306af50 | 142 | orch_hosts_only = [ |
f6b5b4d7 TL |
143 | merge_dicts( |
144 | { | |
145 | 'ceph_version': '', | |
146 | 'services': [], | |
147 | 'sources': { | |
148 | 'ceph': False, | |
149 | 'orchestrator': True | |
150 | } | |
151 | }, orch_hosts_map[hostname]) for hostname in orch_hosts_map | |
e306af50 TL |
152 | ] |
153 | hosts.extend(orch_hosts_only) | |
154 | return hosts | |
9f95a23c TL |
155 | |
156 | ||
157 | def get_hosts(from_ceph=True, from_orchestrator=True): | |
e306af50 TL |
158 | """ |
159 | Get hosts from various sources. | |
160 | """ | |
9f95a23c TL |
161 | ceph_hosts = [] |
162 | if from_ceph: | |
e306af50 | 163 | ceph_hosts = [ |
f6b5b4d7 TL |
164 | merge_dicts( |
165 | server, { | |
166 | 'addr': '', | |
167 | 'labels': [], | |
168 | 'service_type': '', | |
169 | 'sources': { | |
170 | 'ceph': True, | |
171 | 'orchestrator': False | |
172 | }, | |
173 | 'status': '' | |
174 | }) for server in mgr.list_servers() | |
e306af50 | 175 | ] |
9f95a23c TL |
176 | if from_orchestrator: |
177 | orch = OrchClient.instance() | |
178 | if orch.available(): | |
179 | return merge_hosts_by_hostname(ceph_hosts, orch.hosts.list()) | |
180 | return ceph_hosts | |
11fdf7f2 TL |
181 | |
182 | ||
f6b5b4d7 TL |
183 | def get_host(hostname: str) -> Dict: |
184 | """ | |
185 | Get a specific host from Ceph or Orchestrator (if available). | |
186 | :param hostname: The name of the host to fetch. | |
187 | :raises: cherrypy.HTTPError: If host not found. | |
188 | """ | |
189 | for host in get_hosts(): | |
190 | if host['hostname'] == hostname: | |
191 | return host | |
192 | raise cherrypy.HTTPError(404) | |
193 | ||
194 | ||
f67539c2 TL |
195 | def get_device_osd_map(): |
196 | """Get mappings from inventory devices to OSD IDs. | |
197 | ||
198 | :return: Returns a dictionary containing mappings. Note one device might | |
199 | shared between multiple OSDs. | |
200 | e.g. { | |
201 | 'node1': { | |
202 | 'nvme0n1': [0, 1], | |
203 | 'vdc': [0], | |
204 | 'vdb': [1] | |
205 | }, | |
206 | 'node2': { | |
207 | 'vdc': [2] | |
208 | } | |
209 | } | |
210 | :rtype: dict | |
211 | """ | |
212 | result: dict = {} | |
213 | for osd_id, osd_metadata in mgr.get('osd_metadata').items(): | |
214 | hostname = osd_metadata.get('hostname') | |
215 | devices = osd_metadata.get('devices') | |
216 | if not hostname or not devices: | |
217 | continue | |
218 | if hostname not in result: | |
219 | result[hostname] = {} | |
220 | # for OSD contains multiple devices, devices is in `sda,sdb` | |
221 | for device in devices.split(','): | |
222 | if device not in result[hostname]: | |
223 | result[hostname][device] = [int(osd_id)] | |
224 | else: | |
225 | result[hostname][device].append(int(osd_id)) | |
226 | return result | |
227 | ||
228 | ||
229 | def get_inventories(hosts: Optional[List[str]] = None, | |
230 | refresh: Optional[bool] = None) -> List[dict]: | |
231 | """Get inventories from the Orchestrator and link devices with OSD IDs. | |
232 | ||
233 | :param hosts: Hostnames to query. | |
234 | :param refresh: Ask the Orchestrator to refresh the inventories. Note the this is an | |
235 | asynchronous operation, the updated version of inventories need to | |
236 | be re-qeuried later. | |
237 | :return: Returns list of inventory. | |
238 | :rtype: list | |
239 | """ | |
240 | do_refresh = False | |
241 | if refresh is not None: | |
242 | do_refresh = str_to_bool(refresh) | |
243 | orch = OrchClient.instance() | |
244 | inventory_hosts = [host.to_json() | |
245 | for host in orch.inventory.list(hosts=hosts, refresh=do_refresh)] | |
246 | device_osd_map = get_device_osd_map() | |
247 | for inventory_host in inventory_hosts: | |
248 | host_osds = device_osd_map.get(inventory_host['name']) | |
249 | for device in inventory_host['devices']: | |
250 | if host_osds: # pragma: no cover | |
251 | dev_name = os.path.basename(device['path']) | |
252 | device['osd_ids'] = sorted(host_osds.get(dev_name, [])) | |
253 | else: | |
254 | device['osd_ids'] = [] | |
255 | return inventory_hosts | |
256 | ||
257 | ||
11fdf7f2 | 258 | @ApiController('/host', Scope.HOSTS) |
f67539c2 | 259 | @ControllerDoc("Get Host Details", "Host") |
11fdf7f2 | 260 | class Host(RESTController): |
f67539c2 TL |
261 | @EndpointDoc("List Host Specifications", |
262 | parameters={ | |
263 | 'sources': (str, 'Host Sources'), | |
264 | }, | |
265 | responses={200: LIST_HOST_SCHEMA}) | |
9f95a23c TL |
266 | def list(self, sources=None): |
267 | if sources is None: | |
268 | return get_hosts() | |
269 | _sources = sources.split(',') | |
270 | from_ceph = 'ceph' in _sources | |
271 | from_orchestrator = 'orchestrator' in _sources | |
272 | return get_hosts(from_ceph, from_orchestrator) | |
273 | ||
f67539c2 | 274 | @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_CREATE]) |
9f95a23c TL |
275 | @handle_orchestrator_error('host') |
276 | @host_task('create', {'hostname': '{hostname}'}) | |
f6b5b4d7 | 277 | def create(self, hostname): # pragma: no cover - requires realtime env |
9f95a23c TL |
278 | orch_client = OrchClient.instance() |
279 | self._check_orchestrator_host_op(orch_client, hostname, True) | |
280 | orch_client.hosts.add(hostname) | |
f91f0fd5 | 281 | create._cp_config = {'tools.json_in.force': False} # pylint: disable=W0212 |
9f95a23c | 282 | |
f67539c2 | 283 | @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE]) |
9f95a23c TL |
284 | @handle_orchestrator_error('host') |
285 | @host_task('delete', {'hostname': '{hostname}'}) | |
f91f0fd5 | 286 | @allow_empty_body |
f6b5b4d7 | 287 | def delete(self, hostname): # pragma: no cover - requires realtime env |
9f95a23c TL |
288 | orch_client = OrchClient.instance() |
289 | self._check_orchestrator_host_op(orch_client, hostname, False) | |
290 | orch_client.hosts.remove(hostname) | |
291 | ||
f6b5b4d7 | 292 | def _check_orchestrator_host_op(self, orch_client, hostname, add_host=True): # pragma:no cover |
9f95a23c TL |
293 | """Check if we can adding or removing a host with orchestrator |
294 | ||
295 | :param orch_client: Orchestrator client | |
296 | :param add: True for adding host operation, False for removing host | |
297 | :raise DashboardException | |
298 | """ | |
299 | host = orch_client.hosts.get(hostname) | |
300 | if add_host and host: | |
301 | raise DashboardException( | |
302 | code='orchestrator_add_existed_host', | |
303 | msg='{} is already in orchestrator'.format(hostname), | |
304 | component='orchestrator') | |
305 | if not add_host and not host: | |
306 | raise DashboardException( | |
307 | code='orchestrator_remove_nonexistent_host', | |
308 | msg='Remove a non-existent host {} from orchestrator'.format( | |
309 | hostname), | |
310 | component='orchestrator') | |
311 | ||
312 | @RESTController.Resource('GET') | |
313 | def devices(self, hostname): | |
314 | # (str) -> List | |
315 | return CephService.get_devices_by_host(hostname) | |
316 | ||
317 | @RESTController.Resource('GET') | |
318 | def smart(self, hostname): | |
319 | # type: (str) -> dict | |
320 | return CephService.get_smart_data_by_host(hostname) | |
321 | ||
322 | @RESTController.Resource('GET') | |
f67539c2 TL |
323 | @raise_if_no_orchestrator([OrchFeature.DEVICE_LIST]) |
324 | @handle_orchestrator_error('host') | |
325 | @EndpointDoc('Get inventory of a host', | |
326 | parameters={ | |
327 | 'hostname': (str, 'Hostname'), | |
328 | 'refresh': (str, 'Trigger asynchronous refresh'), | |
329 | }, | |
330 | responses={200: INVENTORY_SCHEMA}) | |
331 | def inventory(self, hostname, refresh=None): | |
332 | inventory = get_inventories([hostname], refresh) | |
333 | if inventory: | |
334 | return inventory[0] | |
335 | return {} | |
336 | ||
337 | @RESTController.Resource('POST') | |
338 | @UpdatePermission | |
339 | @raise_if_no_orchestrator([OrchFeature.DEVICE_BLINK_LIGHT]) | |
340 | @handle_orchestrator_error('host') | |
341 | @host_task('identify_device', ['{hostname}', '{device}'], wait_for=2.0) | |
342 | def identify_device(self, hostname, device, duration): | |
343 | # type: (str, str, int) -> None | |
344 | """ | |
345 | Identify a device by switching on the device light for N seconds. | |
346 | :param hostname: The hostname of the device to process. | |
347 | :param device: The device identifier to process, e.g. ``/dev/dm-0`` or | |
348 | ``ABC1234DEF567-1R1234_ABC8DE0Q``. | |
349 | :param duration: The duration in seconds how long the LED should flash. | |
350 | """ | |
351 | orch = OrchClient.instance() | |
352 | TaskManager.current_task().set_progress(0) | |
353 | orch.blink_device_light(hostname, device, 'ident', True) | |
354 | for i in range(int(duration)): | |
355 | percentage = int(round(i / float(duration) * 100)) | |
356 | TaskManager.current_task().set_progress(percentage) | |
357 | time.sleep(1) | |
358 | orch.blink_device_light(hostname, device, 'ident', False) | |
359 | TaskManager.current_task().set_progress(100) | |
360 | ||
361 | @RESTController.Resource('GET') | |
362 | @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST]) | |
9f95a23c TL |
363 | def daemons(self, hostname: str) -> List[dict]: |
364 | orch = OrchClient.instance() | |
f91f0fd5 | 365 | daemons = orch.services.list_daemons(hostname=hostname) |
9f95a23c | 366 | return [d.to_json() for d in daemons] |
f6b5b4d7 TL |
367 | |
368 | @handle_orchestrator_error('host') | |
369 | def get(self, hostname: str) -> Dict: | |
370 | """ | |
371 | Get the specified host. | |
372 | :raises: cherrypy.HTTPError: If host not found. | |
373 | """ | |
374 | return get_host(hostname) | |
375 | ||
f67539c2 TL |
376 | @raise_if_no_orchestrator([OrchFeature.HOST_LABEL_ADD, |
377 | OrchFeature.HOST_LABEL_REMOVE, | |
378 | OrchFeature.HOST_MAINTENANCE_ENTER, | |
379 | OrchFeature.HOST_MAINTENANCE_EXIT]) | |
f6b5b4d7 | 380 | @handle_orchestrator_error('host') |
f67539c2 TL |
381 | @EndpointDoc('', |
382 | parameters={ | |
383 | 'hostname': (str, 'Hostname'), | |
384 | 'update_labels': (bool, 'Update Labels'), | |
385 | 'labels': ([str], 'Host Labels'), | |
386 | 'maintenance': (bool, 'Enter/Exit Maintenance'), | |
387 | 'force': (bool, 'Force Enter Maintenance') | |
388 | }, | |
389 | responses={200: None, 204: None}) | |
390 | def set(self, hostname: str, update_labels: bool = False, | |
391 | labels: List[str] = None, maintenance: bool = False, | |
392 | force: bool = False): | |
f6b5b4d7 TL |
393 | """ |
394 | Update the specified host. | |
395 | Note, this is only supported when Ceph Orchestrator is enabled. | |
396 | :param hostname: The name of the host to be processed. | |
f67539c2 | 397 | :param update_labels: To update the labels. |
f6b5b4d7 | 398 | :param labels: List of labels. |
f67539c2 TL |
399 | :param maintenance: Enter/Exit maintenance mode. |
400 | :param force: Force enter maintenance mode. | |
f6b5b4d7 TL |
401 | """ |
402 | orch = OrchClient.instance() | |
403 | host = get_host(hostname) | |
f67539c2 TL |
404 | |
405 | if maintenance: | |
406 | status = host['status'] | |
407 | if status != 'maintenance': | |
408 | orch.hosts.enter_maintenance(hostname, force) | |
409 | ||
410 | if status == 'maintenance': | |
411 | orch.hosts.exit_maintenance(hostname) | |
412 | ||
413 | if update_labels: | |
414 | # only allow List[str] type for labels | |
415 | if not isinstance(labels, list): | |
416 | raise DashboardException( | |
417 | msg='Expected list of labels. Please check API documentation.', | |
418 | http_status_code=400, | |
419 | component='orchestrator') | |
420 | current_labels = set(host['labels']) | |
421 | # Remove labels. | |
422 | remove_labels = list(current_labels.difference(set(labels))) | |
423 | for label in remove_labels: | |
424 | orch.hosts.remove_label(hostname, label) | |
425 | # Add labels. | |
426 | add_labels = list(set(labels).difference(current_labels)) | |
427 | for label in add_labels: | |
428 | orch.hosts.add_label(hostname, label) | |
f6b5b4d7 TL |
429 | |
430 | ||
431 | @UiApiController('/host', Scope.HOSTS) | |
432 | class HostUi(BaseController): | |
433 | @Endpoint('GET') | |
434 | @ReadPermission | |
435 | @handle_orchestrator_error('host') | |
436 | def labels(self) -> List[str]: | |
437 | """ | |
438 | Get all host labels. | |
439 | Note, host labels are only supported when Ceph Orchestrator is enabled. | |
440 | If Ceph Orchestrator is not enabled, an empty list is returned. | |
441 | :return: A list of all host labels. | |
442 | """ | |
443 | labels = [] | |
444 | orch = OrchClient.instance() | |
445 | if orch.available(): | |
446 | for host in orch.hosts.list(): | |
447 | labels.extend(host.labels) | |
448 | labels.sort() | |
449 | return list(set(labels)) # Filter duplicate labels. | |
f67539c2 TL |
450 | |
451 | @Endpoint('GET') | |
452 | @ReadPermission | |
453 | @raise_if_no_orchestrator([OrchFeature.DEVICE_LIST]) | |
454 | @handle_orchestrator_error('host') | |
455 | def inventory(self, refresh=None): | |
456 | return get_inventories(None, refresh) |