diff --git a/qa/tasks/mgr/dashboard_v2/test_rbd.py b/qa/tasks/mgr/dashboard_v2/test_rbd.py index eeae1d289bc..0835fc27fff 100644 --- a/qa/tasks/mgr/dashboard_v2/test_rbd.py +++ b/qa/tasks/mgr/dashboard_v2/test_rbd.py @@ -2,6 +2,8 @@ from __future__ import absolute_import +import unittest + from .helper import DashboardTestCase, authenticate @@ -40,3 +42,72 @@ class RbdTest(DashboardTestCase): self.assertEqual(img2['obj_size'], 4194304) self.assertEqual(img2['features_name'], 'deep-flatten, exclusive-lock, fast-diff, layering, object-map') + + @authenticate + def test_create(self): + rbd_name = 'test_rbd' + data = {'pool_name': 'rbd', + 'name': rbd_name, + 'size': 10240} + self._post('/api/rbd', data) + self.assertStatus(201) + self.assertJsonBody({"success": True}) + + # TODO: change to GET the specific RBD instead of the list as soon as it is available? + get_res = self._get('/api/rbd/rbd') + self.assertStatus(200) + + for rbd in get_res['value']: + if rbd['name'] == rbd_name: + self.assertEqual(rbd['size'], 10240) + self.assertEqual(rbd['num_objs'], 1) + self.assertEqual(rbd['obj_size'], 4194304) + self.assertEqual(rbd['features_name'], + 'deep-flatten, exclusive-lock, fast-diff, layering, object-map') + break + + # TODO: Re-enable this test for bluestore cluster by figuring out how to skip none-bluestore + # ones automatically + @unittest.skip("requires bluestore cluster") + @authenticate + def test_create_rbd_in_data_pool(self): + self._ceph_cmd(['osd', 'pool', 'create', 'data_pool', '12', '12', 'erasure']) + self._ceph_cmd(['osd', 'pool', 'application', 'enable', 'data_pool', 'rbd']) + self._ceph_cmd(['osd', 'pool', 'set', 'data_pool', 'allow_ec_overwrites', 'true']) + + rbd_name = 'test_rbd_in_data_pool' + data = {'pool_name': 'rbd', + 'name': rbd_name, + 'size': 10240, + 'data_pool': 'data_pool'} + self._post('/api/rbd', data) + self.assertStatus(201) + self.assertJsonBody({"success": True}) + + # TODO: possibly change to GET the specific RBD (see above) + get_res = self._get('/api/rbd/rbd') + self.assertStatus(200) + + for rbd in get_res['value']: + if rbd['name'] == rbd_name: + self.assertEqual(rbd['size'], 10240) + self.assertEqual(rbd['num_objs'], 1) + self.assertEqual(rbd['obj_size'], 4194304) + self.assertEqual(rbd['features_name'], 'data-pool, deep-flatten, exclusive-lock, ' + 'fast-diff, layering, object-map') + break + + self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool', + '--yes-i-really-really-mean-it']) + + @authenticate + def test_create_rbd_twice(self): + data = {'pool_name': 'rbd', + 'name': 'test_rbd_twice', + 'size': 10240} + self._post('/api/rbd', data) + + self._post('/api/rbd', data) + self.assertStatus(400) + self.assertJsonBody({"success": False, "errno": 17, + "detail": "[errno 17] error creating image"}) diff --git a/src/pybind/mgr/dashboard_v2/controllers/rbd.py b/src/pybind/mgr/dashboard_v2/controllers/rbd.py index 87a5d767828..b73697b0a16 100644 --- a/src/pybind/mgr/dashboard_v2/controllers/rbd.py +++ b/src/pybind/mgr/dashboard_v2/controllers/rbd.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import +import math +import cherrypy import rbd from .. import mgr @@ -11,6 +13,18 @@ from ..tools import ApiController, AuthRequired, RESTController, ViewCache @AuthRequired() class Rbd(RESTController): + RBD_FEATURES_NAME_MAPPING = { + rbd.RBD_FEATURE_LAYERING: "layering", + rbd.RBD_FEATURE_STRIPINGV2: "striping", + rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock", + rbd.RBD_FEATURE_OBJECT_MAP: "object-map", + rbd.RBD_FEATURE_FAST_DIFF: "fast-diff", + rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten", + rbd.RBD_FEATURE_JOURNALING: "journaling", + rbd.RBD_FEATURE_DATA_POOL: "data-pool", + rbd.RBD_FEATURE_OPERATIONS: "operations", + } + def __init__(self): self.rbd = None @@ -22,21 +36,33 @@ class Rbd(RESTController): >>> Rbd._format_bitmask(45) 'deep-flatten, exclusive-lock, layering, object-map' """ - RBD_FEATURES_NAME_MAPPING = { - rbd.RBD_FEATURE_LAYERING: "layering", - rbd.RBD_FEATURE_STRIPINGV2: "striping", - rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock", - rbd.RBD_FEATURE_OBJECT_MAP: "object-map", - rbd.RBD_FEATURE_FAST_DIFF: "fast-diff", - rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten", - rbd.RBD_FEATURE_JOURNALING: "journaling", - rbd.RBD_FEATURE_DATA_POOL: "data-pool", - rbd.RBD_FEATURE_OPERATIONS: "operations", - } - names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items() + names = [val for key, val in Rbd.RBD_FEATURES_NAME_MAPPING.items() if key & features == key] return ', '.join(sorted(names)) + @staticmethod + def _format_features(features): + """ + Converts the features list to bitmask: + + >>> Rbd._format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map']) + 45 + + >>> Rbd._format_features(None) is None + True + + >>> Rbd._format_features('not a list') is None + True + """ + if not features or not isinstance(features, list): + return None + + res = 0 + for key, value in Rbd.RBD_FEATURES_NAME_MAPPING.items(): + if value in features: + res = key | res + return res + @ViewCache() def _rbd_list(self, pool_name): ioctx = mgr.rados.open_ioctx(pool_name) @@ -68,3 +94,36 @@ class Rbd(RESTController): if status == ViewCache.VALUE_EXCEPTION: raise value return {'status': status, 'value': value} + + def create(self, data): + if not self.rbd: + self.rbd = rbd.RBD() + + # Get input values + name = data.get('name') + pool_name = data.get('pool_name') + size = data.get('size') + obj_size = data.get('obj_size') + features = data.get('features') + stripe_unit = data.get('stripe_unit') + stripe_count = data.get('stripe_count') + data_pool = data.get('data_pool') + + # Set order + order = None + if obj_size and obj_size > 0: + order = int(round(math.log(float(obj_size), 2))) + + # Set features + feature_bitmask = self._format_features(features) + + ioctx = mgr.rados.open_ioctx(pool_name) + + try: + self.rbd.create(ioctx, name, size, order=order, old_format=False, + features=feature_bitmask, stripe_unit=stripe_unit, + stripe_count=stripe_count, data_pool=data_pool) + except rbd.OSError as e: + cherrypy.response.status = 400 + return {'success': False, 'detail': str(e), 'errno': e.errno} + return {'success': True}