blob: 6e194f9550583ae8ff17a6f9f40b3d9267489f2d [file] [log] [blame]
Dan Smitha15846e2021-04-27 11:59:22 -07001# Copyright 2021 Red Hat, Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import io
17
18from oslo_utils import units
19from tempest.common import utils
20from tempest.common import waiters
21from tempest import config
22from tempest.lib.common.utils import data_utils
23from tempest.lib.common.utils import test_utils
24from tempest.lib import decorators
25from tempest.lib import exceptions as lib_exc
26from tempest.scenario import manager
27
28CONF = config.CONF
29
30
31class ImageQuotaTest(manager.ScenarioTest):
32 credentials = ['primary', 'system_admin']
33
34 @classmethod
35 def resource_setup(cls):
36 super(ImageQuotaTest, cls).resource_setup()
37
38 # Figure out and record the glance service id
39 services = cls.os_system_admin.identity_services_v3_client.\
40 list_services()
41 glance_services = [x for x in services['services']
42 if x['name'] == 'glance']
43 cls.glance_service_id = glance_services[0]['id']
44
45 # Pre-create all the quota limits and record their IDs so we can
46 # update them in-place without needing to know which ones have been
47 # created and in which order.
48 cls.limit_ids = {}
49
50 try:
51 cls.limit_ids['image_size_total'] = cls._create_limit(
52 'image_size_total', 10)
53 cls.limit_ids['image_stage_total'] = cls._create_limit(
54 'image_stage_total', 10)
55 cls.limit_ids['image_count_total'] = cls._create_limit(
56 'image_count_total', 10)
57 cls.limit_ids['image_count_uploading'] = cls._create_limit(
58 'image_count_uploading', 10)
59 except lib_exc.Forbidden:
60 # If we fail to set limits, it means they are not
61 # registered, and thus we will skip these tests once we
62 # have our os_system_admin client and run
63 # check_quotas_enabled().
64 pass
65
66 def setUp(self):
67 super(ImageQuotaTest, self).setUp()
68 self.created_images = []
69
70 def create_image(self, data=None, **kwargs):
71 """Wrapper that returns a test image."""
72
73 if 'name' not in kwargs:
Martin Kopec213d0a42023-11-30 10:28:14 +010074 name = data_utils.rand_name(
75 prefix=CONF.resource_name_prefix,
76 name=self.__name__ + "-image")
Dan Smitha15846e2021-04-27 11:59:22 -070077 kwargs['name'] = name
78
79 params = dict(kwargs)
80 if data:
81 # NOTE: On glance v1 API, the data should be passed on
82 # a header. Then here handles the data separately.
83 params['data'] = data
84
85 image = self.image_client.create_image(**params)
86 # Image objects returned by the v1 client have the image
87 # data inside a dict that is keyed against 'image'.
88 if 'image' in image:
89 image = image['image']
90 self.created_images.append(image['id'])
91 self.addCleanup(
92 self.image_client.wait_for_resource_deletion,
93 image['id'])
94 self.addCleanup(
95 test_utils.call_and_ignore_notfound_exc,
96 self.image_client.delete_image, image['id'])
97 return image
98
99 def check_quotas_enabled(self):
100 # Check to see if we should even be running these tests. Use
101 # the presence of a registered limit that we recognize as an
102 # indication. This will be set up by the operator (or
103 # devstack) if glance is configured to use/honor the unified
104 # limits. If one is set, they must all be set, because glance
105 # has a single all-or-nothing flag for whether or not to use
106 # keystone limits. If anything, checking only one helps to
107 # assert the assumption that, if enabled, they must all be at
108 # least registered for proper operation.
109 registered_limits = self.os_system_admin.identity_limits_client.\
110 get_registered_limits()['registered_limits']
111 if 'image_count_total' not in [x['resource_name']
112 for x in registered_limits]:
113 raise self.skipException('Target system is not configured with '
114 'glance unified limits')
115
116 @classmethod
117 def _create_limit(cls, name, value):
118 return cls.os_system_admin.identity_limits_client.create_limit(
119 CONF.identity.region, cls.glance_service_id,
120 cls.image_client.tenant_id, name, value)['limits'][0]['id']
121
122 def _update_limit(self, name, value):
123 self.os_system_admin.identity_limits_client.update_limit(
124 self.limit_ids[name], value)
125
126 def _cleanup_images(self):
127 while self.created_images:
128 image_id = self.created_images.pop()
129 try:
130 self.image_client.delete_image(image_id)
131 except lib_exc.NotFound:
132 pass
133
134 @decorators.idempotent_id('9b74fe24-183b-41e6-bf42-84c2958a7be8')
135 @utils.services('image', 'identity')
136 def test_image_count_quota(self):
137 self.check_quotas_enabled()
138
139 # Set a quota on the number of images for our tenant to one.
140 self._update_limit('image_count_total', 1)
141
142 # Create one image
143 image = self.create_image(name='first',
144 container_format='bare',
145 disk_format='raw',
146 visibility='private')
147
148 # Second image would put us over quota, so expect failure.
149 self.assertRaises(lib_exc.OverLimit,
150 self.create_image,
151 name='second',
152 container_format='bare',
153 disk_format='raw',
154 visibility='private')
155
156 # Update our limit to two.
157 self._update_limit('image_count_total', 2)
158
159 # Now the same create should succeed.
160 self.create_image(name='second',
161 container_format='bare',
162 disk_format='raw',
163 visibility='private')
164
165 # Third image would put us over quota, so expect failure.
166 self.assertRaises(lib_exc.OverLimit,
167 self.create_image,
168 name='third',
169 container_format='bare',
170 disk_format='raw',
171 visibility='private')
172
173 # Delete the first image to put us under quota.
174 self.image_client.delete_image(image['id'])
175
176 # Now the same create should succeed.
177 self.create_image(name='third',
178 container_format='bare',
179 disk_format='raw',
180 visibility='private')
181
182 # Delete all the images we created before the next test runs,
183 # so that it starts with full quota.
184 self._cleanup_images()
185
186 @decorators.idempotent_id('b103788b-5329-4aa9-8b0d-97f8733460db')
187 @utils.services('image', 'identity')
188 def test_image_count_uploading_quota(self):
189 if not CONF.image_feature_enabled.import_image:
190 skip_msg = (
191 "%s skipped as image import is not available" % __name__)
192 raise self.skipException(skip_msg)
193
194 self.check_quotas_enabled()
195
196 # Set a quota on the number of images we can have in uploading state.
197 self._update_limit('image_stage_total', 10)
198 self._update_limit('image_size_total', 10)
199 self._update_limit('image_count_total', 10)
200 self._update_limit('image_count_uploading', 1)
201
202 file_content = data_utils.random_bytes(1 * units.Mi)
203
204 # Create and stage an image
205 image1 = self.create_image(name='first',
206 container_format='bare',
207 disk_format='raw',
208 visibility='private')
209 self.image_client.stage_image_file(image1['id'],
210 io.BytesIO(file_content))
211
212 # Check that we can not stage another
213 image2 = self.create_image(name='second',
214 container_format='bare',
215 disk_format='raw',
216 visibility='private')
217 self.assertRaises(lib_exc.OverLimit,
218 self.image_client.stage_image_file,
219 image2['id'], io.BytesIO(file_content))
220
221 # ... nor upload directly
222 image3 = self.create_image(name='third',
223 container_format='bare',
224 disk_format='raw',
225 visibility='private')
226 self.assertRaises(lib_exc.OverLimit,
227 self.image_client.store_image_file,
228 image3['id'],
229 io.BytesIO(file_content))
230
231 # Update our quota to make room
232 self._update_limit('image_count_uploading', 2)
233
234 # Now our upload should work
235 self.image_client.store_image_file(image3['id'],
236 io.BytesIO(file_content))
237
238 # ...and because that is no longer in uploading state, we should be
239 # able to stage our second image from above.
240 self.image_client.stage_image_file(image2['id'],
241 io.BytesIO(file_content))
242
243 # Finish our import of image2
244 self.image_client.image_import(image2['id'], method='glance-direct')
245 waiters.wait_for_image_imported_to_stores(self.image_client,
246 image2['id'])
247
248 # Set our quota back to one
249 self._update_limit('image_count_uploading', 1)
250
251 # Since image1 is still staged, we should not be able to upload
252 # an image.
253 image4 = self.create_image(name='fourth',
254 container_format='bare',
255 disk_format='raw',
256 visibility='private')
257 self.assertRaises(lib_exc.OverLimit,
258 self.image_client.store_image_file,
259 image4['id'],
260 io.BytesIO(file_content))
261
262 # Finish our import of image1 to make space in our uploading quota.
263 self.image_client.image_import(image1['id'], method='glance-direct')
264 waiters.wait_for_image_imported_to_stores(self.image_client,
265 image1['id'])
266
267 # Make sure that freed up the one upload quota to complete our upload
268 self.image_client.store_image_file(image4['id'],
269 io.BytesIO(file_content))
270
271 # Delete all the images we created before the next test runs,
272 # so that it starts with full quota.
273 self._cleanup_images()
274
275 @decorators.idempotent_id('05e8d064-c39a-4801-8c6a-465df375ec5b')
276 @utils.services('image', 'identity')
277 def test_image_size_quota(self):
278 self.check_quotas_enabled()
279
280 # Set a quota on the image size for our tenant to 1MiB, and allow ten
281 # images.
282 self._update_limit('image_size_total', 1)
283 self._update_limit('image_count_total', 10)
284 self._update_limit('image_count_uploading', 10)
285
286 file_content = data_utils.random_bytes(1 * units.Mi)
287
288 # Create and upload a 1MiB image.
289 image1 = self.create_image(name='first',
290 container_format='bare',
291 disk_format='raw',
292 visibility='private')
293 self.image_client.store_image_file(image1['id'],
294 io.BytesIO(file_content))
295
296 # Create and upload a second 1MiB image. This succeeds, but
297 # after completion, we are over quota. Despite us being at
298 # quota above, the initial quota check for the second
299 # operation has no idea what the image size will be, and thus
300 # uses delta=0. This will succeed because we're not
301 # technically over-quota and have not asked for any more (this
302 # is oslo.limit behavior). After the second operation,
303 # however, we will be over-quota regardless of the delta and
304 # subsequent attempts will fail. Because glance goes not
305 # require an image size to be declared before upload, this is
306 # really the best it can do without an API change.
307 image2 = self.create_image(name='second',
308 container_format='bare',
309 disk_format='raw',
310 visibility='private')
311 self.image_client.store_image_file(image2['id'],
312 io.BytesIO(file_content))
313
314 # Create and attempt to upload a third 1MiB image. This should fail to
315 # upload (but not create) because we are over quota.
316 image3 = self.create_image(name='third',
317 container_format='bare',
318 disk_format='raw',
319 visibility='private')
320 self.assertRaises(lib_exc.OverLimit,
321 self.image_client.store_image_file,
322 image3['id'], io.BytesIO(file_content))
323
324 # Increase our size quota to 2MiB.
325 self._update_limit('image_size_total', 2)
326
327 # Now the upload of the already-created image is allowed, but
328 # after completion, we are over quota again.
329 self.image_client.store_image_file(image3['id'],
330 io.BytesIO(file_content))
331
332 # Create and attempt to upload a fourth 1MiB image. This should
333 # fail to upload (but not create) because we are over quota.
334 image4 = self.create_image(name='fourth',
335 container_format='bare',
336 disk_format='raw',
337 visibility='private')
338 self.assertRaises(lib_exc.OverLimit,
339 self.image_client.store_image_file,
340 image4['id'], io.BytesIO(file_content))
341
342 # Delete our first image to make space in our existing 2MiB quota.
343 self.image_client.delete_image(image1['id'])
344
345 # Now the upload of the already-created image is allowed.
346 self.image_client.store_image_file(image4['id'],
347 io.BytesIO(file_content))
348
349 # Delete all the images we created before the next test runs,
350 # so that it starts with full quota.
351 self._cleanup_images()
352
353 @decorators.idempotent_id('fc76b8d9-aae5-46fb-9285-099e37f311f7')
354 @utils.services('image', 'identity')
355 def test_image_stage_quota(self):
356 if not CONF.image_feature_enabled.import_image:
357 skip_msg = (
358 "%s skipped as image import is not available" % __name__)
359 raise self.skipException(skip_msg)
360
361 self.check_quotas_enabled()
362
363 # Create a staging quota of 1MiB, allow 10MiB of active
364 # images, and a total of ten images.
365 self._update_limit('image_stage_total', 1)
366 self._update_limit('image_size_total', 10)
367 self._update_limit('image_count_total', 10)
368 self._update_limit('image_count_uploading', 10)
369
370 file_content = data_utils.random_bytes(1 * units.Mi)
371
372 # Create and stage a 1MiB image.
373 image1 = self.create_image(name='first',
374 container_format='bare',
375 disk_format='raw',
376 visibility='private')
377 self.image_client.stage_image_file(image1['id'],
378 io.BytesIO(file_content))
379
380 # Create and stage a second 1MiB image. This succeeds, but
381 # after completion, we are over quota.
382 image2 = self.create_image(name='second',
383 container_format='bare',
384 disk_format='raw',
385 visibility='private')
386 self.image_client.stage_image_file(image2['id'],
387 io.BytesIO(file_content))
388
389 # Create and attempt to stage a third 1MiB image. This should fail to
390 # stage (but not create) because we are over quota.
391 image3 = self.create_image(name='third',
392 container_format='bare',
393 disk_format='raw',
394 visibility='private')
395 self.assertRaises(lib_exc.OverLimit,
396 self.image_client.stage_image_file,
397 image3['id'], io.BytesIO(file_content))
398
399 # Make sure that even though we are over our stage quota, we
400 # can still create and upload an image the regular way.
401 image_upload = self.create_image(name='uploaded',
402 container_format='bare',
403 disk_format='raw',
404 visibility='private')
405 self.image_client.store_image_file(image_upload['id'],
406 io.BytesIO(file_content))
407
408 # Increase our stage quota to two MiB.
409 self._update_limit('image_stage_total', 2)
410
411 # Now the upload of the already-created image is allowed, but
412 # after completion, we are over quota again.
413 self.image_client.stage_image_file(image3['id'],
414 io.BytesIO(file_content))
415
416 # Create and attempt to stage a fourth 1MiB image. This should
417 # fail to stage (but not create) because we are over quota.
418 image4 = self.create_image(name='fourth',
419 container_format='bare',
420 disk_format='raw',
421 visibility='private')
422 self.assertRaises(lib_exc.OverLimit,
423 self.image_client.stage_image_file,
424 image4['id'], io.BytesIO(file_content))
425
426 # Finish our import of image1 to make space in our stage quota.
427 self.image_client.image_import(image1['id'], method='glance-direct')
428 waiters.wait_for_image_imported_to_stores(self.image_client,
429 image1['id'])
430
431 # Now the upload of the already-created image is allowed.
432 self.image_client.stage_image_file(image4['id'],
433 io.BytesIO(file_content))
434
435 # Delete all the images we created before the next test runs,
436 # so that it starts with full quota.
437 self._cleanup_images()