Tests for volume<->image dependencies

Detect problems related to Cinder volumes not allowing
Glance images to be deleted.

This adds the option
CONF.volume_feature_enabled.volume_image_dep_tests.
This defaults to True because it is expected to pass on all
configurations other than pre-Caracal RBD backends.

Change-Id: I5fee23951958fc0031b59ce437a963c4cea28529
diff --git a/cinder_tempest_plugin/api/volume/test_volume_dependency.py b/cinder_tempest_plugin/api/volume/test_volume_dependency.py
index b204e84..5ea067f 100644
--- a/cinder_tempest_plugin/api/volume/test_volume_dependency.py
+++ b/cinder_tempest_plugin/api/volume/test_volume_dependency.py
@@ -13,8 +13,12 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+
+from tempest.common import utils
+from tempest.common import waiters
 from tempest import config
 from tempest.lib import decorators
+import testtools
 
 from cinder_tempest_plugin.api.volume import base
 
@@ -119,3 +123,133 @@
         self._delete_vol_and_wait(volume_1)
         self._delete_vol_and_wait(volume_2)
         self._delete_vol_and_wait(volume_3)
+
+
+class VolumeImageDependencyTests(base.BaseVolumeTest):
+    """Volume<->image dependency tests.
+
+    These tests perform clones to/from volumes and images,
+    deleting images/volumes that other volumes were cloned from.
+
+    Images and volumes are expected to be independent at the OpenStack
+    level, but in some configurations (i.e. when using Ceph as storage
+    for both Cinder and Glance) it was possible to end up with images
+    or volumes that could not be deleted.  This was fixed for RBD in
+    Cinder 2024.1 change I009d0748f.
+
+    """
+
+    min_microversion = '3.40'
+
+    @classmethod
+    def del_image(cls, image_id):
+        images_client = cls.os_primary.image_client_v2
+        images_client.delete_image(image_id)
+        images_client.wait_for_resource_deletion(image_id)
+
+    @testtools.skipUnless(CONF.volume_feature_enabled.volume_image_dep_tests,
+                          reason='Volume/image dependency tests not enabled.')
+    @utils.services('image', 'volume')
+    @decorators.idempotent_id('7a9fba78-2e4b-42b1-9898-bb4a60685320')
+    def test_image_volume_dependencies_1(self):
+        # image -> volume
+        image_args = {
+            'disk_format': 'raw',
+            'container_format': 'bare',
+            'name': 'image-for-test-7a9fba78-2e4b-42b1-9898-bb4a60685320'
+        }
+        image = self.create_image_with_data(**image_args)
+
+        # create a volume from the image
+        vol_args = {'name': ('volume1-for-test'
+                             '7a9fba78-2e4b-42b1-9898-bb4a60685320'),
+                    'imageRef': image['id']}
+        volume1 = self.create_volume(**vol_args)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume1['id'],
+                                                'available')
+
+        self.volumes_client.delete_volume(volume1['id'])
+        self.volumes_client.wait_for_resource_deletion(volume1['id'])
+
+        self.del_image(image['id'])
+
+    @testtools.skipUnless(CONF.volume_feature_enabled.volume_image_dep_tests,
+                          reason='Volume/image dependency tests not enabled.')
+    @utils.services('image', 'volume')
+    @decorators.idempotent_id('0e20bd6e-440f-41d8-9b5d-fc047ac00423')
+    def test_image_volume_dependencies_2(self):
+        # image -> volume -> volume
+
+        image_args = {
+            'disk_format': 'raw',
+            'container_format': 'bare',
+            'name': 'image-for-test-0e20bd6e-440f-41d8-9b5d-fc047ac00423'
+        }
+        image = self.create_image_with_data(**image_args)
+
+        # create a volume from the image
+        vol_args = {'name': ('volume1-for-test'
+                             '0e20bd6e-440f-41d8-9b5d-fc047ac00423'),
+                    'imageRef': image['id']}
+        volume1 = self.create_volume(**vol_args)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume1['id'],
+                                                'available')
+
+        vol2_args = {'name': ('volume2-for-test-'
+                              '0e20bd6e-440f-41d8-9b5d-fc047ac00423'),
+                     'source_volid': volume1['id']}
+        volume2 = self.create_volume(**vol2_args)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume2['id'],
+                                                'available')
+
+        self.volumes_client.delete_volume(volume1['id'])
+        self.volumes_client.wait_for_resource_deletion(volume1['id'])
+
+        self.del_image(image['id'])
+
+    @testtools.skipUnless(CONF.volume_feature_enabled.volume_image_dep_tests,
+                          reason='Volume/image dependency tests not enabled.')
+    @decorators.idempotent_id('e6050452-06bd-4c7f-9912-45178c83e379')
+    @utils.services('image', 'volume')
+    def test_image_volume_dependencies_3(self):
+        # image -> volume -> snap -> volume
+
+        image_args = {
+            'disk_format': 'raw',
+            'container_format': 'bare',
+            'name': 'image-for-test-e6050452-06bd-4c7f-9912-45178c83e379'
+        }
+        image = self.create_image_with_data(**image_args)
+
+        # create a volume from the image
+        vol_args = {'name': ('volume1-for-test'
+                             'e6050452-06bd-4c7f-9912-45178c83e379'),
+                    'imageRef': image['id']}
+        volume1 = self.create_volume(**vol_args)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume1['id'],
+                                                'available')
+
+        snapshot1 = self.create_snapshot(volume1['id'])
+
+        vol2_args = {'name': ('volume2-for-test-'
+                              'e6050452-06bd-4c7f-9912-45178c83e379'),
+                     'snapshot_id': snapshot1['id']}
+        volume2 = self.create_volume(**vol2_args)
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                volume2['id'],
+                                                'available')
+
+        self.snapshots_client.delete_snapshot(snapshot1['id'])
+        self.snapshots_client.wait_for_resource_deletion(snapshot1['id'])
+
+        self.volumes_client.delete_volume(volume2['id'])
+        self.volumes_client.wait_for_resource_deletion(volume2['id'])
+
+        self.del_image(image['id'])
+
+        self.volumes_client.delete_volume(volume1['id'])
+        self.volumes_client.wait_for_resource_deletion(volume1['id'])
diff --git a/cinder_tempest_plugin/config.py b/cinder_tempest_plugin/config.py
index 78dd6ea..53222b8 100644
--- a/cinder_tempest_plugin/config.py
+++ b/cinder_tempest_plugin/config.py
@@ -22,6 +22,9 @@
     cfg.BoolOpt('volume_revert',
                 default=False,
                 help='Enable to run Cinder volume revert tests'),
+    cfg.BoolOpt('volume_image_dep_tests',
+                default=True,
+                help='Run tests for dependencies between images and volumes')
 ]
 
 # The barbican service is discovered by config_tempest [1], and will appear