Merge "Bump hacking to 6.1.0"
diff --git a/.zuul.yaml b/.zuul.yaml
index 2b6b59c..cc74f88 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -21,13 +21,11 @@
         - cinder-tempest-plugin-cbak-ceph
         - cinder-tempest-plugin-cbak-s3
         # As per the Tempest "Stable Branch Support Policy", Tempest will only
-        # support the "Maintained" stable branches and not the "Extended Maintained"
-        # branches. That is what we need to do for all tempest plugins. Only jobs
-        # for the current releasable ("Maintained") stable branches should be listed
-        # here.
+        # support the "Maintained" stable branches, so only jobs for the
+        # current stable branches should be listed here.
+        - cinder-tempest-plugin-basic-2024-1
         - cinder-tempest-plugin-basic-2023-2
         - cinder-tempest-plugin-basic-2023-1
-        - cinder-tempest-plugin-basic-zed
         - cinder-tempest-plugin-protection-functional
     gate:
       jobs:
@@ -38,9 +36,9 @@
         - cinder-tempest-plugin-cbak-ceph
     experimental:
       jobs:
+        - cinder-tempest-plugin-cbak-ceph-2024-1
         - cinder-tempest-plugin-cbak-ceph-2023-2
         - cinder-tempest-plugin-cbak-ceph-2023-1
-        - cinder-tempest-plugin-cbak-ceph-zed
 
 - job:
     name: cinder-tempest-plugin-protection-functional
@@ -256,6 +254,11 @@
       Integration tests that runs with the ceph devstack plugin, py3
       and enable the backup service.
     vars:
+      # FIXME: change I29b1af0a4034decad to tempest added image format tests that
+      # cannot pass in this job because the image data takes a optimized path that
+      # bypasses nova's checks.  Until the nova team decides on a strategy to handle
+      # this issue, we skip these tests.
+      tempest_exclude_regex: (tempest.api.image.v2.test_images_formats.ImagesFormatTest.test_compute_rejects)
       configure_swap_size: 4096
       devstack_local_conf:
         test-config:
@@ -269,6 +272,12 @@
     timeout: 10800
 
 - job:
+    name: cinder-tempest-plugin-cbak-ceph-2024-1
+    parent: cinder-tempest-plugin-cbak-ceph
+    nodeset: openstack-single-node-jammy
+    override-checkout: stable/2024.1
+
+- job:
     name: cinder-tempest-plugin-cbak-ceph-2023-2
     parent: cinder-tempest-plugin-cbak-ceph
     nodeset: openstack-single-node-jammy
@@ -280,12 +289,6 @@
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2023.1
 
-- job:
-    name: cinder-tempest-plugin-cbak-ceph-zed
-    parent: cinder-tempest-plugin-cbak-ceph
-    nodeset: openstack-single-node-focal
-    override-checkout: stable/zed
-
 # variant for pre-Ussuri branches (no volume revert for Ceph),
 # should this job be used on those branches
 - job:
@@ -416,6 +419,12 @@
       - ^releasenotes/.*$
 
 - job:
+    name: cinder-tempest-plugin-basic-2024-1
+    parent: cinder-tempest-plugin-basic
+    nodeset: openstack-single-node-jammy
+    override-checkout: stable/2024.1
+
+- job:
     name: cinder-tempest-plugin-basic-2023-2
     parent: cinder-tempest-plugin-basic
     nodeset: openstack-single-node-jammy
@@ -426,9 +435,3 @@
     parent: cinder-tempest-plugin-basic
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2023.1
-
-- job:
-    name: cinder-tempest-plugin-basic-zed
-    parent: cinder-tempest-plugin-basic
-    nodeset: openstack-single-node-focal
-    override-checkout: stable/zed
diff --git a/cinder_tempest_plugin/api/volume/base.py b/cinder_tempest_plugin/api/volume/base.py
index 1fd82bf..c0f53bd 100644
--- a/cinder_tempest_plugin/api/volume/base.py
+++ b/cinder_tempest_plugin/api/volume/base.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import io
+
 from tempest.common import compute
 from tempest.common import waiters
 from tempest import config
@@ -158,6 +160,29 @@
                         body['id'])
         return body
 
+    @classmethod
+    def create_image_with_data(cls, **kwargs):
+        # we do this as a class method so we can use the
+        # addClassResourceCleanup functionality of tempest.test.BaseTestCase
+        images_client = cls.os_primary.image_client_v2
+        if 'min_disk' not in kwargs:
+            kwargs['min_disk'] = 1
+        response = images_client.create_image(**kwargs)
+        image_id = response['id']
+        cls.addClassResourceCleanup(
+            images_client.wait_for_resource_deletion, image_id)
+        cls.addClassResourceCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            images_client.delete_image, image_id)
+
+        # upload "data" to image
+        image_file = io.BytesIO(data_utils.random_bytes(size=1024))
+        images_client.store_image_file(image_id, image_file)
+
+        waiters.wait_for_image_status(images_client, image_id, 'active')
+        image = images_client.show_image(image_id)
+        return image
+
 
 class BaseVolumeAdminTest(BaseVolumeTest):
     """Base test case class for all Volume Admin API tests."""
diff --git a/cinder_tempest_plugin/api/volume/test_create_from_image.py b/cinder_tempest_plugin/api/volume/test_create_from_image.py
index acb1943..f44f630 100644
--- a/cinder_tempest_plugin/api/volume/test_create_from_image.py
+++ b/cinder_tempest_plugin/api/volume/test_create_from_image.py
@@ -10,12 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import io
-
-from tempest.common import waiters
 from tempest import config
-from tempest.lib.common.utils import data_utils
-from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
 from cinder_tempest_plugin.api.volume import base
@@ -32,29 +27,6 @@
         if not CONF.service_available.glance:
             raise cls.skipException("Glance service is disabled")
 
-    @classmethod
-    def create_image_with_data(cls, **kwargs):
-        # we do this as a class method so we can use the
-        # addClassResourceCleanup functionality of tempest.test.BaseTestCase
-        images_client = cls.os_primary.image_client_v2
-        if 'min_disk' not in kwargs:
-            kwargs['min_disk'] = 1
-        response = images_client.create_image(**kwargs)
-        image_id = response['id']
-        cls.addClassResourceCleanup(
-            images_client.wait_for_resource_deletion, image_id)
-        cls.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            images_client.delete_image, image_id)
-
-        # upload "data" to image
-        image_file = io.BytesIO(data_utils.random_bytes(size=1024))
-        images_client.store_image_file(image_id, image_file)
-
-        waiters.wait_for_image_status(images_client, image_id, 'active')
-        image = images_client.show_image(image_id)
-        return image
-
     @decorators.idempotent_id('6e9266ff-a917-4dd5-aa4a-c36e59e7a2a6')
     def test_create_from_image_with_volume_type_image_property(self):
         """Verify that the cinder_img_volume_type image property works.
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
diff --git a/cinder_tempest_plugin/rbac/v3/test_snapshots.py b/cinder_tempest_plugin/rbac/v3/test_snapshots.py
index f11da42..14377a9 100644
--- a/cinder_tempest_plugin/rbac/v3/test_snapshots.py
+++ b/cinder_tempest_plugin/rbac/v3/test_snapshots.py
@@ -17,6 +17,8 @@
 from tempest.lib import decorators
 from tempest.lib import exceptions
 
+import testtools
+
 from cinder_tempest_plugin.rbac.v3 import base as rbac_base
 
 CONF = config.CONF
@@ -309,10 +311,15 @@
     def test_force_delete_snapshot(self):
         self._force_delete_snapshot(expected_status=exceptions.Forbidden)
 
+    # We don't need to skip the unmanage because the failure should happen at
+    # the API level, so it should fail regardless of the driver support for
+    # unmanaging snapshots.
     @decorators.idempotent_id('35495666-b663-4c68-ba44-0695e30a6838')
     def test_unmanage_snapshot(self):
         self._unmanage_snapshot(expected_status=exceptions.Forbidden)
 
+    @testtools.skipUnless(CONF.volume_feature_enabled.manage_snapshot,
+                          'manage snapshots are disabled')
     @decorators.idempotent_id('d2d1326d-fb47-4448-a1e1-2d1219d30fd5')
     def test_manage_snapshot(self):
         self._manage_snapshot(
@@ -362,10 +369,15 @@
     def test_force_delete_snapshot(self):
         self._force_delete_snapshot(expected_status=exceptions.Forbidden)
 
+    # We don't need to skip the unmanage because the failure should happen at
+    # the API level, so it should fail regardless of the driver support for
+    # unmanaging snapshots.
     @decorators.idempotent_id('dd7da3da-68ef-42f5-af1d-29803a4a04fd')
     def test_unmanage_snapshot(self):
         self._unmanage_snapshot(expected_status=exceptions.Forbidden)
 
+    @testtools.skipUnless(CONF.volume_feature_enabled.manage_snapshot,
+                          'manage snapshots are disabled')
     @decorators.idempotent_id('c2501d05-9bca-42d7-9ab5-c0d9133e762f')
     def test_manage_snapshot(self):
         self._manage_snapshot(
diff --git a/cinder_tempest_plugin/scenario/manager.py b/cinder_tempest_plugin/scenario/manager.py
index 8598ade..cffa044 100644
--- a/cinder_tempest_plugin/scenario/manager.py
+++ b/cinder_tempest_plugin/scenario/manager.py
@@ -110,7 +110,6 @@
                 (mount_path, filename))
             md5 = ssh_client.exec_command(
                 'sudo md5sum -b %s/%s|cut -c 1-32' % (mount_path, filename))
-            ssh_client.exec_command('sudo sync')
         return md5
 
     def get_md5_from_file(self, instance, instance_ip, filename,
diff --git a/cinder_tempest_plugin/scenario/test_snapshots.py b/cinder_tempest_plugin/scenario/test_snapshots.py
index f376954..02cd6bd 100644
--- a/cinder_tempest_plugin/scenario/test_snapshots.py
+++ b/cinder_tempest_plugin/scenario/test_snapshots.py
@@ -14,10 +14,16 @@
 #    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.scenario import manager
 
+CONF = config.CONF
+
 
 class SnapshotDataIntegrityTests(manager.ScenarioTest):
 
@@ -121,3 +127,33 @@
 
             self.assertEqual(count_snap, i)
             self.assertEqual(file_map[i], md5_file)
+
+
+class SnapshotDependencyTests(manager.ScenarioTest):
+    @testtools.skipUnless(CONF.volume_feature_enabled.volume_image_dep_tests,
+                          'dependency tests not enabled')
+    @decorators.idempotent_id('e7028f52-f6d4-479c-8809-6f6cf96cfe0f')
+    @utils.services('image', 'volume')
+    def test_snapshot_removal(self):
+        volume_1 = self.create_volume()
+
+        snapshot_1 = self.create_volume_snapshot(volume_1['id'], force=True)
+        waiters.wait_for_volume_resource_status(
+            self.snapshots_client, snapshot_1['id'], 'available')
+
+        clone_kwargs = {'snapshot_id': snapshot_1['id'],
+                        'size': volume_1['size']}
+        volume_2 = self.volumes_client.create_volume(**clone_kwargs)['volume']
+
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, volume_2['id'], 'available')
+        volume_2 = self.volumes_client.show_volume(volume_2['id'])['volume']
+
+        self.snapshots_client.delete_snapshot(snapshot_1['id'])
+        self.snapshots_client.wait_for_resource_deletion(snapshot_1['id'])
+
+        self.volumes_client.delete_volume(volume_1['id'])
+        self.volumes_client.wait_for_resource_deletion(volume_1['id'])
+
+        self.volumes_client.delete_volume(volume_2['id'])
+        self.volumes_client.wait_for_resource_deletion(volume_2['id'])
diff --git a/setup.cfg b/setup.cfg
index f224c5c..3d74cb9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,7 @@
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
 
 [files]
 packages =