summaryrefslogtreecommitdiff
path: root/tempest/scenario
diff options
context:
space:
mode:
authorDan Smith <dansmith@redhat.com>2021-04-27 11:59:22 -0700
committerDan Smith <dansmith@redhat.com>2021-07-16 08:11:08 -0700
commita15846ea1681297cc334a79e815802540fda26b3 (patch)
tree80ed495d88f9bbca62de0c832b7517038339da7f /tempest/scenario
parentef987c411dd4db841c600263907bdd2c38f7e1c1 (diff)
downloadtempest-a15846ea1681297cc334a79e815802540fda26b3.tar.gz
Add tests for glance unified quotas
Related to blueprint glance-unified-quotas Depends-On: https://review.opendev.org/c/openstack/devstack/+/788056 Change-Id: I08ccf5627ba98173507791c10fef0e7917880bc3
Diffstat (limited to 'tempest/scenario')
-rw-r--r--tempest/scenario/test_unified_limits.py435
1 files changed, 435 insertions, 0 deletions
diff --git a/tempest/scenario/test_unified_limits.py b/tempest/scenario/test_unified_limits.py
new file mode 100644
index 000000000..22256b409
--- /dev/null
+++ b/tempest/scenario/test_unified_limits.py
@@ -0,0 +1,435 @@
+# Copyright 2021 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import io
+
+from oslo_utils import units
+from tempest.common import utils
+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 tempest.lib import exceptions as lib_exc
+from tempest.scenario import manager
+
+CONF = config.CONF
+
+
+class ImageQuotaTest(manager.ScenarioTest):
+ credentials = ['primary', 'system_admin']
+
+ @classmethod
+ def resource_setup(cls):
+ super(ImageQuotaTest, cls).resource_setup()
+
+ # Figure out and record the glance service id
+ services = cls.os_system_admin.identity_services_v3_client.\
+ list_services()
+ glance_services = [x for x in services['services']
+ if x['name'] == 'glance']
+ cls.glance_service_id = glance_services[0]['id']
+
+ # Pre-create all the quota limits and record their IDs so we can
+ # update them in-place without needing to know which ones have been
+ # created and in which order.
+ cls.limit_ids = {}
+
+ try:
+ cls.limit_ids['image_size_total'] = cls._create_limit(
+ 'image_size_total', 10)
+ cls.limit_ids['image_stage_total'] = cls._create_limit(
+ 'image_stage_total', 10)
+ cls.limit_ids['image_count_total'] = cls._create_limit(
+ 'image_count_total', 10)
+ cls.limit_ids['image_count_uploading'] = cls._create_limit(
+ 'image_count_uploading', 10)
+ except lib_exc.Forbidden:
+ # If we fail to set limits, it means they are not
+ # registered, and thus we will skip these tests once we
+ # have our os_system_admin client and run
+ # check_quotas_enabled().
+ pass
+
+ def setUp(self):
+ super(ImageQuotaTest, self).setUp()
+ self.created_images = []
+
+ def create_image(self, data=None, **kwargs):
+ """Wrapper that returns a test image."""
+
+ if 'name' not in kwargs:
+ name = data_utils.rand_name(self.__name__ + "-image")
+ kwargs['name'] = name
+
+ params = dict(kwargs)
+ if data:
+ # NOTE: On glance v1 API, the data should be passed on
+ # a header. Then here handles the data separately.
+ params['data'] = data
+
+ image = self.image_client.create_image(**params)
+ # Image objects returned by the v1 client have the image
+ # data inside a dict that is keyed against 'image'.
+ if 'image' in image:
+ image = image['image']
+ self.created_images.append(image['id'])
+ self.addCleanup(
+ self.image_client.wait_for_resource_deletion,
+ image['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.image_client.delete_image, image['id'])
+ return image
+
+ def check_quotas_enabled(self):
+ # Check to see if we should even be running these tests. Use
+ # the presence of a registered limit that we recognize as an
+ # indication. This will be set up by the operator (or
+ # devstack) if glance is configured to use/honor the unified
+ # limits. If one is set, they must all be set, because glance
+ # has a single all-or-nothing flag for whether or not to use
+ # keystone limits. If anything, checking only one helps to
+ # assert the assumption that, if enabled, they must all be at
+ # least registered for proper operation.
+ registered_limits = self.os_system_admin.identity_limits_client.\
+ get_registered_limits()['registered_limits']
+ if 'image_count_total' not in [x['resource_name']
+ for x in registered_limits]:
+ raise self.skipException('Target system is not configured with '
+ 'glance unified limits')
+
+ @classmethod
+ def _create_limit(cls, name, value):
+ return cls.os_system_admin.identity_limits_client.create_limit(
+ CONF.identity.region, cls.glance_service_id,
+ cls.image_client.tenant_id, name, value)['limits'][0]['id']
+
+ def _update_limit(self, name, value):
+ self.os_system_admin.identity_limits_client.update_limit(
+ self.limit_ids[name], value)
+
+ def _cleanup_images(self):
+ while self.created_images:
+ image_id = self.created_images.pop()
+ try:
+ self.image_client.delete_image(image_id)
+ except lib_exc.NotFound:
+ pass
+
+ @decorators.idempotent_id('9b74fe24-183b-41e6-bf42-84c2958a7be8')
+ @utils.services('image', 'identity')
+ def test_image_count_quota(self):
+ self.check_quotas_enabled()
+
+ # Set a quota on the number of images for our tenant to one.
+ self._update_limit('image_count_total', 1)
+
+ # Create one image
+ image = self.create_image(name='first',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+
+ # Second image would put us over quota, so expect failure.
+ self.assertRaises(lib_exc.OverLimit,
+ self.create_image,
+ name='second',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+
+ # Update our limit to two.
+ self._update_limit('image_count_total', 2)
+
+ # Now the same create should succeed.
+ self.create_image(name='second',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+
+ # Third image would put us over quota, so expect failure.
+ self.assertRaises(lib_exc.OverLimit,
+ self.create_image,
+ name='third',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+
+ # Delete the first image to put us under quota.
+ self.image_client.delete_image(image['id'])
+
+ # Now the same create should succeed.
+ self.create_image(name='third',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+
+ # Delete all the images we created before the next test runs,
+ # so that it starts with full quota.
+ self._cleanup_images()
+
+ @decorators.idempotent_id('b103788b-5329-4aa9-8b0d-97f8733460db')
+ @utils.services('image', 'identity')
+ def test_image_count_uploading_quota(self):
+ if not CONF.image_feature_enabled.import_image:
+ skip_msg = (
+ "%s skipped as image import is not available" % __name__)
+ raise self.skipException(skip_msg)
+
+ self.check_quotas_enabled()
+
+ # Set a quota on the number of images we can have in uploading state.
+ self._update_limit('image_stage_total', 10)
+ self._update_limit('image_size_total', 10)
+ self._update_limit('image_count_total', 10)
+ self._update_limit('image_count_uploading', 1)
+
+ file_content = data_utils.random_bytes(1 * units.Mi)
+
+ # Create and stage an image
+ image1 = self.create_image(name='first',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.stage_image_file(image1['id'],
+ io.BytesIO(file_content))
+
+ # Check that we can not stage another
+ image2 = self.create_image(name='second',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.stage_image_file,
+ image2['id'], io.BytesIO(file_content))
+
+ # ... nor upload directly
+ image3 = self.create_image(name='third',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.store_image_file,
+ image3['id'],
+ io.BytesIO(file_content))
+
+ # Update our quota to make room
+ self._update_limit('image_count_uploading', 2)
+
+ # Now our upload should work
+ self.image_client.store_image_file(image3['id'],
+ io.BytesIO(file_content))
+
+ # ...and because that is no longer in uploading state, we should be
+ # able to stage our second image from above.
+ self.image_client.stage_image_file(image2['id'],
+ io.BytesIO(file_content))
+
+ # Finish our import of image2
+ self.image_client.image_import(image2['id'], method='glance-direct')
+ waiters.wait_for_image_imported_to_stores(self.image_client,
+ image2['id'])
+
+ # Set our quota back to one
+ self._update_limit('image_count_uploading', 1)
+
+ # Since image1 is still staged, we should not be able to upload
+ # an image.
+ image4 = self.create_image(name='fourth',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.store_image_file,
+ image4['id'],
+ io.BytesIO(file_content))
+
+ # Finish our import of image1 to make space in our uploading quota.
+ self.image_client.image_import(image1['id'], method='glance-direct')
+ waiters.wait_for_image_imported_to_stores(self.image_client,
+ image1['id'])
+
+ # Make sure that freed up the one upload quota to complete our upload
+ self.image_client.store_image_file(image4['id'],
+ io.BytesIO(file_content))
+
+ # Delete all the images we created before the next test runs,
+ # so that it starts with full quota.
+ self._cleanup_images()
+
+ @decorators.idempotent_id('05e8d064-c39a-4801-8c6a-465df375ec5b')
+ @utils.services('image', 'identity')
+ def test_image_size_quota(self):
+ self.check_quotas_enabled()
+
+ # Set a quota on the image size for our tenant to 1MiB, and allow ten
+ # images.
+ self._update_limit('image_size_total', 1)
+ self._update_limit('image_count_total', 10)
+ self._update_limit('image_count_uploading', 10)
+
+ file_content = data_utils.random_bytes(1 * units.Mi)
+
+ # Create and upload a 1MiB image.
+ image1 = self.create_image(name='first',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.store_image_file(image1['id'],
+ io.BytesIO(file_content))
+
+ # Create and upload a second 1MiB image. This succeeds, but
+ # after completion, we are over quota. Despite us being at
+ # quota above, the initial quota check for the second
+ # operation has no idea what the image size will be, and thus
+ # uses delta=0. This will succeed because we're not
+ # technically over-quota and have not asked for any more (this
+ # is oslo.limit behavior). After the second operation,
+ # however, we will be over-quota regardless of the delta and
+ # subsequent attempts will fail. Because glance goes not
+ # require an image size to be declared before upload, this is
+ # really the best it can do without an API change.
+ image2 = self.create_image(name='second',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.store_image_file(image2['id'],
+ io.BytesIO(file_content))
+
+ # Create and attempt to upload a third 1MiB image. This should fail to
+ # upload (but not create) because we are over quota.
+ image3 = self.create_image(name='third',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.store_image_file,
+ image3['id'], io.BytesIO(file_content))
+
+ # Increase our size quota to 2MiB.
+ self._update_limit('image_size_total', 2)
+
+ # Now the upload of the already-created image is allowed, but
+ # after completion, we are over quota again.
+ self.image_client.store_image_file(image3['id'],
+ io.BytesIO(file_content))
+
+ # Create and attempt to upload a fourth 1MiB image. This should
+ # fail to upload (but not create) because we are over quota.
+ image4 = self.create_image(name='fourth',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.store_image_file,
+ image4['id'], io.BytesIO(file_content))
+
+ # Delete our first image to make space in our existing 2MiB quota.
+ self.image_client.delete_image(image1['id'])
+
+ # Now the upload of the already-created image is allowed.
+ self.image_client.store_image_file(image4['id'],
+ io.BytesIO(file_content))
+
+ # Delete all the images we created before the next test runs,
+ # so that it starts with full quota.
+ self._cleanup_images()
+
+ @decorators.idempotent_id('fc76b8d9-aae5-46fb-9285-099e37f311f7')
+ @utils.services('image', 'identity')
+ def test_image_stage_quota(self):
+ if not CONF.image_feature_enabled.import_image:
+ skip_msg = (
+ "%s skipped as image import is not available" % __name__)
+ raise self.skipException(skip_msg)
+
+ self.check_quotas_enabled()
+
+ # Create a staging quota of 1MiB, allow 10MiB of active
+ # images, and a total of ten images.
+ self._update_limit('image_stage_total', 1)
+ self._update_limit('image_size_total', 10)
+ self._update_limit('image_count_total', 10)
+ self._update_limit('image_count_uploading', 10)
+
+ file_content = data_utils.random_bytes(1 * units.Mi)
+
+ # Create and stage a 1MiB image.
+ image1 = self.create_image(name='first',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.stage_image_file(image1['id'],
+ io.BytesIO(file_content))
+
+ # Create and stage a second 1MiB image. This succeeds, but
+ # after completion, we are over quota.
+ image2 = self.create_image(name='second',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.stage_image_file(image2['id'],
+ io.BytesIO(file_content))
+
+ # Create and attempt to stage a third 1MiB image. This should fail to
+ # stage (but not create) because we are over quota.
+ image3 = self.create_image(name='third',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.stage_image_file,
+ image3['id'], io.BytesIO(file_content))
+
+ # Make sure that even though we are over our stage quota, we
+ # can still create and upload an image the regular way.
+ image_upload = self.create_image(name='uploaded',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.image_client.store_image_file(image_upload['id'],
+ io.BytesIO(file_content))
+
+ # Increase our stage quota to two MiB.
+ self._update_limit('image_stage_total', 2)
+
+ # Now the upload of the already-created image is allowed, but
+ # after completion, we are over quota again.
+ self.image_client.stage_image_file(image3['id'],
+ io.BytesIO(file_content))
+
+ # Create and attempt to stage a fourth 1MiB image. This should
+ # fail to stage (but not create) because we are over quota.
+ image4 = self.create_image(name='fourth',
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ self.assertRaises(lib_exc.OverLimit,
+ self.image_client.stage_image_file,
+ image4['id'], io.BytesIO(file_content))
+
+ # Finish our import of image1 to make space in our stage quota.
+ self.image_client.image_import(image1['id'], method='glance-direct')
+ waiters.wait_for_image_imported_to_stores(self.image_client,
+ image1['id'])
+
+ # Now the upload of the already-created image is allowed.
+ self.image_client.stage_image_file(image4['id'],
+ io.BytesIO(file_content))
+
+ # Delete all the images we created before the next test runs,
+ # so that it starts with full quota.
+ self._cleanup_images()