blob: 3279d6d57f49c2399c111096674ceefb9e0b3a61 [file] [log] [blame]
From 8fafd470b34b4b94d0e6e65f1a6049232f06b9f8 Mon Sep 17 00:00:00 2001
From: Peter Penchev <openstack-dev@storpool.com>
Date: Wed, 22 Jun 2022 10:04:31 +0300
Subject: [PATCH 2/8] Add the clone_across_pools driver capability
Let drivers declare that they can clone a volume into a different pool and
relax the checks when creating a volume from an image (both with and
without the image cache).
TODO: document this capability a bit more.
Change-Id: Idbac4dae00f9fa03fb14547a204666721bf67d96
Implements: blueprint clone-across-pools
---
cinder/image/cache.py | 11 ++-
cinder/tests/unit/image/test_cache.py | 14 ++--
.../volume/flows/test_create_volume_flow.py | 73 +++++++++++++++++++
cinder/volume/flows/manager/create_volume.py | 8 +-
cinder/volume/manager.py | 3 +-
doc/source/reference/support-matrix.ini | 73 +++++++++++++++++++
6 files changed, 169 insertions(+), 13 deletions(-)
diff --git a/cinder/image/cache.py b/cinder/image/cache.py
index 6d30210a4..a79451bf2 100644
--- a/cinder/image/cache.py
+++ b/cinder/image/cache.py
@@ -34,11 +34,13 @@ class ImageVolumeCache(object):
db,
volume_api,
max_cache_size_gb: int = 0,
- max_cache_size_count: int = 0):
+ max_cache_size_count: int = 0,
+ clone_across_pools: bool = False):
self.db = db
self.volume_api = volume_api
self.max_cache_size_gb = int(max_cache_size_gb)
self.max_cache_size_count = int(max_cache_size_count)
+ self.clone_across_pools = bool(clone_across_pools)
self.notifier = rpc.get_notifier('volume', CONF.host)
def get_by_image_volume(self,
@@ -55,11 +57,12 @@ class ImageVolumeCache(object):
self._notify_cache_eviction(context, cache_entry['image_id'],
cache_entry['host'])
- @staticmethod
- def _get_query_filters(volume_ref: objects.Volume) -> dict:
+ def _get_query_filters(self, volume_ref: objects.Volume) -> dict:
if volume_ref.is_clustered:
return {'cluster_name': volume_ref.cluster_name}
- return {'host': volume_ref.host}
+ if not self.clone_across_pools:
+ return {'host': volume_ref.host}
+ return {}
def get_entry(self,
context: context.RequestContext,
diff --git a/cinder/tests/unit/image/test_cache.py b/cinder/tests/unit/image/test_cache.py
index c3aba9b28..b8b704e0d 100644
--- a/cinder/tests/unit/image/test_cache.py
+++ b/cinder/tests/unit/image/test_cache.py
@@ -42,11 +42,12 @@ class ImageVolumeCacheTestCase(test.TestCase):
self.volume.update(vol_params)
self.volume_ovo = objects.Volume(self.context, **vol_params)
- def _build_cache(self, max_gb=0, max_count=0):
+ def _build_cache(self, max_gb=0, max_count=0, clone_across_pools=False):
cache = image_cache.ImageVolumeCache(self.mock_db,
self.mock_volume_api,
max_gb,
- max_count)
+ max_count,
+ clone_across_pools)
cache.notifier = self.notifier
return cache
@@ -91,9 +92,10 @@ class ImageVolumeCacheTestCase(test.TestCase):
self.assertEqual(entry['image_id'], msg['payload']['image_id'])
self.assertEqual(1, len(self.notifier.notifications))
- @ddt.data(True, False)
- def test_get_entry(self, clustered):
- cache = self._build_cache()
+ @ddt.data((True, True), (True, False), (False, True), (False, False))
+ @ddt.unpack
+ def test_get_entry(self, clustered, clone_across_pools):
+ cache = self._build_cache(clone_across_pools=clone_across_pools)
entry = self._build_entry()
image_meta = {
'is_public': True,
@@ -107,7 +109,7 @@ class ImageVolumeCacheTestCase(test.TestCase):
image_volume_cache_get_and_update_last_used.return_value) = entry
if not clustered:
self.volume_ovo.cluster_name = None
- expect = {'host': self.volume.host}
+ expect = {} if clone_across_pools else {'host': self.volume.host}
else:
expect = {'cluster_name': self.volume.cluster_name}
found_entry = cache.get_entry(self.context,
diff --git a/cinder/tests/unit/volume/flows/test_create_volume_flow.py b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
index 2a7b625bc..b7d43008f 100644
--- a/cinder/tests/unit/volume/flows/test_create_volume_flow.py
+++ b/cinder/tests/unit/volume/flows/test_create_volume_flow.py
@@ -1058,6 +1058,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
self, volume_get_by_id, vol_update, rekey_vol, cleanup_cg):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
@@ -1083,6 +1084,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
handle_bootable, cleanup_cg):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
@@ -1108,6 +1110,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
mock_cleanup_cg):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
@@ -1144,6 +1147,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
mock_cleanup_cg):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_cache = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
@@ -1192,6 +1196,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
mock_cleanup_cg):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
@@ -1250,6 +1255,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
driver_error):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
backup_host = 'host@backend#pool'
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
@@ -1289,6 +1295,7 @@ class CreateVolumeFlowManagerTestCase(test.TestCase):
def test_create_drive_error(self, mock_message_create):
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_volume_manager = mock.MagicMock()
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
fake_volume_manager, fake_db, fake_driver)
@@ -1490,6 +1497,7 @@ class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase):
spec=utils.get_file_spec())
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
mock.MagicMock(), fake_db, fake_driver)
fake_image_service = fake_image.FakeImageService()
@@ -1516,6 +1524,7 @@ class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase):
'cinder_encryption_key_id': None}
fake_driver.clone_image.return_value = (None, False)
+ fake_db.volume_get_all.return_value = []
fake_db.volume_get_all_by_host.return_value = [image_volume]
fake_manager._create_from_image(self.ctxt,
@@ -1534,6 +1543,69 @@ class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase):
self.assertFalse(fake_driver.create_cloned_volume.called)
mock_cleanup_cg.assert_called_once_with(volume)
+ @mock.patch('cinder.volume.flows.manager.create_volume.'
+ 'CreateVolumeFromSpecTask.'
+ '_cleanup_cg_in_volume')
+ @mock.patch('cinder.image.image_utils.TemporaryImages.fetch')
+ @mock.patch('cinder.volume.flows.manager.create_volume.'
+ 'CreateVolumeFromSpecTask.'
+ '_handle_bootable_volume_glance_meta')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_create_from_image_across(self, mock_qemu_info, handle_bootable,
+ mock_fetch_img, mock_cleanup_cg,
+ format='raw', owner=None,
+ location=True):
+ self.flags(allowed_direct_url_schemes=['cinder'])
+ mock_fetch_img.return_value = mock.MagicMock(
+ spec=utils.get_file_spec())
+ fake_db = mock.MagicMock()
+ fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {'clone_across_pools': True}
+ fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
+ mock.MagicMock(), fake_db, fake_driver)
+ fake_image_service = fake_image.FakeImageService()
+
+ volume = fake_volume.fake_volume_obj(self.ctxt,
+ host='host@backend#pool')
+ image_volume = fake_volume.fake_volume_obj(self.ctxt,
+ volume_metadata={})
+ image_id = fakes.IMAGE_ID
+ image_info = imageutils.QemuImgInfo()
+ image_info.virtual_size = '1073741824'
+ mock_qemu_info.return_value = image_info
+
+ url = 'cinder://%s' % image_volume['id']
+ image_location = None
+ if location:
+ image_location = (url, [{'url': url, 'metadata': {}}])
+ image_meta = {'id': image_id,
+ 'container_format': 'bare',
+ 'disk_format': format,
+ 'size': 1024,
+ 'owner': owner or self.ctxt.project_id,
+ 'virtual_size': None,
+ 'cinder_encryption_key_id': None}
+
+ fake_driver.clone_image.return_value = (None, False)
+ fake_db.volume_get_all.return_value = [image_volume]
+ fake_db.volume_get_all_by_host.return_value = []
+
+ fake_manager._create_from_image(self.ctxt,
+ volume,
+ image_location,
+ image_id,
+ image_meta,
+ fake_image_service)
+ if format == 'raw' and not owner and location:
+ fake_driver.create_cloned_volume.assert_called_once_with(
+ volume, image_volume)
+ handle_bootable.assert_called_once_with(self.ctxt, volume,
+ image_id=image_id,
+ image_meta=image_meta)
+ else:
+ self.assertFalse(fake_driver.create_cloned_volume.called)
+ mock_cleanup_cg.assert_called_once_with(volume)
+
LEGACY_URI = 'cinder://%s' % fakes.VOLUME_ID
MULTISTORE_URI = 'cinder://fake-store/%s' % fakes.VOLUME_ID
@@ -1560,6 +1632,7 @@ class CreateVolumeFlowManagerGlanceCinderBackendCase(test.TestCase):
spec=utils.get_file_spec())
fake_db = mock.MagicMock()
fake_driver = mock.MagicMock()
+ fake_driver.capabilities = {}
fake_manager = create_volume_manager.CreateVolumeFromSpecTask(
mock.MagicMock(), fake_db, fake_driver)
fake_image_service = fake_image.FakeImageService()
diff --git a/cinder/volume/flows/manager/create_volume.py b/cinder/volume/flows/manager/create_volume.py
index 4c18d7d84..187f3b3ed 100644
--- a/cinder/volume/flows/manager/create_volume.py
+++ b/cinder/volume/flows/manager/create_volume.py
@@ -732,8 +732,12 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
urls = list(set([direct_url]
+ [loc.get('url') for loc in locations or []]))
image_volume_ids = self._extract_cinder_ids(urls)
- image_volumes = self.db.volume_get_all_by_host(
- context, volume['host'], filters={'id': image_volume_ids})
+ if self.driver.capabilities.get('clone_across_pools'):
+ image_volumes = self.db.volume_get_all(
+ context, filters={'id': image_volume_ids})
+ else:
+ image_volumes = self.db.volume_get_all_by_host(
+ context, volume['host'], filters={'id': image_volume_ids})
for image_volume in image_volumes:
# For the case image volume is stored in the service tenant,
diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py
index 804a1c6cb..ecf264d10 100644
--- a/cinder/volume/manager.py
+++ b/cinder/volume/manager.py
@@ -356,7 +356,8 @@ class VolumeManager(manager.CleanableManager,
self.db,
cinder_volume.API(),
max_cache_size,
- max_cache_entries
+ max_cache_entries,
+ self.driver.capabilities.get('clone_across_pools', False)
)
LOG.info('Image-volume cache enabled for host %(host)s.',
{'host': self.host})
diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini
index a379139b9..38a279266 100644
--- a/doc/source/reference/support-matrix.ini
+++ b/doc/source/reference/support-matrix.ini
@@ -957,3 +957,76 @@ driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.zadara=missing
+
+[operation.clone_across_pools]
+title=Clone a volume into a different pool
+status=optional
+notes=Vendor drivers that support cloning a volume into a different
+ storage pool, e.g. when creating a volume from a Cinder-backed
+ Glance image.
+driver.datera=missing
+driver.dell_emc_powermax=missing
+driver.dell_emc_powerstore=missing
+driver.dell_emc_powervault=missing
+driver.dell_emc_sc=missing
+driver.dell_emc_unity=missing
+driver.dell_emc_vmax_af=missing
+driver.dell_emc_vmax_3=missing
+driver.dell_emc_vnx=missing
+driver.dell_emc_powerflex=missing
+driver.dell_emc_xtremio=missing
+driver.fujitsu_eternus=missing
+driver.hitachi_vsp=missing
+driver.hpe_3par=missing
+driver.hpe_msa=missing
+driver.hpe_nimble=missing
+driver.huawei_t_v1=missing
+driver.huawei_t_v2=missing
+driver.huawei_v3=missing
+driver.huawei_f_v3=missing
+driver.huawei_f_v5=missing
+driver.huawei_v5=missing
+driver.huawei_18000=missing
+driver.huawei_dorado=missing
+driver.huawei_fusionstorage=missing
+driver.infinidat=missing
+driver.ibm_ds8k=missing
+driver.ibm_flashsystem=missing
+driver.ibm_gpfs=missing
+driver.ibm_storwize=missing
+driver.ibm_xiv=missing
+driver.infortrend=missing
+driver.inspur=missing
+driver.inspur_as13000=missing
+driver.kaminario=missing
+driver.kioxia_kumoscale=missing
+driver.lenovo=missing
+driver.lightbits_lightos=missing
+driver.linbit_linstor=missing
+driver.lvm=missing
+driver.macrosan=missing
+driver.nec=missing
+driver.nec_v=missing
+driver.netapp_ontap=missing
+driver.netapp_solidfire=missing
+driver.nexenta=missing
+driver.nfs=missing
+driver.opene_joviandss=missing
+driver.prophetstor=missing
+driver.pure=missing
+driver.qnap=missing
+driver.quobyte=missing
+driver.rbd=missing
+driver.rbd_iscsi=missing
+driver.sandstone=missing
+driver.seagate=missing
+driver.storpool=missing
+driver.synology=missing
+driver.toyou_netstor=missing
+driver.vrtsaccess=missing
+driver.vrtscnfs=missing
+driver.vzstorage=missing
+driver.vmware=missing
+driver.win_iscsi=missing
+driver.win_smb=missing
+driver.zadara=missing
--
2.35.1