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
258 @ApiController('/host', Scope
.HOSTS
)
259 @ControllerDoc("Get Host Details", "Host")
260 class Host(RESTController
):
261 @EndpointDoc("List Host Specifications",
263 'sources': (str, 'Host Sources'),
265 responses
={200: LIST_HOST_SCHEMA
})
266 def list(self
, sources
=None):
269 _sources
= sources
.split(',')
270 from_ceph
= 'ceph' in _sources
271 from_orchestrator
= 'orchestrator' in _sources
272 return get_hosts(from_ceph
, from_orchestrator
)
274 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_CREATE
])
275 @handle_orchestrator_error('host')
276 @host_task('create', {'hostname': '{hostname}'})
277 def create(self
, hostname
): # pragma: no cover - requires realtime env
278 orch_client
= OrchClient
.instance()
279 self
._check
_orchestrator
_host
_op
(orch_client
, hostname
, True)
280 orch_client
.hosts
.add(hostname
)
281 create
._cp
_config
= {'tools.json_in.force': False} # pylint: disable=W0212
283 @raise_if_no_orchestrator([OrchFeature
.HOST_LIST
, OrchFeature
.HOST_DELETE
])
284 @handle_orchestrator_error('host')
285 @host_task('delete', {'hostname': '{hostname}'})
287 def delete(self
, hostname
): # pragma: no cover - requires realtime env
288 orch_client
= OrchClient
.instance()
289 self
._check
_orchestrator
_host
_op
(orch_client
, hostname
, False)
290 orch_client
.hosts
.remove(hostname
)
292 def _check_orchestrator_host_op(self
, orch_client
, hostname
, add_host
=True): # pragma:no cover
293 """Check if we can adding or removing a host with orchestrator
295 :param orch_client: Orchestrator client
296 :param add: True for adding host operation, False for removing host
297 :raise DashboardException
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(
310 component
='orchestrator')
312 @RESTController.Resource('GET')
313 def devices(self
, hostname
):
315 return CephService
.get_devices_by_host(hostname
)
317 @RESTController.Resource('GET')
318 def smart(self
, hostname
):
319 # type: (str) -> dict
320 return CephService
.get_smart_data_by_host(hostname
)
322 @RESTController.Resource('GET')
323 @raise_if_no_orchestrator([OrchFeature
.DEVICE_LIST
])
324 @handle_orchestrator_error('host')
325 @EndpointDoc('Get inventory of a host',
327 'hostname': (str, 'Hostname'),
328 'refresh': (str, 'Trigger asynchronous refresh'),
330 responses
={200: INVENTORY_SCHEMA
})
331 def inventory(self
, hostname
, refresh
=None):
332 inventory
= get_inventories([hostname
], refresh
)
337 @RESTController.Resource('POST')
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
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.
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
)
358 orch
.blink_device_light(hostname
, device
, 'ident', False)
359 TaskManager
.current_task().set_progress(100)
361 @RESTController.Resource('GET')
362 @raise_if_no_orchestrator([OrchFeature
.DAEMON_LIST
])
363 def daemons(self
, hostname
: str) -> List
[dict]:
364 orch
= OrchClient
.instance()
365 daemons
= orch
.services
.list_daemons(hostname
=hostname
)
366 return [d
.to_json() for d
in daemons
]
368 @handle_orchestrator_error('host')
369 def get(self
, hostname
: str) -> Dict
:
371 Get the specified host.
372 :raises: cherrypy.HTTPError: If host not found.
374 return get_host(hostname
)
376 @raise_if_no_orchestrator([OrchFeature
.HOST_LABEL_ADD
,
377 OrchFeature
.HOST_LABEL_REMOVE
,
378 OrchFeature
.HOST_MAINTENANCE_ENTER
,
379 OrchFeature
.HOST_MAINTENANCE_EXIT
])
380 @handle_orchestrator_error('host')
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')
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):
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.
397 :param update_labels: To update the labels.
398 :param labels: List of labels.
399 :param maintenance: Enter/Exit maintenance mode.
400 :param force: Force enter maintenance mode.
402 orch
= OrchClient
.instance()
403 host
= get_host(hostname
)
406 status
= host
['status']
407 if status
!= 'maintenance':
408 orch
.hosts
.enter_maintenance(hostname
, force
)
410 if status
== 'maintenance':
411 orch
.hosts
.exit_maintenance(hostname
)
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'])
422 remove_labels
= list(current_labels
.difference(set(labels
)))
423 for label
in remove_labels
:
424 orch
.hosts
.remove_label(hostname
, label
)
426 add_labels
= list(set(labels
).difference(current_labels
))
427 for label
in add_labels
:
428 orch
.hosts
.add_label(hostname
, label
)
431 @UiApiController('/host', Scope
.HOSTS
)
432 class HostUi(BaseController
):
435 @handle_orchestrator_error('host')
436 def labels(self
) -> List
[str]:
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.
444 orch
= OrchClient
.instance()
446 for host
in orch
.hosts
.list():
447 labels
.extend(host
.labels
)
449 return list(set(labels
)) # Filter duplicate labels.
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
)