]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/controllers/host.py
update source to Ceph Pacific 16.2.2
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / host.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
f6b5b4d7 3
9f95a23c 4import copy
f67539c2
TL
5import os
6import time
7from typing import Dict, List, Optional
f6b5b4d7
TL
8
9import cherrypy
9f95a23c
TL
10from mgr_util import merge_dicts
11from orchestrator import HostSpec
f67539c2 12
11fdf7f2 13from .. import mgr
9f95a23c 14from ..exceptions import DashboardException
11fdf7f2 15from ..security import Scope
9f95a23c
TL
16from ..services.ceph_service import CephService
17from ..services.exception import handle_orchestrator_error
f67539c2
TL
18from ..services.orchestrator import OrchClient, OrchFeature
19from ..tools import TaskManager, str_to_bool
20from . import ApiController, BaseController, ControllerDoc, Endpoint, \
21 EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \
22 UpdatePermission, allow_empty_body
23from .orchestrator import raise_if_no_orchestrator
24
25LIST_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
42INVENTORY_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
111def host_task(name, metadata, wait_for=10.0):
112 return Task("host/{}".format(name), metadata, wait_for)
113
114
115def 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
157def 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
183def 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
195def 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
229def 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 260class 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)
432class 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)