| # 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( |
| prefix=CONF.resource_name_prefix, |
| 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() |