]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: allow to get/update RBD image metadata via REST API
authorMykola Golub <mykola.golub@clyso.com>
Sun, 21 Aug 2022 10:15:30 +0000 (11:15 +0100)
committerMykola Golub <mykola.golub@clyso.com>
Sun, 6 Nov 2022 14:28:41 +0000 (14:28 +0000)
Fixes: https://github.com/ceph/ceph/pull/47842
Signed-off-by: Mykola Golub <mykola.golub@clyso.com>
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rbd.py

index 500a49f0c726c5d401d7e6218ac230c1b6b17d67..9cd22068d7f46795f2039bae37759d70e4fd47ed 100644 (file)
@@ -250,6 +250,7 @@ class RbdTest(DashboardTestCase):
                 'source': JLeaf(int),
                 'value': JLeaf(str),
             })),
+            'metadata': JObj({}, allow_unknown=True),
             'mirror_mode': JLeaf(str),
         })
         self.assertSchema(img, schema)
@@ -365,6 +366,25 @@ class RbdTest(DashboardTestCase):
 
         self.remove_image(pool, None, image_name)
 
+    def test_create_with_metadata(self):
+        pool = 'rbd'
+        image_name = 'image_with_meta'
+        size = 10240
+        metadata = {
+            'test1': 'test',
+            'test2': 'value',
+        }
+
+        self.create_image(pool, None, image_name, size, metadata=metadata)
+        self.assertStatus(201)
+        img = self.get_image('rbd', None, image_name)
+        self.assertStatus(200)
+        self.assertEqual(len(metadata), len(img['metadata']))
+        for meta in metadata:
+            self.assertIn(meta, img['metadata'])
+
+        self.remove_image(pool, None, image_name)
+
     def test_create_rbd_in_data_pool(self):
         if not self.bluestore_support:
             self.skipTest('requires bluestore cluster')
@@ -606,6 +626,47 @@ class RbdTest(DashboardTestCase):
         self.remove_image(pool, None, image)
         self.assertStatus(204)
 
+    def test_image_change_meta(self):
+        pool = 'rbd'
+        image = 'image_with_meta'
+        initial_meta = {
+            'test1': 'test',
+            'test2': 'value',
+            'test3': None,
+        }
+        initial_expect = {
+            'test1': 'test',
+            'test2': 'value',
+        }
+        new_meta = {
+            'test1': None,
+            'test2': 'new_value',
+            'test3': 'value',
+            'test4': None,
+        }
+        new_expect = {
+            'test2': 'new_value',
+            'test3': 'value',
+        }
+
+        self.create_image(pool, None, image, 2**30, metadata=initial_meta)
+        self.assertStatus(201)
+        img = self.get_image(pool, None, image)
+        self.assertStatus(200)
+        self.assertEqual(len(initial_expect), len(img['metadata']))
+        for meta in initial_expect:
+            self.assertIn(meta, img['metadata'])
+
+        self.edit_image(pool, None, image, metadata=new_meta)
+        img = self.get_image(pool, None, image)
+        self.assertStatus(200)
+        self.assertEqual(len(new_expect), len(img['metadata']))
+        for meta in new_expect:
+            self.assertIn(meta, img['metadata'])
+
+        self.remove_image(pool, None, image)
+        self.assertStatus(204)
+
     def test_update_snapshot(self):
         self.create_snapshot('rbd', None, 'img1', 'snap5')
         self.assertStatus(201)
@@ -662,7 +723,8 @@ class RbdTest(DashboardTestCase):
         self.assertStatus(204)
 
     def test_clone(self):
-        self.create_image('rbd', None, 'cimg', 2**30, features=["layering"])
+        self.create_image('rbd', None, 'cimg', 2**30, features=["layering"],
+                          metadata={'key1': 'val1'})
         self.assertStatus(201)
         self.create_snapshot('rbd', None, 'cimg', 'snap1')
         self.assertStatus(201)
@@ -670,7 +732,8 @@ class RbdTest(DashboardTestCase):
         self.assertStatus(200)
         self.clone_image('rbd', None, 'cimg', 'snap1', 'rbd', None, 'cimg-clone',
                          features=["layering", "exclusive-lock", "fast-diff",
-                                   "object-map"])
+                                   "object-map"],
+                         metadata={'key1': None, 'key2': 'val2'})
         self.assertStatus([200, 201])
 
         img = self.get_image('rbd', None, 'cimg-clone')
@@ -679,7 +742,8 @@ class RbdTest(DashboardTestCase):
                                                  'fast-diff', 'layering',
                                                  'object-map'],
                              parent={'pool_name': 'rbd', 'pool_namespace': '',
-                                     'image_name': 'cimg', 'snap_name': 'snap1'})
+                                     'image_name': 'cimg', 'snap_name': 'snap1'},
+                             metadata={'key2': 'val2'})
 
         res = self.remove_image('rbd', None, 'cimg')
         self.assertStatus(400)
@@ -694,7 +758,8 @@ class RbdTest(DashboardTestCase):
     def test_copy(self):
         self.create_image('rbd', None, 'coimg', 2**30,
                           features=["layering", "exclusive-lock", "fast-diff",
-                                    "object-map"])
+                                    "object-map"],
+                          metadata={'key1': 'val1'})
         self.assertStatus(201)
 
         self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '5M',
@@ -702,18 +767,21 @@ class RbdTest(DashboardTestCase):
 
         self.copy_image('rbd', None, 'coimg', 'rbd_iscsi', None, 'coimg-copy',
                         features=["layering", "fast-diff", "exclusive-lock",
-                                  "object-map"])
+                                  "object-map"],
+                        metadata={'key1': None, 'key2': 'val2'})
         self.assertStatus([200, 201])
 
         img = self.get_image('rbd', None, 'coimg')
         self.assertStatus(200)
         self._validate_image(img, features_name=['layering', 'exclusive-lock',
-                                                 'fast-diff', 'object-map'])
+                                                 'fast-diff', 'object-map'],
+                             metadata={'key1': 'val1'})
 
         img_copy = self.get_image('rbd_iscsi', None, 'coimg-copy')
         self._validate_image(img_copy, features_name=['exclusive-lock',
                                                       'fast-diff', 'layering',
                                                       'object-map'],
+                             metadata={'key2': 'val2'},
                              disk_usage=img['disk_usage'])
 
         self.remove_image('rbd', None, 'coimg')
index d83cd3973bf05bbbf2706edcc5a17da83cd1840b..f61466aa88a6288f4eae4d78c3782adef29cc3bb 100644 (file)
@@ -16,9 +16,9 @@ from ..security import Scope
 from ..services.ceph_service import CephService
 from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception
 from ..services.rbd import MIRROR_IMAGE_MODE, RbdConfiguration, \
-    RbdMirroringService, RbdService, RbdSnapshotService, format_bitmask, \
-    format_features, get_image_spec, parse_image_spec, rbd_call, \
-    rbd_image_call
+    RbdImageMetadataService, RbdMirroringService, RbdService, \
+    RbdSnapshotService, format_bitmask, format_features, get_image_spec, \
+    parse_image_spec, rbd_call, rbd_image_call
 from ..tools import ViewCache, str_to_bool
 from . import APIDoc, APIRouter, BaseController, CreatePermission, \
     DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \
@@ -99,6 +99,10 @@ class Rbd(RESTController):
 
             images[i]['configuration'] = RbdConfiguration(
                 pool, image['namespace'], image['name']).list()
+            images[i]['metadata'] = rbd_image_call(
+                pool, image['namespace'], image['name'],
+                lambda ioctx, image: RbdImageMetadataService(image).list())
+
         return list(pool_result.values())
 
     @handle_rbd_error()
@@ -124,7 +128,8 @@ class Rbd(RESTController):
              {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0)
     def create(self, name, pool_name, size, namespace=None, schedule_interval='',
                obj_size=None, features=None, stripe_unit=None, stripe_count=None,
-               data_pool=None, configuration=None, mirror_mode=None):
+               data_pool=None, configuration=None, metadata=None,
+               mirror_mode=None):
 
         size = int(size)
 
@@ -144,8 +149,12 @@ class Rbd(RESTController):
                             stripe_count=stripe_count, data_pool=data_pool)
             RbdConfiguration(pool_ioctx=ioctx, namespace=namespace,
                              image_name=name).set_configuration(configuration)
+            if metadata:
+                with rbd.Image(ioctx, name) as image:
+                    RbdImageMetadataService(image).set_metadata(metadata)
 
         rbd_call(pool_name, namespace, _create)
+
         if mirror_mode:
             RbdMirroringService.enable_image(name, pool_name, namespace,
                                              MIRROR_IMAGE_MODE[mirror_mode])
@@ -168,7 +177,7 @@ class Rbd(RESTController):
 
     @RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
     def set(self, image_spec, name=None, size=None, features=None,
-            configuration=None, enable_mirror=None, primary=None,
+            configuration=None, metadata=None, enable_mirror=None, primary=None,
             resync=False, mirror_mode=None, schedule_interval='',
             remove_scheduling=False):
 
@@ -206,6 +215,8 @@ class Rbd(RESTController):
 
             RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
                 configuration)
+            if metadata:
+                RbdImageMetadataService(image).set_metadata(metadata)
 
             mirror_image_info = image.mirror_image_get_info()
             if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED:
@@ -244,7 +255,8 @@ class Rbd(RESTController):
     @allow_empty_body
     def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name,
              snapshot_name=None, obj_size=None, features=None,
-             stripe_unit=None, stripe_count=None, data_pool=None, configuration=None):
+             stripe_unit=None, stripe_count=None, data_pool=None,
+             configuration=None, metadata=None):
         pool_name, namespace, image_name = parse_image_spec(image_spec)
 
         def _src_copy(s_ioctx, s_img):
@@ -264,6 +276,9 @@ class Rbd(RESTController):
                            stripe_unit, stripe_count, data_pool)
                 RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
                     configuration)
+                if metadata:
+                    with rbd.Image(d_ioctx, dest_image_name) as image:
+                        RbdImageMetadataService(image).set_metadata(metadata)
 
             return rbd_call(dest_pool_name, dest_namespace, _copy)
 
@@ -395,7 +410,8 @@ class RbdSnapshot(RESTController):
     @allow_empty_body
     def clone(self, image_spec, snapshot_name, child_pool_name,
               child_image_name, child_namespace=None, obj_size=None, features=None,
-              stripe_unit=None, stripe_count=None, data_pool=None, configuration=None):
+              stripe_unit=None, stripe_count=None, data_pool=None,
+              configuration=None, metadata=None):
         """
         Clones a snapshot to an image
         """
@@ -419,6 +435,9 @@ class RbdSnapshot(RESTController):
 
                 RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
                     configuration)
+                if metadata:
+                    with rbd.Image(ioctx, child_image_name) as image:
+                        RbdImageMetadataService(image).set_metadata(metadata)
 
             return rbd_call(child_pool_name, child_namespace, _clone)
 
index 3f92ab9e0ea7c97c1cc0a496ee7a953778986ca8..378c1dbee2c92ab490f50e716f1958abf6ae60db 100644 (file)
@@ -224,6 +224,8 @@ paths:
                   type: string
                 features:
                   type: string
+                metadata:
+                  type: string
                 mirror_mode:
                   type: string
                 name:
@@ -566,6 +568,8 @@ paths:
                   type: string
                 features:
                   type: string
+                metadata:
+                  type: string
                 mirror_mode:
                   type: string
                 name:
@@ -633,6 +637,8 @@ paths:
                   type: string
                 features:
                   type: string
+                metadata:
+                  type: string
                 obj_size:
                   type: integer
                 snapshot_name:
@@ -902,6 +908,8 @@ paths:
                   type: string
                 features:
                   type: string
+                metadata:
+                  type: string
                 obj_size:
                   type: integer
                 stripe_count:
index d2a0312029b343d96526dd78c6191089bf7c4dcc..ae8cea6feb9bd3929339898b93151a84a736bdf4 100644 (file)
@@ -377,6 +377,8 @@ class RbdService(object):
             stat['configuration'] = RbdConfiguration(
                 pool_ioctx=ioctx, image_name=image_name, image_ioctx=img).list()
 
+            stat['metadata'] = RbdImageMetadataService(img).list()
+
             return stat
 
     @classmethod
@@ -569,3 +571,32 @@ class RbdMirroringService:
     @classmethod
     def snapshot_schedule_remove(cls, image_spec: str):
         _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)
+
+
+class RbdImageMetadataService(object):
+    def __init__(self, image):
+        self._image = image
+
+    def list(self):
+        result = self._image.metadata_list()
+        # filter out configuration metadata
+        return {v[0]: v[1] for v in result if not v[0].startswith('conf_')}
+
+    def get(self, name):
+        return self._image.metadata_get(name)
+
+    def set(self, name, value):
+        self._image.metadata_set(name, value)
+
+    def remove(self, name):
+        try:
+            self._image.metadata_remove(name)
+        except KeyError:
+            pass
+
+    def set_metadata(self, metadata):
+        for name, value in metadata.items():
+            if value is not None:
+                self.set(name, value)
+            else:
+                self.remove(name)