1 # -*- coding: utf-8 -*-
6 from typing
import Dict
, List
, Optional
9 from mgr_util
import merge_dicts
10 from orchestrator
import HostSpec
13 from ..exceptions
import DashboardException
14 from ..security
import Scope
15 from ..services
.ceph_service
import CephService
16 from ..services
.exception
import handle_orchestrator_error
17 from ..services
.orchestrator
import OrchClient
, OrchFeature
18 from ..tools
import TaskManager
, merge_list_of_dicts_by_key
, str_to_bool
19 from . import APIDoc
, APIRouter
, BaseController
, Endpoint
, EndpointDoc
, \
20 ReadPermission
, RESTController
, Task
, UIRouter
, UpdatePermission
, \
22 from ._version
import APIVersion
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(sources
=None):
159 Get hosts from various sources.
162 from_orchestrator
= True
164 _sources
= sources
.split(',')
165 from_ceph
= 'ceph' in _sources
166 from_orchestrator
= 'orchestrator' in _sources
177 'orchestrator': False
180 }) for server
in mgr
.list_servers()
182 if from_orchestrator
:
183 orch
= OrchClient
.instance()
185 return merge_hosts_by_hostname(ceph_hosts
, orch
.hosts
.list())
189 def get_host(hostname
: str) -> Dict
:
191 Get a specific host from Ceph or Orchestrator (if available).
192 :param hostname: The name of the host to fetch.
193 :raises: cherrypy.HTTPError: If host not found.
195 for host
in get_hosts():
196 if host
['hostname'] == hostname
:
198 raise cherrypy
.HTTPError(404)
201 def get_device_osd_map():
202 """Get mappings from inventory devices to OSD IDs.
204 :return: Returns a dictionary containing mappings. Note one device might
205 shared between multiple OSDs.
219 for osd_id
, osd_metadata
in mgr
.get('osd_metadata').items():
220 hostname
= osd_metadata
.get('hostname')
221 devices
= osd_metadata
.get('devices')
222 if not hostname
or not devices
:
224 if hostname
not in result
:
225 result
[hostname
] = {}
226 # for OSD contains multiple devices, devices is in `sda,sdb`
227 for device
in devices
.split(','):
228 if device
not in result
[hostname
]:
229 result
[hostname
][device
] = [int(osd_id
)]
231 result
[hostname
][device
].append(int(osd_id
))
235 def get_inventories(hosts
: Optional
[List
[str]] = None,
236 refresh
: Optional
[bool] = None) -> List
[dict]:
237 """Get inventories from the Orchestrator and link devices with OSD IDs.
239 :param hosts: Hostnames to query.
240 :param refresh: Ask the Orchestrator to refresh the inventories. Note the this is an
241 asynchronous operation, the updated version of inventories need to
243 :return: Returns list of inventory.
247 if refresh
is not None:
248 do_refresh
= str_to_bool(refresh
)
249 orch
= OrchClient
.instance()
250 inventory_hosts
= [host
.to_json()
251 for host
in orch
.inventory
.list(hosts
=hosts
, refresh
=do_refresh
)]
252 device_osd_map
= get_device_osd_map()
253 for inventory_host
in inventory_hosts
:
254 host_osds
= device_osd_map
.get(inventory_host
['name'])
255 for device
in inventory_host
['devices']:
256 if host_osds
: # pragma: no cover
257 dev_name
= os
.path
.basename(device
['path'])
258 device
['osd_ids'] = sorted(host_osds
.get(dev_name
, []))
260 device
['osd_ids'] = []
261 return inventory_hosts
265 def add_host(hostname
: str, addr
: Optional
[str] = None,
266 labels
: Optional
[List
[str]] = None,
267 status
: Optional
[str] = None):
268 orch_client
= OrchClient
.instance()
270 host
.check_orchestrator_host_op(orch_client
, hostname
)
271 orch_client
.hosts
.add(hostname
, addr
, labels
)
272 if status
== 'maintenance':
273 orch_client
.hosts
.enter_maintenance(hostname
)
276 @APIRouter('/host', Scope
.HOSTS
)
277 @APIDoc("Get Host Details", "Host")
278 class Host(RESTController
):
279 @EndpointDoc("List Host Specifications",
281 'sources': (str, 'Host Sources'),
282 'facts': (bool, 'Host Facts')
284 responses
={200: LIST_HOST_SCHEMA
})
285 @RESTController.MethodMap(version
=APIVersion(1, 1))
286 def list(self
, sources
=None, facts
=False):
287 hosts
= get_hosts(sources
)
288 orch
= OrchClient
.instance()
289 if str_to_bool(facts
):
291 if not orch
.get_missing_features(['get_facts']):
292 hosts_facts
= orch
.hosts
.get_facts()
293 return merge_list_of_dicts_by_key(hosts
, hosts_facts
, 'hostname')
295 raise DashboardException(
296 code
='invalid_orchestrator_backend', # pragma: no cover
297 msg
="Please enable the cephadm orchestrator backend "
298 "(try `ceph orch set backend cephadm`)",
299 component
='orchestrator',
300 http_status_code
=400)
302 raise DashboardException(code
='orchestrator_status_unavailable', # pragma: no cover
303 msg
="Please configure and enable the orchestrator if you "
304 "really want to gather facts from hosts",
305 component
='orchestrator',
306 http_status_code
=400)
309 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_ADD
])
310 @handle_orchestrator_error('host')
311 @host_task('add', {'hostname': '{hostname}'})
314 'hostname': (str, 'Hostname'),
315 'addr': (str, 'Network Address'),
316 'labels': ([str], 'Host Labels'),
317 'status': (str, 'Host Status')
319 responses
={200: None, 204: None})
320 @RESTController.MethodMap(version
=APIVersion
.EXPERIMENTAL
)
321 def create(self
, hostname
: str,
322 addr
: Optional
[str] = None,
323 labels
: Optional
[List
[str]] = None,
324 status
: Optional
[str] = None): # pragma: no cover - requires realtime env
325 add_host(hostname
, addr
, labels
, status
)
327 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_REMOVE
])
328 @handle_orchestrator_error('host')
329 @host_task('remove', {'hostname': '{hostname}'})
331 def delete(self
, hostname
): # pragma: no cover - requires realtime env
332 orch_client
= OrchClient
.instance()
333 self
.check_orchestrator_host_op(orch_client
, hostname
, False)
334 orch_client
.hosts
.remove(hostname
)
336 def check_orchestrator_host_op(self
, orch_client
, hostname
, add
=True): # pragma:no cover
337 """Check if we can adding or removing a host with orchestrator
339 :param orch_client: Orchestrator client
340 :param add: True for adding host operation, False for removing host
341 :raise DashboardException
343 host
= orch_client
.hosts
.get(hostname
)
345 raise DashboardException(
346 code
='orchestrator_add_existed_host',
347 msg
='{} is already in orchestrator'.format(hostname
),
348 component
='orchestrator')
349 if not add
and not host
:
350 raise DashboardException(
351 code
='orchestrator_remove_nonexistent_host',
352 msg
='Remove a non-existent host {} from orchestrator'.format(hostname
),
353 component
='orchestrator')
355 @RESTController.Resource('GET')
356 def devices(self
, hostname
):
358 return CephService
.get_devices_by_host(hostname
)
360 @RESTController.Resource('GET')
361 def smart(self
, hostname
):
362 # type: (str) -> dict
363 return CephService
.get_smart_data_by_host(hostname
)
365 @RESTController.Resource('GET')
366 @raise_if_no_orchestrator([OrchFeature
.DEVICE_LIST
])
367 @handle_orchestrator_error('host')
368 @EndpointDoc('Get inventory of a host',
370 'hostname': (str, 'Hostname'),
371 'refresh': (str, 'Trigger asynchronous refresh'),
373 responses
={200: INVENTORY_SCHEMA
})
374 def inventory(self
, hostname
, refresh
=None):
375 inventory
= get_inventories([hostname
], refresh
)
380 @RESTController.Resource('POST')
382 @raise_if_no_orchestrator([OrchFeature
.DEVICE_BLINK_LIGHT
])
383 @handle_orchestrator_error('host')
384 @host_task('identify_device', ['{hostname}', '{device}'], wait_for
=2.0)
385 def identify_device(self
, hostname
, device
, duration
):
386 # type: (str, str, int) -> None
388 Identify a device by switching on the device light for N seconds.
389 :param hostname: The hostname of the device to process.
390 :param device: The device identifier to process, e.g. ``/dev/dm-0`` or
391 ``ABC1234DEF567-1R1234_ABC8DE0Q``.
392 :param duration: The duration in seconds how long the LED should flash.
394 orch
= OrchClient
.instance()
395 TaskManager
.current_task().set_progress(0)
396 orch
.blink_device_light(hostname
, device
, 'ident', True)
397 for i
in range(int(duration
)):
398 percentage
= int(round(i
/ float(duration
) * 100))
399 TaskManager
.current_task().set_progress(percentage
)
401 orch
.blink_device_light(hostname
, device
, 'ident', False)
402 TaskManager
.current_task().set_progress(100)
404 @RESTController.Resource('GET')
405 @raise_if_no_orchestrator([OrchFeature
.DAEMON_LIST
])
406 def daemons(self
, hostname
: str) -> List
[dict]:
407 orch
= OrchClient
.instance()
408 daemons
= orch
.services
.list_daemons(hostname
=hostname
)
409 return [d
.to_dict() for d
in daemons
]
411 @handle_orchestrator_error('host')
412 def get(self
, hostname
: str) -> Dict
:
414 Get the specified host.
415 :raises: cherrypy.HTTPError: If host not found.
417 return get_host(hostname
)
419 @raise_if_no_orchestrator([OrchFeature
.HOST_LABEL_ADD
,
420 OrchFeature
.HOST_LABEL_REMOVE
,
421 OrchFeature
.HOST_MAINTENANCE_ENTER
,
422 OrchFeature
.HOST_MAINTENANCE_EXIT
,
423 OrchFeature
.HOST_DRAIN
])
424 @handle_orchestrator_error('host')
427 'hostname': (str, 'Hostname'),
428 'update_labels': (bool, 'Update Labels'),
429 'labels': ([str], 'Host Labels'),
430 'maintenance': (bool, 'Enter/Exit Maintenance'),
431 'force': (bool, 'Force Enter Maintenance'),
432 'drain': (bool, 'Drain Host')
434 responses
={200: None, 204: None})
435 @RESTController.MethodMap(version
=APIVersion
.EXPERIMENTAL
)
436 def set(self
, hostname
: str, update_labels
: bool = False,
437 labels
: List
[str] = None, maintenance
: bool = False,
438 force
: bool = False, drain
: bool = False):
440 Update the specified host.
441 Note, this is only supported when Ceph Orchestrator is enabled.
442 :param hostname: The name of the host to be processed.
443 :param update_labels: To update the labels.
444 :param labels: List of labels.
445 :param maintenance: Enter/Exit maintenance mode.
446 :param force: Force enter maintenance mode.
447 :param drain: Drain host
449 orch
= OrchClient
.instance()
450 host
= get_host(hostname
)
453 status
= host
['status']
454 if status
!= 'maintenance':
455 orch
.hosts
.enter_maintenance(hostname
, force
)
457 if status
== 'maintenance':
458 orch
.hosts
.exit_maintenance(hostname
)
461 orch
.hosts
.drain(hostname
)
464 # only allow List[str] type for labels
465 if not isinstance(labels
, list):
466 raise DashboardException(
467 msg
='Expected list of labels. Please check API documentation.',
468 http_status_code
=400,
469 component
='orchestrator')
470 current_labels
= set(host
['labels'])
472 remove_labels
= list(current_labels
.difference(set(labels
)))
473 for label
in remove_labels
:
474 orch
.hosts
.remove_label(hostname
, label
)
476 add_labels
= list(set(labels
).difference(current_labels
))
477 for label
in add_labels
:
478 orch
.hosts
.add_label(hostname
, label
)
481 @UIRouter('/host', Scope
.HOSTS
)
482 class HostUi(BaseController
):
485 @handle_orchestrator_error('host')
486 def labels(self
) -> List
[str]:
489 Note, host labels are only supported when Ceph Orchestrator is enabled.
490 If Ceph Orchestrator is not enabled, an empty list is returned.
491 :return: A list of all host labels.
494 orch
= OrchClient
.instance()
496 for host
in orch
.hosts
.list():
497 labels
.extend(host
.labels
)
499 return list(set(labels
)) # Filter duplicate labels.
503 @raise_if_no_orchestrator([OrchFeature
.DEVICE_LIST
])
504 @handle_orchestrator_error('host')
505 def inventory(self
, refresh
=None):
506 return get_inventories(None, refresh
)