1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
7 from typing
import Dict
, List
, Optional
10 from mgr_util
import merge_dicts
11 from orchestrator
import HostSpec
14 from ..exceptions
import DashboardException
15 from ..security
import Scope
16 from ..services
.ceph_service
import CephService
17 from ..services
.exception
import handle_orchestrator_error
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
26 "hostname": (str, "Hostname"),
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, ""),
37 "orchestrator": (bool, "")
43 "name": (str, "Hostname"),
44 "addr": (str, "Host address"),
46 "rejected_reasons": ([str], ""),
47 "available": (bool, "If the device can be provisioned to an OSD"),
48 "path": (str, "Device path"),
50 "removable": (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, ""),
65 "sectorsize": (int, ""),
67 "human_readable_size": (str, ""),
68 "holders": ([str], "")
72 "sectorsize": (str, ""),
74 "human_readable_size": (str, ""),
81 "cluster_name": (str, ""),
83 "osd_fsid": (str, ""),
84 "cluster_fsid": (str, ""),
85 "osdspec_affinity": (str, ""),
86 "block_uuid": (str, ""),
88 "human_readable_type": (str, "Device type. ssd or hdd"),
89 "device_id": (str, "Device's udev ID"),
91 "serialNum": (str, ""),
92 "transport": (str, ""),
93 "mediaType": (str, ""),
95 "linkSpeed": (str, ""),
98 "IDENTsupport": (str, ""),
99 "IDENTstatus": (str, ""),
100 "FAILsupport": (str, ""),
101 "FAILstatus": (str, ""),
103 "errors": ([str], "")
105 "osd_ids": ([int], "Device OSD IDs")
107 "labels": ([str], "Host labels")
111 def host_task(name
, metadata
, wait_for
=10.0):
112 return Task("host/{}".format(name
), metadata
, wait_for
)
115 def merge_hosts_by_hostname(ceph_hosts
, orch_hosts
):
116 # type: (List[dict], List[HostSpec]) -> List[dict]
118 Merge Ceph hosts with orchestrator hosts by hostnames.
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
126 hosts
= copy
.deepcopy(ceph_hosts
)
127 orch_hosts_map
= {host
.hostname
: host
.to_json() for host
in orch_hosts
}
130 for hostname
in orch_hosts_map
:
131 orch_hosts_map
[hostname
]['labels'].sort()
133 # Hosts in both Ceph and Orchestrator.
135 hostname
= host
['hostname']
136 if hostname
in orch_hosts_map
:
137 host
.update(orch_hosts_map
[hostname
])
138 host
['sources']['orchestrator'] = True
139 orch_hosts_map
.pop(hostname
)
141 # Hosts only in Orchestrator.
151 }, orch_hosts_map
[hostname
]) for hostname
in orch_hosts_map
153 hosts
.extend(orch_hosts_only
)
157 def get_hosts(from_ceph
=True, from_orchestrator
=True):
159 Get hosts from various sources.
171 'orchestrator': False
174 }) for server
in mgr
.list_servers()
176 if from_orchestrator
:
177 orch
= OrchClient
.instance()
179 return merge_hosts_by_hostname(ceph_hosts
, orch
.hosts
.list())
183 def get_host(hostname
: str) -> Dict
:
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.
189 for host
in get_hosts():
190 if host
['hostname'] == hostname
:
192 raise cherrypy
.HTTPError(404)
195 def get_device_osd_map():
196 """Get mappings from inventory devices to OSD IDs.
198 :return: Returns a dictionary containing mappings. Note one device might
199 shared between multiple OSDs.
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
:
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
)]
225 result
[hostname
][device
].append(int(osd_id
))
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.
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
237 :return: Returns list of inventory.
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
, []))
254 device
['osd_ids'] = []
255 return inventory_hosts
259 def add_host(hostname
: str, addr
: Optional
[str] = None,
260 labels
: Optional
[List
[str]] = None,
261 status
: Optional
[str] = None):
262 orch_client
= OrchClient
.instance()
264 host
.check_orchestrator_host_op(orch_client
, hostname
)
265 orch_client
.hosts
.add(hostname
, addr
, labels
)
266 if status
== 'maintenance':
267 orch_client
.hosts
.enter_maintenance(hostname
)
270 @ApiController('/host', Scope
.HOSTS
)
271 @ControllerDoc("Get Host Details", "Host")
272 class Host(RESTController
):
273 @EndpointDoc("List Host Specifications",
275 'sources': (str, 'Host Sources'),
277 responses
={200: LIST_HOST_SCHEMA
})
278 def list(self
, sources
=None):
281 _sources
= sources
.split(',')
282 from_ceph
= 'ceph' in _sources
283 from_orchestrator
= 'orchestrator' in _sources
284 return get_hosts(from_ceph
, from_orchestrator
)
286 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_CREATE
])
287 @handle_orchestrator_error('host')
288 @host_task('create', {'hostname': '{hostname}'})
291 'hostname': (str, 'Hostname'),
292 'addr': (str, 'Network Address'),
293 'labels': ([str], 'Host Labels'),
294 'status': (str, 'Host Status'),
296 responses
={200: None, 204: None})
297 @RESTController.MethodMap(version
='0.1')
298 def create(self
, hostname
: str,
299 addr
: Optional
[str] = None,
300 labels
: Optional
[List
[str]] = None,
301 status
: Optional
[str] = None): # pragma: no cover - requires realtime env
302 add_host(hostname
, addr
, labels
, status
)
304 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_DELETE
])
305 @handle_orchestrator_error('host')
306 @host_task('delete', {'hostname': '{hostname}'})
308 def delete(self
, hostname
): # pragma: no cover - requires realtime env
309 orch_client
= OrchClient
.instance()
310 self
.check_orchestrator_host_op(orch_client
, hostname
, False)
311 orch_client
.hosts
.remove(hostname
)
313 def check_orchestrator_host_op(self
, orch_client
, hostname
, add
=True): # pragma:no cover
314 """Check if we can adding or removing a host with orchestrator
316 :param orch_client: Orchestrator client
317 :param add: True for adding host operation, False for removing host
318 :raise DashboardException
320 host
= orch_client
.hosts
.get(hostname
)
322 raise DashboardException(
323 code
='orchestrator_add_existed_host',
324 msg
='{} is already in orchestrator'.format(hostname
),
325 component
='orchestrator')
326 if not add
and not host
:
327 raise DashboardException(
328 code
='orchestrator_remove_nonexistent_host',
329 msg
='Remove a non-existent host {} from orchestrator'.format(hostname
),
330 component
='orchestrator')
332 @RESTController.Resource('GET')
333 def devices(self
, hostname
):
335 return CephService
.get_devices_by_host(hostname
)
337 @RESTController.Resource('GET')
338 def smart(self
, hostname
):
339 # type: (str) -> dict
340 return CephService
.get_smart_data_by_host(hostname
)
342 @RESTController.Resource('GET')
343 @raise_if_no_orchestrator([OrchFeature
.DEVICE_LIST
])
344 @handle_orchestrator_error('host')
345 @EndpointDoc('Get inventory of a host',
347 'hostname': (str, 'Hostname'),
348 'refresh': (str, 'Trigger asynchronous refresh'),
350 responses
={200: INVENTORY_SCHEMA
})
351 def inventory(self
, hostname
, refresh
=None):
352 inventory
= get_inventories([hostname
], refresh
)
357 @RESTController.Resource('POST')
359 @raise_if_no_orchestrator([OrchFeature
.DEVICE_BLINK_LIGHT
])
360 @handle_orchestrator_error('host')
361 @host_task('identify_device', ['{hostname}', '{device}'], wait_for
=2.0)
362 def identify_device(self
, hostname
, device
, duration
):
363 # type: (str, str, int) -> None
365 Identify a device by switching on the device light for N seconds.
366 :param hostname: The hostname of the device to process.
367 :param device: The device identifier to process, e.g. ``/dev/dm-0`` or
368 ``ABC1234DEF567-1R1234_ABC8DE0Q``.
369 :param duration: The duration in seconds how long the LED should flash.
371 orch
= OrchClient
.instance()
372 TaskManager
.current_task().set_progress(0)
373 orch
.blink_device_light(hostname
, device
, 'ident', True)
374 for i
in range(int(duration
)):
375 percentage
= int(round(i
/ float(duration
) * 100))
376 TaskManager
.current_task().set_progress(percentage
)
378 orch
.blink_device_light(hostname
, device
, 'ident', False)
379 TaskManager
.current_task().set_progress(100)
381 @RESTController.Resource('GET')
382 @raise_if_no_orchestrator([OrchFeature
.DAEMON_LIST
])
383 def daemons(self
, hostname
: str) -> List
[dict]:
384 orch
= OrchClient
.instance()
385 daemons
= orch
.services
.list_daemons(hostname
=hostname
)
386 return [d
.to_dict() for d
in daemons
]
388 @handle_orchestrator_error('host')
389 def get(self
, hostname
: str) -> Dict
:
391 Get the specified host.
392 :raises: cherrypy.HTTPError: If host not found.
394 return get_host(hostname
)
396 @raise_if_no_orchestrator([OrchFeature
.HOST_LABEL_ADD
,
397 OrchFeature
.HOST_LABEL_REMOVE
,
398 OrchFeature
.HOST_MAINTENANCE_ENTER
,
399 OrchFeature
.HOST_MAINTENANCE_EXIT
])
400 @handle_orchestrator_error('host')
403 'hostname': (str, 'Hostname'),
404 'update_labels': (bool, 'Update Labels'),
405 'labels': ([str], 'Host Labels'),
406 'maintenance': (bool, 'Enter/Exit Maintenance'),
407 'force': (bool, 'Force Enter Maintenance')
409 responses
={200: None, 204: None})
410 @RESTController.MethodMap(version
='0.1')
411 def set(self
, hostname
: str, update_labels
: bool = False,
412 labels
: List
[str] = None, maintenance
: bool = False,
413 force
: bool = False):
415 Update the specified host.
416 Note, this is only supported when Ceph Orchestrator is enabled.
417 :param hostname: The name of the host to be processed.
418 :param update_labels: To update the labels.
419 :param labels: List of labels.
420 :param maintenance: Enter/Exit maintenance mode.
421 :param force: Force enter maintenance mode.
423 orch
= OrchClient
.instance()
424 host
= get_host(hostname
)
427 status
= host
['status']
428 if status
!= 'maintenance':
429 orch
.hosts
.enter_maintenance(hostname
, force
)
431 if status
== 'maintenance':
432 orch
.hosts
.exit_maintenance(hostname
)
435 # only allow List[str] type for labels
436 if not isinstance(labels
, list):
437 raise DashboardException(
438 msg
='Expected list of labels. Please check API documentation.',
439 http_status_code
=400,
440 component
='orchestrator')
441 current_labels
= set(host
['labels'])
443 remove_labels
= list(current_labels
.difference(set(labels
)))
444 for label
in remove_labels
:
445 orch
.hosts
.remove_label(hostname
, label
)
447 add_labels
= list(set(labels
).difference(current_labels
))
448 for label
in add_labels
:
449 orch
.hosts
.add_label(hostname
, label
)
452 @UiApiController('/host', Scope
.HOSTS
)
453 class HostUi(BaseController
):
456 @handle_orchestrator_error('host')
457 def labels(self
) -> List
[str]:
460 Note, host labels are only supported when Ceph Orchestrator is enabled.
461 If Ceph Orchestrator is not enabled, an empty list is returned.
462 :return: A list of all host labels.
465 orch
= OrchClient
.instance()
467 for host
in orch
.hosts
.list():
468 labels
.extend(host
.labels
)
470 return list(set(labels
)) # Filter duplicate labels.
474 @raise_if_no_orchestrator([OrchFeature
.DEVICE_LIST
])
475 @handle_orchestrator_error('host')
476 def inventory(self
, refresh
=None):
477 return get_inventories(None, refresh
)