diff --git a/.zuul.yaml b/.zuul.yaml index 2b6b59c..e0a0b2e 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 @@ -269,6 +267,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 @@ -279,12 +283,6 @@ parent: cinder-tempest-plugin-cbak-ceph 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 @@ -416,6 +414,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 +430,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 @@ -157,6 +159,29 @@ self.os_primary.servers_client.delete_server, 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): 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 @@ -31,29 +26,6 @@ super(VolumeAndVolumeTypeFromImageTest, cls).skip_checks() 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): 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/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,9 +14,15 @@ # 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 =