]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: retrieve disk status
authorPere Diaz Bou <pdiazbou@redhat.com>
Fri, 4 Mar 2022 08:58:36 +0000 (09:58 +0100)
committerPere Diaz Bou <pdiazbou@redhat.com>
Wed, 15 Jun 2022 09:10:05 +0000 (11:10 +0200)
Signed-off-by: Pere Diaz Bou <pdiazbou@redhat.com>
(cherry picked from commit a1d1c853a5e4ff9a317591b99b75e005ccc862c9)

Conflicts:
src/pybind/mgr/dashboard/tests/test_osd.py

There were some extra tests that came with this commit. This was
resolved by adding test_deployment_options alone instead of all the
incoming changes.

src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/tests/test_osd.py

index ca4bf58c683102a3066cef9c8ffc597aa4a9b2b0..d52b652ee210503abcb10544f415e2d2f648e3c3 100644 (file)
@@ -17,8 +17,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
 
@@ -48,6 +48,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)
 
@@ -292,6 +346,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.
 
@@ -331,6 +394,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))
 
@@ -406,6 +471,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):
index 790c9b359262cab34c0bea42fd5707e2aef68a6b..775714dcac1231cdf86c42db0cd230366520bb00 100644 (file)
@@ -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()
 
@@ -316,3 +317,48 @@ class OsdTest(ControllerTestCase):
             data = {'action': action}
             self._task_put('/api/osd/1/mark', data)
             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'])