From 349c8b1430d06795b28a715d011779a02f3f51e5 Mon Sep 17 00:00:00 2001 From: Mykola Golub Date: Sun, 21 Aug 2022 11:15:30 +0100 Subject: [PATCH] mgr/dashboard: allow to get/update RBD image metadata via REST API Fixes: https://github.com/ceph/ceph/pull/47842 Signed-off-by: Mykola Golub --- qa/tasks/mgr/dashboard/test_rbd.py | 80 +++++++++++++++++++-- src/pybind/mgr/dashboard/controllers/rbd.py | 33 +++++++-- src/pybind/mgr/dashboard/openapi.yaml | 8 +++ src/pybind/mgr/dashboard/services/rbd.py | 31 ++++++++ 4 files changed, 139 insertions(+), 13 deletions(-) diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index 500a49f0c72..9cd22068d7f 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -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') diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index d83cd3973bf..f61466aa88a 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -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) diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3f92ab9e0ea..378c1dbee2c 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -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: diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index d2a0312029b..ae8cea6feb9 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -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) -- 2.39.5