]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/host.py
import ceph pacific 16.2.5
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / host.py
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import
3
4 import copy
5 import os
6 import time
7 from typing import Dict, List, Optional
8
9 import cherrypy
10 from mgr_util import merge_dicts
11 from orchestrator import HostSpec
12
13 from .. import mgr
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
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 }
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]
117 """
118 Merge Ceph hosts with orchestrator hosts by hostnames.
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 """
126 hosts = copy.deepcopy(ceph_hosts)
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.
134 for host in hosts:
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)
140
141 # Hosts only in Orchestrator.
142 orch_hosts_only = [
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
152 ]
153 hosts.extend(orch_hosts_only)
154 return hosts
155
156
157 def get_hosts(from_ceph=True, from_orchestrator=True):
158 """
159 Get hosts from various sources.
160 """
161 ceph_hosts = []
162 if from_ceph:
163 ceph_hosts = [
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()
175 ]
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
181
182
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
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
258 @allow_empty_body
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()
263 host = Host()
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)
268
269
270 @ApiController('/host', Scope.HOSTS)
271 @ControllerDoc("Get Host Details", "Host")
272 class Host(RESTController):
273 @EndpointDoc("List Host Specifications",
274 parameters={
275 'sources': (str, 'Host Sources'),
276 },
277 responses={200: LIST_HOST_SCHEMA})
278 def list(self, sources=None):
279 if sources is None:
280 return get_hosts()
281 _sources = sources.split(',')
282 from_ceph = 'ceph' in _sources
283 from_orchestrator = 'orchestrator' in _sources
284 return get_hosts(from_ceph, from_orchestrator)
285
286 @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_CREATE])
287 @handle_orchestrator_error('host')
288 @host_task('create', {'hostname': '{hostname}'})
289 @EndpointDoc('',
290 parameters={
291 'hostname': (str, 'Hostname'),
292 'addr': (str, 'Network Address'),
293 'labels': ([str], 'Host Labels'),
294 'status': (str, 'Host Status'),
295 },
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)
303
304 @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE])
305 @handle_orchestrator_error('host')
306 @host_task('delete', {'hostname': '{hostname}'})
307 @allow_empty_body
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)
312
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
315
316 :param orch_client: Orchestrator client
317 :param add: True for adding host operation, False for removing host
318 :raise DashboardException
319 """
320 host = orch_client.hosts.get(hostname)
321 if add and host:
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')
331
332 @RESTController.Resource('GET')
333 def devices(self, hostname):
334 # (str) -> List
335 return CephService.get_devices_by_host(hostname)
336
337 @RESTController.Resource('GET')
338 def smart(self, hostname):
339 # type: (str) -> dict
340 return CephService.get_smart_data_by_host(hostname)
341
342 @RESTController.Resource('GET')
343 @raise_if_no_orchestrator([OrchFeature.DEVICE_LIST])
344 @handle_orchestrator_error('host')
345 @EndpointDoc('Get inventory of a host',
346 parameters={
347 'hostname': (str, 'Hostname'),
348 'refresh': (str, 'Trigger asynchronous refresh'),
349 },
350 responses={200: INVENTORY_SCHEMA})
351 def inventory(self, hostname, refresh=None):
352 inventory = get_inventories([hostname], refresh)
353 if inventory:
354 return inventory[0]
355 return {}
356
357 @RESTController.Resource('POST')
358 @UpdatePermission
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
364 """
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.
370 """
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)
377 time.sleep(1)
378 orch.blink_device_light(hostname, device, 'ident', False)
379 TaskManager.current_task().set_progress(100)
380
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]
387
388 @handle_orchestrator_error('host')
389 def get(self, hostname: str) -> Dict:
390 """
391 Get the specified host.
392 :raises: cherrypy.HTTPError: If host not found.
393 """
394 return get_host(hostname)
395
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')
401 @EndpointDoc('',
402 parameters={
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')
408 },
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):
414 """
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.
422 """
423 orch = OrchClient.instance()
424 host = get_host(hostname)
425
426 if maintenance:
427 status = host['status']
428 if status != 'maintenance':
429 orch.hosts.enter_maintenance(hostname, force)
430
431 if status == 'maintenance':
432 orch.hosts.exit_maintenance(hostname)
433
434 if update_labels:
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'])
442 # Remove labels.
443 remove_labels = list(current_labels.difference(set(labels)))
444 for label in remove_labels:
445 orch.hosts.remove_label(hostname, label)
446 # Add labels.
447 add_labels = list(set(labels).difference(current_labels))
448 for label in add_labels:
449 orch.hosts.add_label(hostname, label)
450
451
452 @UiApiController('/host', Scope.HOSTS)
453 class HostUi(BaseController):
454 @Endpoint('GET')
455 @ReadPermission
456 @handle_orchestrator_error('host')
457 def labels(self) -> List[str]:
458 """
459 Get all host labels.
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.
463 """
464 labels = []
465 orch = OrchClient.instance()
466 if orch.available():
467 for host in orch.hosts.list():
468 labels.extend(host.labels)
469 labels.sort()
470 return list(set(labels)) # Filter duplicate labels.
471
472 @Endpoint('GET')
473 @ReadPermission
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)