diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index 9f6fc163..88841a08 100755 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -171,6 +171,7 @@ elif [[ "$DRIVER" == "zfsonlinux" ]]; then MANILA_TEMPEST_CONCURRENCY=8 RUN_MANILA_CG_TESTS=False RUN_MANILA_MANAGE_TESTS=True + RUN_MANILA_MANAGE_SNAPSHOT_TESTS=True iniset $TEMPEST_CONFIG share run_migration_tests False iniset $TEMPEST_CONFIG share run_quota_tests True iniset $TEMPEST_CONFIG share run_replication_tests True diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index e2c7a712..6b6bbd07 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -33,7 +33,7 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | Driver name | create delete share | manage unmanage share | extend share | shrink share | create delete snapshot | create share from snapshot | manage unmanage snapshot | +========================================+=======================+=======================+==============+==============+========================+============================+==========================+ -| ZFSonLinux | M | N | M | M | M | M | \- | +| ZFSonLinux | M | N | M | M | M | M | N | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | Generic (Cinder as back-end) | J | K | L | L | J | J | M | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ diff --git a/doc/source/devref/zfs_on_linux_driver.rst b/doc/source/devref/zfs_on_linux_driver.rst index 4ee56ad0..f6f9a6d1 100644 --- a/doc/source/devref/zfs_on_linux_driver.rst +++ b/doc/source/devref/zfs_on_linux_driver.rst @@ -48,6 +48,8 @@ The following operations are supported: * Deny NFS Share access * Create snapshot * Delete snapshot +* Manage snapshot +* Unmanage snapshot * Create share from snapshot * Extend share * Shrink share diff --git a/manila/share/drivers/zfsonlinux/driver.py b/manila/share/drivers/zfsonlinux/driver.py index 43ecf674..9ff85472 100644 --- a/manila/share/drivers/zfsonlinux/driver.py +++ b/manila/share/drivers/zfsonlinux/driver.py @@ -18,6 +18,7 @@ Module with ZFSonLinux share driver that utilizes ZFS filesystem resources and exports them as shares. """ +import math import time from oslo_config import cfg @@ -492,6 +493,7 @@ class ZFSonLinuxShareDriver(zfs_utils.ExecuteMixin, driver.ShareDriver): } ) self.zfs('snapshot', snapshot_name) + return {"provider_location": snapshot_name} @ensure_share_server_not_provided def delete_snapshot(self, context, snapshot, share_server=None): @@ -519,7 +521,7 @@ class ZFSonLinuxShareDriver(zfs_utils.ExecuteMixin, driver.ShareDriver): _LW("Snapshot with '%(id)s' ID and '%(name)s' NAME is " "absent on backend. Nothing has been deleted."), {'id': snapshot['id'], 'name': snapshot_name}) - self.private_storage.delete(snapshot['id']) + self.private_storage.delete(snapshot['snapshot_id']) @ensure_share_server_not_provided def create_share_from_snapshot(self, context, share, snapshot, @@ -723,6 +725,54 @@ class ZFSonLinuxShareDriver(zfs_utils.ExecuteMixin, driver.ShareDriver): """Removes the specified share from Manila management.""" self.private_storage.delete(share['id']) + def manage_existing_snapshot(self, snapshot_instance, driver_options): + """Manage existing share snapshot with manila. + + :param snapshot_instance: SnapshotInstance data + :param driver_options: expects only one optional key 'size'. + :return: dict with share snapshot instance fields for update, example: + {'size': 1, + 'provider_location': 'path/to/some/dataset@some_snapshot_tag'} + """ + snapshot_size = int(driver_options.get("size", 0)) + old_provider_location = snapshot_instance.get("provider_location") + old_snapshot_tag = old_provider_location.split("@")[-1] + new_snapshot_tag = self._get_snapshot_name(snapshot_instance["id"]) + + self.private_storage.update( + snapshot_instance["snapshot_id"], { + "entity_type": "snapshot", + "old_snapshot_tag": old_snapshot_tag, + "snapshot_tag": new_snapshot_tag, + } + ) + + try: + self.zfs("list", "-r", "-t", "snapshot", old_provider_location) + except exception.ProcessExecutionError as e: + raise exception.ManageInvalidShareSnapshot(reason=e.stderr) + + if not snapshot_size: + consumed_space = self.get_zfs_option(old_provider_location, "used") + consumed_space = utils.translate_string_size_to_float( + consumed_space) + snapshot_size = int(math.ceil(consumed_space)) + + dataset_name = self.private_storage.get( + snapshot_instance["share_instance_id"], "dataset_name") + new_provider_location = dataset_name + "@" + new_snapshot_tag + + self.zfs("rename", old_provider_location, new_provider_location) + + return { + "size": snapshot_size, + "provider_location": new_provider_location, + } + + def unmanage_snapshot(self, snapshot_instance): + """Unmanage dataset snapshot.""" + self.private_storage.delete(snapshot_instance["snapshot_id"]) + def _get_replication_snapshot_prefix(self, replica): """Returns replica-based snapshot prefix.""" replication_snapshot_prefix = "%s_%s" % ( diff --git a/manila/tests/share/drivers/zfsonlinux/test_driver.py b/manila/tests/share/drivers/zfsonlinux/test_driver.py index e34f2b1f..310bdbe1 100644 --- a/manila/tests/share/drivers/zfsonlinux/test_driver.py +++ b/manila/tests/share/drivers/zfsonlinux/test_driver.py @@ -632,7 +632,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase): snapshot['share_instance_id'], {'dataset_name': 'foo_data_set_name'}) - self.driver.create_snapshot('fake_context', snapshot) + result = self.driver.create_snapshot('fake_context', snapshot) self.driver.zfs.assert_called_once_with( 'snapshot', snapshot_name) @@ -640,6 +640,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase): snapshot_name.split('@')[-1], self.driver.private_storage.get( snapshot['snapshot_id'], 'snapshot_tag')) + self.assertEqual({"provider_location": snapshot_name}, result) def test_delete_snapshot(self): snapshot = { @@ -1194,6 +1195,89 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase): self.driver.private_storage.delete.assert_called_once_with(share['id']) + @ddt.data( + {}, + {"size": 5}, + {"size": "5"}, + ) + def test_manage_existing_snapshot(self, driver_options): + dataset_name = "path/to/dataset" + old_provider_location = dataset_name + "@original_snapshot_tag" + snapshot_instance = { + "id": "fake_snapshot_instance_id", + "share_instance_id": "fake_share_instance_id", + "snapshot_id": "fake_snapshot_id", + "provider_location": old_provider_location, + } + new_snapshot_tag = "fake_new_snapshot_tag" + new_provider_location = ( + old_provider_location.split("@")[0] + "@" + new_snapshot_tag) + + self.mock_object(self.driver, "zfs") + self.mock_object( + self.driver, "get_zfs_option", mock.Mock(return_value="5G")) + self.mock_object( + self.driver, + '_get_snapshot_name', + mock.Mock(return_value=new_snapshot_tag)) + self.driver.private_storage.update( + snapshot_instance["share_instance_id"], + {"dataset_name": dataset_name}) + + result = self.driver.manage_existing_snapshot( + snapshot_instance, driver_options) + + expected_result = { + "size": 5, + "provider_location": new_provider_location, + } + self.assertEqual(expected_result, result) + self.driver._get_snapshot_name.assert_called_once_with( + snapshot_instance["id"]) + self.driver.zfs.assert_has_calls([ + mock.call("list", "-r", "-t", "snapshot", old_provider_location), + mock.call("rename", old_provider_location, new_provider_location), + ]) + + def test_manage_existing_snapshot_not_found(self): + dataset_name = "path/to/dataset" + old_provider_location = dataset_name + "@original_snapshot_tag" + new_snapshot_tag = "fake_new_snapshot_tag" + snapshot_instance = { + "id": "fake_snapshot_instance_id", + "snapshot_id": "fake_snapshot_id", + "provider_location": old_provider_location, + } + self.mock_object( + self.driver, "_get_snapshot_name", + mock.Mock(return_value=new_snapshot_tag)) + self.mock_object( + self.driver, "zfs", + mock.Mock(side_effect=exception.ProcessExecutionError("FAKE"))) + + self.assertRaises( + exception.ManageInvalidShareSnapshot, + self.driver.manage_existing_snapshot, + snapshot_instance, {}, + ) + + self.driver.zfs.assert_called_once_with( + "list", "-r", "-t", "snapshot", old_provider_location) + self.driver._get_snapshot_name.assert_called_once_with( + snapshot_instance["id"]) + + def test_unmanage_snapshot(self): + snapshot_instance = { + "id": "fake_snapshot_instance_id", + "snapshot_id": "fake_snapshot_id", + } + self.mock_object(self.driver.private_storage, "delete") + + self.driver.unmanage_snapshot(snapshot_instance) + + self.driver.private_storage.delete.assert_called_once_with( + snapshot_instance["snapshot_id"]) + def test__delete_dataset_or_snapshot_with_retry_snapshot(self): self.mock_object(self.driver, 'get_zfs_option') self.mock_object(self.driver, 'zfs') diff --git a/releasenotes/notes/manage-snapshot-in-zfsonlinux-driver-6478d8d5b3c6a97f.yaml b/releasenotes/notes/manage-snapshot-in-zfsonlinux-driver-6478d8d5b3c6a97f.yaml new file mode 100644 index 00000000..3745d61d --- /dev/null +++ b/releasenotes/notes/manage-snapshot-in-zfsonlinux-driver-6478d8d5b3c6a97f.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support of 'manage snapshot' feature to ZFSonLinux driver.