From 1ee11d090d11fdb438abe3ed5073e39daa43c2e8 Mon Sep 17 00:00:00 2001 From: Pere Diaz Bou Date: Fri, 4 Mar 2022 09:58:36 +0100 Subject: [PATCH] mgr/dashboard: retrieve disk status Signed-off-by: Pere Diaz Bou (cherry picked from commit a1d1c853a5e4ff9a317591b99b75e005ccc862c9) --- src/pybind/mgr/dashboard/controllers/osd.py | 99 ++++++++++++++++++++- src/pybind/mgr/dashboard/tests/test_osd.py | 50 ++++++++++- 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index ceebb5acdafbf..ea12a842ea4bf 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -16,8 +16,8 @@ from ..services.exception import handle_orchestrator_error, handle_send_command_ from ..services.orchestrator import OrchClient, OrchFeature from ..tools import str_to_bool from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \ - EndpointDoc, ReadPermission, RESTController, Task, UpdatePermission, \ - allow_empty_body + EndpointDoc, ReadPermission, RESTController, Task, UIRouter, \ + UpdatePermission, allow_empty_body from ._version import APIVersion from .orchestrator import raise_if_no_orchestrator @@ -47,6 +47,60 @@ EXPORT_INDIV_FLAGS_GET_SCHEMA = { } +class DeploymentOption: + def __init__(self, name: str, available=False, capacity=0, used=0, hdd_used=0, + ssd_used=0, nvme_used=0): + self.name = name + self.available = available + self.capacity = capacity + self.used = used + self.hdd_used = hdd_used + self.ssd_used = ssd_used + self.nvme_used = nvme_used + + def as_dict(self): + return { + 'name': self.name, + 'available': self.available, + 'capacity': self.capacity, + 'used': self.used, + 'hdd_used': self.hdd_used, + 'ssd_used': self.ssd_used, + 'nvme_used': self.nvme_used + } + + +class DeploymentOptions: + def __init__(self): + self.options = { + 'cost-capacity': DeploymentOption('cost-capacity'), + 'throughput': DeploymentOption('throughput-optimized'), + 'iops': DeploymentOption('iops-optimized'), + } + self.recommended_option = None + + def as_dict(self): + return { + 'options': {k: v.as_dict() for k, v in self.options.items()}, + 'recommended_option': self.recommended_option + } + + +predefined_drive_groups = { + 'cost-capacity': { + 'service_type': 'osd', + 'placement': { + 'host_pattern': '*' + }, + 'data_devices': { + 'rotational': 1 + } + }, + 'throughput': {}, + 'iops': {}, +} + + def osd_task(name, metadata, wait_for=2.0): return Task("osd/{}".format(name), metadata, wait_for) @@ -291,6 +345,15 @@ class Osd(RESTController): id=int(svc_id), weight=float(weight)) + def _create_predefined_drive_group(self, data): + orch = OrchClient.instance() + if data == 'cost-capacity': + try: + orch.osds.create([DriveGroupSpec.from_json( + predefined_drive_groups['cost-capacity'])]) + except (ValueError, TypeError, DriveGroupValidationError) as e: + raise DashboardException(e, component='osd') + def _create_bare(self, data): """Create a OSD container that has no associated device. @@ -330,6 +393,8 @@ class Osd(RESTController): return self._create_bare(data) if method == 'drive_groups': return self._create_with_drive_groups(data) + if method == 'predefined': + return self._create_predefined_drive_group(data) raise DashboardException( component='osd', http_status_code=400, msg='Unknown method: {}'.format(method)) @@ -405,6 +470,36 @@ class Osd(RESTController): return CephService.send_command('mon', 'device ls-by-daemon', who='osd.{}'.format(svc_id)) +@UIRouter('/osd', Scope.OSD) +@APIDoc("Dashboard UI helper function; not part of the public API", "OsdUI") +class OsdUi(Osd): + @Endpoint('GET', version=APIVersion.EXPERIMENTAL) + @ReadPermission + @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST]) + @handle_orchestrator_error('host') + def deployment_options(self): + orch = OrchClient.instance() + hdds = 0 + ssds = 0 + nvmes = 0 + res = DeploymentOptions() + devices = {} + for inventory_host in orch.inventory.list(hosts=None, refresh=True): + for device in inventory_host.devices.devices: + if device.available: + devices[device.path] = device + if device.human_readable_type == 'hdd': + hdds += 1 + elif device.human_readable_type == 'ssd': + ssds += 1 + elif device.human_readable_type == 'nvme': + nvmes += 1 + if hdds: + res.options['cost-capacity'].available = True + res.recommended_option = 'cost-capacity' + return res.as_dict() + + @APIRouter('/osd/flags', Scope.OSD) @APIDoc(group='OSD') class OsdFlagsController(RESTController): diff --git a/src/pybind/mgr/dashboard/tests/test_osd.py b/src/pybind/mgr/dashboard/tests/test_osd.py index 775c6ca739708..33b7ebcaea0d8 100644 --- a/src/pybind/mgr/dashboard/tests/test_osd.py +++ b/src/pybind/mgr/dashboard/tests/test_osd.py @@ -8,7 +8,8 @@ from ceph.deployment.drive_group import DeviceSelection, DriveGroupSpec # type: from ceph.deployment.service_spec import PlacementSpec # type: ignore from .. import mgr -from ..controllers.osd import Osd +from ..controllers._version import APIVersion +from ..controllers.osd import Osd, OsdUi from ..tests import ControllerTestCase from ..tools import NotificationQueue, TaskManager from .helper import update_dict # pylint: disable=import-error @@ -191,7 +192,7 @@ class OsdHelper(object): class OsdTest(ControllerTestCase): @classmethod def setup_server(cls): - cls.setup_controllers([Osd]) + cls.setup_controllers([Osd, OsdUi]) NotificationQueue.start_queue() TaskManager.init() @@ -374,3 +375,48 @@ class OsdTest(ControllerTestCase): self._task_post('/api/osd/1/reweight', {'weight': '1'}) instance.send_command.assert_called_with('mon', 'osd reweight', id=1, weight=1.0) self.assertStatus(200) + + @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance') + def test_deployment_options(self, instance): + fake_client = mock.Mock() + instance.return_value = fake_client + fake_client.get_missing_features.return_value = [] + + class MockDevice: + def __init__(self, human_readable_type, path, available=True): + self.human_readable_type = human_readable_type + self.available = available + self.path = path + + def create_invetory_host(devices_data): + inventory_host = mock.Mock() + inventory_host.devices.devices = [] + for data in devices_data: + inventory_host.devices.devices.append(MockDevice(data['type'], data['path'])) + return inventory_host + + devices_data = [ + {'type': 'hdd', 'path': '/dev/sda'}, + {'type': 'hdd', 'path': '/dev/sdb'}, + {'type': 'hdd', 'path': '/dev/sdc'}, + {'type': 'hdd', 'path': '/dev/sdd'}, + {'type': 'hdd', 'path': '/dev/sde'}, + ] + inventory_host = create_invetory_host(devices_data) + fake_client.inventory.list.return_value = [inventory_host] + self._get('/ui-api/osd/deployment_options', version=APIVersion(0, 1)) + self.assertStatus(200) + res = self.json_body() + self.assertTrue(res['options']['cost-capacity']['available']) + assert res['recommended_option'] == 'cost-capacity' + + for data in devices_data: + data['type'] = 'ssd' + inventory_host = create_invetory_host(devices_data) + fake_client.inventory.list.return_value = [inventory_host] + + self._get('/ui-api/osd/deployment_options', version=APIVersion(0, 1)) + self.assertStatus(200) + res = self.json_body() + self.assertFalse(res['options']['cost-capacity']['available']) + self.assertIsNone(res['recommended_option']) -- 2.39.5