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
}
+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)
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.
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))
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):
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
class OsdTest(ControllerTestCase):
@classmethod
def setup_server(cls):
- cls.setup_controllers([Osd])
+ cls.setup_controllers([Osd, OsdUi])
NotificationQueue.start_queue()
TaskManager.init()
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'])