From 39fafe74625423e5d23a5f1bbd74e58ef9055573 Mon Sep 17 00:00:00 2001 From: Ilya Dryomov Date: Sun, 28 Apr 2024 19:19:22 +0200 Subject: [PATCH] pybind/rbd: expose RBD_IMAGE_OPTION_CLONE_FORMAT option It takes effect with clone(), deep_copy() and migration_prepare(). Fixes: https://tracker.ceph.com/issues/65624 Signed-off-by: Ilya Dryomov --- PendingReleaseNotes | 4 +- src/pybind/rbd/c_rbd.pxd | 1 + src/pybind/rbd/mock_rbd.pxi | 1 + src/pybind/rbd/rbd.pyx | 25 +++- src/test/pybind/test_rbd.py | 240 ++++++++++++++++++++++++++++++++++-- 5 files changed, 258 insertions(+), 13 deletions(-) diff --git a/PendingReleaseNotes b/PendingReleaseNotes index 82d56bfbb605d..ce499bed0769f 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -168,7 +168,6 @@ CephFS: Disallow delegating preallocated inode ranges to clients. Config default json format produces a rather massive output in large clusters and isn't scalable. So we have removed the 'network_ping_times' section from the output. Details in the tracker: https://tracker.ceph.com/issues/57460 - * CephFS: The `subvolume snapshot clone` command now depends on the config option `snapshot_clone_no_wait` which is used to reject the clone operation when all the cloner threads are busy. This config option is enabled by default which means @@ -177,6 +176,9 @@ CephFS: Disallow delegating preallocated inode ranges to clients. Config `ceph config get mgr mgr/volumes/snapshot_clone_no_wait` and it can be disabled by using: `ceph config set mgr mgr/volumes/snapshot_clone_no_wait false` +* RBD: `RBD_IMAGE_OPTION_CLONE_FORMAT` option has been exposed in Python + bindings via `clone_format` optional parameter to `clone`, `deep_copy` and + `migration_prepare` methods. >=18.0.0 diff --git a/src/pybind/rbd/c_rbd.pxd b/src/pybind/rbd/c_rbd.pxd index bda23bbc4735f..5ece401bf1d36 100644 --- a/src/pybind/rbd/c_rbd.pxd +++ b/src/pybind/rbd/c_rbd.pxd @@ -45,6 +45,7 @@ cdef extern from "rbd/librbd.h" nogil: _RBD_IMAGE_OPTION_STRIPE_UNIT "RBD_IMAGE_OPTION_STRIPE_UNIT" _RBD_IMAGE_OPTION_STRIPE_COUNT "RBD_IMAGE_OPTION_STRIPE_COUNT" _RBD_IMAGE_OPTION_DATA_POOL "RBD_IMAGE_OPTION_DATA_POOL" + _RBD_IMAGE_OPTION_CLONE_FORMAT "RBD_IMAGE_OPTION_CLONE_FORMAT" RBD_MAX_BLOCK_NAME_SIZE RBD_MAX_IMAGE_NAME_SIZE diff --git a/src/pybind/rbd/mock_rbd.pxi b/src/pybind/rbd/mock_rbd.pxi index 364f965fbad43..68802e674a00b 100644 --- a/src/pybind/rbd/mock_rbd.pxi +++ b/src/pybind/rbd/mock_rbd.pxi @@ -49,6 +49,7 @@ cdef nogil: _RBD_IMAGE_OPTION_STRIPE_UNIT "RBD_IMAGE_OPTION_STRIPE_UNIT" _RBD_IMAGE_OPTION_STRIPE_COUNT "RBD_IMAGE_OPTION_STRIPE_COUNT" _RBD_IMAGE_OPTION_DATA_POOL "RBD_IMAGE_OPTION_DATA_POOL" + _RBD_IMAGE_OPTION_CLONE_FORMAT "RBD_IMAGE_OPTION_CLONE_FORMAT" RBD_MAX_BLOCK_NAME_SIZE RBD_MAX_IMAGE_NAME_SIZE diff --git a/src/pybind/rbd/rbd.pyx b/src/pybind/rbd/rbd.pyx index f59ba23f0fe21..d31d11738d59e 100644 --- a/src/pybind/rbd/rbd.pyx +++ b/src/pybind/rbd/rbd.pyx @@ -115,6 +115,7 @@ RBD_IMAGE_OPTION_ORDER = _RBD_IMAGE_OPTION_ORDER RBD_IMAGE_OPTION_STRIPE_UNIT = _RBD_IMAGE_OPTION_STRIPE_UNIT RBD_IMAGE_OPTION_STRIPE_COUNT = _RBD_IMAGE_OPTION_STRIPE_COUNT RBD_IMAGE_OPTION_DATA_POOL = _RBD_IMAGE_OPTION_DATA_POOL +RBD_IMAGE_OPTION_CLONE_FORMAT = _RBD_IMAGE_OPTION_CLONE_FORMAT RBD_SNAP_NAMESPACE_TYPE_USER = _RBD_SNAP_NAMESPACE_TYPE_USER RBD_SNAP_NAMESPACE_TYPE_GROUP = _RBD_SNAP_NAMESPACE_TYPE_GROUP @@ -632,7 +633,7 @@ class RBD(object): def clone(self, p_ioctx, p_name, p_snapname, c_ioctx, c_name, features=None, order=None, stripe_unit=None, stripe_count=None, - data_pool=None): + data_pool=None, clone_format=None): """ Clone a parent rbd snapshot into a COW sparse child. @@ -656,6 +657,8 @@ class RBD(object): :type stripe_count: int :param data_pool: optional separate pool for data blocks :type data_pool: str + :param clone_format: 1 (requires a protected snapshot), 2 (requires mimic+ clients) + :type clone_format: int :raises: :class:`TypeError` :raises: :class:`InvalidArgument` :raises: :class:`ImageExists` @@ -691,6 +694,9 @@ class RBD(object): if data_pool is not None: rbd_image_options_set_string(opts, RBD_IMAGE_OPTION_DATA_POOL, data_pool) + if clone_format is not None: + rbd_image_options_set_uint64(opts, RBD_IMAGE_OPTION_CLONE_FORMAT, + clone_format) with nogil: ret = rbd_clone3(_p_ioctx, _p_name, _p_snapname, _c_ioctx, _c_name, opts) @@ -956,7 +962,7 @@ class RBD(object): def migration_prepare(self, ioctx, image_name, dest_ioctx, dest_image_name, features=None, order=None, stripe_unit=None, stripe_count=None, - data_pool=None): + data_pool=None, clone_format=None): """ Prepare an RBD image migration. @@ -978,6 +984,9 @@ class RBD(object): :type stripe_count: int :param data_pool: optional separate pool for data blocks :type data_pool: str + :param clone_format: if the source image is a clone, which clone format + to use for the destination image + :type clone_format: int :raises: :class:`TypeError` :raises: :class:`InvalidArgument` :raises: :class:`ImageExists` @@ -1010,6 +1019,9 @@ class RBD(object): if data_pool is not None: rbd_image_options_set_string(opts, RBD_IMAGE_OPTION_DATA_POOL, data_pool) + if clone_format is not None: + rbd_image_options_set_uint64(opts, RBD_IMAGE_OPTION_CLONE_FORMAT, + clone_format) with nogil: ret = rbd_migration_prepare(_ioctx, _image_name, _dest_ioctx, _dest_image_name, opts) @@ -3438,7 +3450,8 @@ cdef class Image(object): @requires_not_closed def deep_copy(self, dest_ioctx, dest_name, features=None, order=None, - stripe_unit=None, stripe_count=None, data_pool=None): + stripe_unit=None, stripe_count=None, data_pool=None, + clone_format=None): """ Deep copy the image to another location. @@ -3456,6 +3469,9 @@ cdef class Image(object): :type stripe_count: int :param data_pool: optional separate pool for data blocks :type data_pool: str + :param clone_format: if the source image is a clone, which clone format + to use for the destination image + :type clone_format: int :raises: :class:`TypeError` :raises: :class:`InvalidArgument` :raises: :class:`ImageExists` @@ -3486,6 +3502,9 @@ cdef class Image(object): if data_pool is not None: rbd_image_options_set_string(opts, RBD_IMAGE_OPTION_DATA_POOL, data_pool) + if clone_format is not None: + rbd_image_options_set_uint64(opts, RBD_IMAGE_OPTION_CLONE_FORMAT, + clone_format) with nogil: ret = rbd_deep_copy(self.image, _dest_ioctx, _dest_name, opts) finally: diff --git a/src/test/pybind/test_rbd.py b/src/test/pybind/test_rbd.py index df47b0d2976be..1bd492bd2d4b6 100644 --- a/src/test/pybind/test_rbd.py +++ b/src/test/pybind/test_rbd.py @@ -32,6 +32,7 @@ from rbd import (RBD, Group, Image, ImageNotFound, InvalidArgument, ImageExists, RBD_MIRROR_IMAGE_DISABLED, MIRROR_IMAGE_STATUS_STATE_UNKNOWN, RBD_MIRROR_IMAGE_MODE_JOURNAL, RBD_MIRROR_IMAGE_MODE_SNAPSHOT, RBD_LOCK_MODE_EXCLUSIVE, RBD_OPERATION_FEATURE_GROUP, + RBD_OPERATION_FEATURE_CLONE_CHILD, RBD_SNAP_NAMESPACE_TYPE_TRASH, RBD_SNAP_NAMESPACE_TYPE_MIRROR, RBD_IMAGE_MIGRATION_STATE_PREPARED, RBD_CONFIG_SOURCE_CONFIG, @@ -827,34 +828,127 @@ class TestImage(object): self.rbd.remove(ioctx, dst_name) @require_features([RBD_FEATURE_LAYERING]) - def test_deep_copy_clone(self): - global ioctx - global features + def test_deep_copy_clone_v1_to_v1(self): self.image.write(b'a' * 256, 0) self.image.create_snap('snap1') self.image.write(b'b' * 256, 0) self.image.protect_snap('snap1') clone_name = get_temp_image_name() dst_name = get_temp_image_name() - self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name) + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=1) with Image(ioctx, clone_name) as child: + eq(0, child.op_features()) child.create_snap('snap1') child.deep_copy(ioctx, dst_name, features=features, order=self.image.stat()['order'], stripe_unit=self.image.stripe_unit(), stripe_count=self.image.stripe_count(), - data_pool=None) + clone_format=1) child.remove_snap('snap1') with Image(ioctx, dst_name) as copy: copy_data = copy.read(0, 256) eq(b'a' * 256, copy_data) + eq(self.image.id(), copy.parent_id()) + eq(0, copy.op_features()) copy.remove_snap('snap1') self.rbd.remove(ioctx, dst_name) self.rbd.remove(ioctx, clone_name) self.image.unprotect_snap('snap1') self.image.remove_snap('snap1') + @require_features([RBD_FEATURE_LAYERING]) + def test_deep_copy_clone_v1_to_v2(self): + self.image.write(b'a' * 256, 0) + self.image.create_snap('snap1') + self.image.write(b'b' * 256, 0) + self.image.protect_snap('snap1') + clone_name = get_temp_image_name() + dst_name = get_temp_image_name() + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=1) + with Image(ioctx, clone_name) as child: + eq(0, child.op_features()) + child.create_snap('snap1') + child.deep_copy(ioctx, dst_name, features=features, + order=self.image.stat()['order'], + stripe_unit=self.image.stripe_unit(), + stripe_count=self.image.stripe_count(), + clone_format=2) + child.remove_snap('snap1') + + with Image(ioctx, dst_name) as copy: + copy_data = copy.read(0, 256) + eq(b'a' * 256, copy_data) + eq(self.image.id(), copy.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, copy.op_features()) + copy.remove_snap('snap1') + self.rbd.remove(ioctx, dst_name) + self.rbd.remove(ioctx, clone_name) + self.image.unprotect_snap('snap1') + self.image.remove_snap('snap1') + + @require_features([RBD_FEATURE_LAYERING]) + def test_deep_copy_clone_v2_to_v1(self): + self.image.write(b'a' * 256, 0) + self.image.create_snap('snap1') + self.image.write(b'b' * 256, 0) + self.image.protect_snap('snap1') + clone_name = get_temp_image_name() + dst_name = get_temp_image_name() + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=2) + with Image(ioctx, clone_name) as child: + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, child.op_features()) + child.create_snap('snap1') + child.deep_copy(ioctx, dst_name, features=features, + order=self.image.stat()['order'], + stripe_unit=self.image.stripe_unit(), + stripe_count=self.image.stripe_count(), + clone_format=1) + child.remove_snap('snap1') + + with Image(ioctx, dst_name) as copy: + copy_data = copy.read(0, 256) + eq(b'a' * 256, copy_data) + eq(self.image.id(), copy.parent_id()) + eq(0, copy.op_features()) + copy.remove_snap('snap1') + self.rbd.remove(ioctx, dst_name) + self.rbd.remove(ioctx, clone_name) + self.image.unprotect_snap('snap1') + self.image.remove_snap('snap1') + + @require_features([RBD_FEATURE_LAYERING]) + def test_deep_copy_clone_v2_to_v2(self): + self.image.write(b'a' * 256, 0) + self.image.create_snap('snap1') + self.image.write(b'b' * 256, 0) + clone_name = get_temp_image_name() + dst_name = get_temp_image_name() + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=2) + with Image(ioctx, clone_name) as child: + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, child.op_features()) + child.create_snap('snap1') + child.deep_copy(ioctx, dst_name, features=features, + order=self.image.stat()['order'], + stripe_unit=self.image.stripe_unit(), + stripe_count=self.image.stripe_count(), + clone_format=2) + child.remove_snap('snap1') + + with Image(ioctx, dst_name) as copy: + copy_data = copy.read(0, 256) + eq(b'a' * 256, copy_data) + eq(self.image.id(), copy.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, copy.op_features()) + copy.remove_snap('snap1') + self.rbd.remove(ioctx, dst_name) + self.rbd.remove(ioctx, clone_name) + self.image.remove_snap('snap1') + def test_create_snap(self): global ioctx self.image.create_snap('snap1') @@ -1562,15 +1656,29 @@ class TestClone(object): image.close() RBD().remove(ioctx, image_name) + def test_clone_format(self): + clone_name2 = get_temp_image_name() + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name2, + features, clone_format=1) + with Image(ioctx, clone_name2) as clone2: + eq(0, clone2.op_features()) + self.rbd.remove(ioctx, clone_name2) + + self.rbd.clone(ioctx, image_name, 'snap1', ioctx, clone_name2, + features, clone_format=2) + with Image(ioctx, clone_name2) as clone2: + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, clone2.op_features()) + self.rbd.remove(ioctx, clone_name2) def test_unprotected(self): self.image.create_snap('snap2') - global features clone_name2 = get_temp_image_name() - rados.conf_set("rbd_default_clone_format", "1") + # clone format 1 requires a protected snapshot assert_raises(InvalidArgument, self.rbd.clone, ioctx, image_name, - 'snap2', ioctx, clone_name2, features) - rados.conf_set("rbd_default_clone_format", "auto") + 'snap2', ioctx, clone_name2, features, clone_format=1) + self.rbd.clone(ioctx, image_name, 'snap2', ioctx, clone_name2, + features, clone_format=2) + self.rbd.remove(ioctx, clone_name2) self.image.remove_snap('snap2') def test_unprotect_with_children(self): @@ -2753,6 +2861,120 @@ class TestMigration(object): RBD().migration_commit(ioctx, image_name) remove_image() + @require_features([RBD_FEATURE_LAYERING]) + def test_migration_clone_v1_to_v1(self): + create_image() + with Image(ioctx, image_name) as image: + image_id = image.id() + image.create_snap('snap1') + image.protect_snap('snap1') + clone_name = get_temp_image_name() + RBD().clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=1) + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(0, clone.op_features()) + + RBD().migration_prepare(ioctx, clone_name, ioctx, clone_name, features=63, + order=23, stripe_unit=1<<23, stripe_count=1, + clone_format=1) + RBD().migration_execute(ioctx, clone_name) + RBD().migration_commit(ioctx, clone_name) + + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(0, clone.op_features()) + RBD().remove(ioctx, clone_name) + with Image(ioctx, image_name) as image: + image.unprotect_snap('snap1') + image.remove_snap('snap1') + remove_image() + + @require_features([RBD_FEATURE_LAYERING]) + def test_migration_clone_v1_to_v2(self): + create_image() + with Image(ioctx, image_name) as image: + image_id = image.id() + image.create_snap('snap1') + image.protect_snap('snap1') + clone_name = get_temp_image_name() + RBD().clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=1) + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(0, clone.op_features()) + + RBD().migration_prepare(ioctx, clone_name, ioctx, clone_name, features=63, + order=23, stripe_unit=1<<23, stripe_count=1, + clone_format=2) + RBD().migration_execute(ioctx, clone_name) + RBD().migration_commit(ioctx, clone_name) + + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, clone.op_features()) + RBD().remove(ioctx, clone_name) + with Image(ioctx, image_name) as image: + image.unprotect_snap('snap1') + image.remove_snap('snap1') + remove_image() + + @require_features([RBD_FEATURE_LAYERING]) + def test_migration_clone_v2_to_v1(self): + create_image() + with Image(ioctx, image_name) as image: + image_id = image.id() + image.create_snap('snap1') + image.protect_snap('snap1') + clone_name = get_temp_image_name() + RBD().clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=2) + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, clone.op_features()) + + RBD().migration_prepare(ioctx, clone_name, ioctx, clone_name, features=63, + order=23, stripe_unit=1<<23, stripe_count=1, + clone_format=1) + RBD().migration_execute(ioctx, clone_name) + RBD().migration_commit(ioctx, clone_name) + + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(0, clone.op_features()) + RBD().remove(ioctx, clone_name) + with Image(ioctx, image_name) as image: + image.unprotect_snap('snap1') + image.remove_snap('snap1') + remove_image() + + @require_features([RBD_FEATURE_LAYERING]) + def test_migration_clone_v2_to_v2(self): + create_image() + with Image(ioctx, image_name) as image: + image_id = image.id() + image.create_snap('snap1') + clone_name = get_temp_image_name() + RBD().clone(ioctx, image_name, 'snap1', ioctx, clone_name, features, + clone_format=2) + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, clone.op_features()) + + RBD().migration_prepare(ioctx, clone_name, ioctx, clone_name, features=63, + order=23, stripe_unit=1<<23, stripe_count=1, + clone_format=2) + RBD().migration_execute(ioctx, clone_name) + RBD().migration_commit(ioctx, clone_name) + + with Image(ioctx, clone_name) as clone: + eq(image_id, clone.parent_id()) + eq(RBD_OPERATION_FEATURE_CLONE_CHILD, clone.op_features()) + RBD().remove(ioctx, clone_name) + with Image(ioctx, image_name) as image: + image.remove_snap('snap1') + remove_image() + def test_migration_import(self): create_image() with Image(ioctx, image_name) as image: -- 2.39.5