blob: 22256b409c4d39f0b36d006408271190fc70b386 [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:
74 name = data_utils.rand_name(self.__name__ + "-image")
75 kwargs['name'] = name
76
77 params = dict(kwargs)
78 if data:
79 # NOTE: On glance v1 API, the data should be passed on
80 # a header. Then here handles the data separately.
81 params['data'] = data
82
83 image = self.image_client.create_image(**params)
84 # Image objects returned by the v1 client have the image
85 # data inside a dict that is keyed against 'image'.
86 if 'image' in image:
87 image = image['image']
88 self.created_images.append(image['id'])
89 self.addCleanup(
90 self.image_client.wait_for_resource_deletion,
91 image['id'])
92 self.addCleanup(
93 test_utils.call_and_ignore_notfound_exc,
94 self.image_client.delete_image, image['id'])
95 return image
96
97 def check_quotas_enabled(self):
98 # Check to see if we should even be running these tests. Use
99 # the presence of a registered limit that we recognize as an
100 # indication. This will be set up by the operator (or
101 # devstack) if glance is configured to use/honor the unified
102 # limits. If one is set, they must all be set, because glance
103 # has a single all-or-nothing flag for whether or not to use
104 # keystone limits. If anything, checking only one helps to
105 # assert the assumption that, if enabled, they must all be at
106 # least registered for proper operation.
107 registered_limits = self.os_system_admin.identity_limits_client.\
108 get_registered_limits()['registered_limits']
109 if 'image_count_total' not in [x['resource_name']
110 for x in registered_limits]:
111 raise self.skipException('Target system is not configured with '
112 'glance unified limits')
113
114 @classmethod
115 def _create_limit(cls, name, value):
116 return cls.os_system_admin.identity_limits_client.create_limit(
117 CONF.identity.region, cls.glance_service_id,
118 cls.image_client.tenant_id, name, value)['limits'][0]['id']
119
120 def _update_limit(self, name, value):
121 self.os_system_admin.identity_limits_client.update_limit(
122 self.limit_ids[name], value)
123
124 def _cleanup_images(self):
125 while self.created_images:
126 image_id = self.created_images.pop()
127 try:
128 self.image_client.delete_image(image_id)
129 except lib_exc.NotFound:
130 pass
131
132 @decorators.idempotent_id('9b74fe24-183b-41e6-bf42-84c2958a7be8')
133 @utils.services('image', 'identity')
134 def test_image_count_quota(self):
135 self.check_quotas_enabled()
136
137 # Set a quota on the number of images for our tenant to one.
138 self._update_limit('image_count_total', 1)
139
140 # Create one image
141 image = self.create_image(name='first',
142 container_format='bare',
143 disk_format='raw',
144 visibility='private')
145
146 # Second image would put us over quota, so expect failure.
147 self.assertRaises(lib_exc.OverLimit,
148 self.create_image,
149 name='second',
150 container_format='bare',
151 disk_format='raw',
152 visibility='private')
153
154 # Update our limit to two.
155 self._update_limit('image_count_total', 2)
156
157 # Now the same create should succeed.
158 self.create_image(name='second',
159 container_format='bare',
160 disk_format='raw',
161 visibility='private')
162
163 # Third image would put us over quota, so expect failure.
164 self.assertRaises(lib_exc.OverLimit,
165 self.create_image,
166 name='third',
167 container_format='bare',
168 disk_format='raw',
169 visibility='private')
170
171 # Delete the first image to put us under quota.
172 self.image_client.delete_image(image['id'])
173
174 # Now the same create should succeed.
175 self.create_image(name='third',
176 container_format='bare',
177 disk_format='raw',
178 visibility='private')
179
180 # Delete all the images we created before the next test runs,
181 # so that it starts with full quota.
182 self._cleanup_images()
183
184 @decorators.idempotent_id('b103788b-5329-4aa9-8b0d-97f8733460db')
185 @utils.services('image', 'identity')
186 def test_image_count_uploading_quota(self):
187 if not CONF.image_feature_enabled.import_image:
188 skip_msg = (
189 "%s skipped as image import is not available" % __name__)
190 raise self.skipException(skip_msg)
191
192 self.check_quotas_enabled()
193
194 # Set a quota on the number of images we can have in uploading state.
195 self._update_limit('image_stage_total', 10)
196 self._update_limit('image_size_total', 10)
197 self._update_limit('image_count_total', 10)
198 self._update_limit('image_count_uploading', 1)
199
200 file_content = data_utils.random_bytes(1 * units.Mi)
201
202 # Create and stage an image
203 image1 = self.create_image(name='first',
204 container_format='bare',
205 disk_format='raw',
206 visibility='private')
207 self.image_client.stage_image_file(image1['id'],
208 io.BytesIO(file_content))
209
210 # Check that we can not stage another
211 image2 = self.create_image(name='second',
212 container_format='bare',
213 disk_format='raw',
214 visibility='private')
215 self.assertRaises(lib_exc.OverLimit,
216 self.image_client.stage_image_file,
217 image2['id'], io.BytesIO(file_content))
218
219 # ... nor upload directly
220 image3 = self.create_image(name='third',
221 container_format='bare',
222 disk_format='raw',
223 visibility='private')
224 self.assertRaises(lib_exc.OverLimit,
225 self.image_client.store_image_file,
226 image3['id'],
227 io.BytesIO(file_content))
228
229 # Update our quota to make room
230 self._update_limit('image_count_uploading', 2)
231
232 # Now our upload should work
233 self.image_client.store_image_file(image3['id'],
234 io.BytesIO(file_content))
235
236 # ...and because that is no longer in uploading state, we should be
237 # able to stage our second image from above.
238 self.image_client.stage_image_file(image2['id'],
239 io.BytesIO(file_content))
240
241 # Finish our import of image2
242 self.image_client.image_import(image2['id'], method='glance-direct')
243 waiters.wait_for_image_imported_to_stores(self.image_client,
244 image2['id'])
245
246 # Set our quota back to one
247 self._update_limit('image_count_uploading', 1)
248
249 # Since image1 is still staged, we should not be able to upload
250 # an image.
251 image4 = self.create_image(name='fourth',
252 container_format='bare',
253 disk_format='raw',
254 visibility='private')
255 self.assertRaises(lib_exc.OverLimit,
256 self.image_client.store_image_file,
257 image4['id'],
258 io.BytesIO(file_content))
259
260 # Finish our import of image1 to make space in our uploading quota.
261 self.image_client.image_import(image1['id'], method='glance-direct')
262 waiters.wait_for_image_imported_to_stores(self.image_client,
263 image1['id'])
264
265 # Make sure that freed up the one upload quota to complete our upload
266 self.image_client.store_image_file(image4['id'],
267 io.BytesIO(file_content))
268
269 # Delete all the images we created before the next test runs,
270 # so that it starts with full quota.
271 self._cleanup_images()
272
273 @decorators.idempotent_id('05e8d064-c39a-4801-8c6a-465df375ec5b')
274 @utils.services('image', 'identity')
275 def test_image_size_quota(self):
276 self.check_quotas_enabled()
277
278 # Set a quota on the image size for our tenant to 1MiB, and allow ten
279 # images.
280 self._update_limit('image_size_total', 1)
281 self._update_limit('image_count_total', 10)
282 self._update_limit('image_count_uploading', 10)
283
284 file_content = data_utils.random_bytes(1 * units.Mi)
285
286 # Create and upload a 1MiB image.
287 image1 = self.create_image(name='first',
288 container_format='bare',
289 disk_format='raw',
290 visibility='private')
291 self.image_client.store_image_file(image1['id'],
292 io.BytesIO(file_content))
293
294 # Create and upload a second 1MiB image. This succeeds, but
295 # after completion, we are over quota. Despite us being at
296 # quota above, the initial quota check for the second
297 # operation has no idea what the image size will be, and thus
298 # uses delta=0. This will succeed because we're not
299 # technically over-quota and have not asked for any more (this
300 # is oslo.limit behavior). After the second operation,
301 # however, we will be over-quota regardless of the delta and
302 # subsequent attempts will fail. Because glance goes not
303 # require an image size to be declared before upload, this is
304 # really the best it can do without an API change.
305 image2 = self.create_image(name='second',
306 container_format='bare',
307 disk_format='raw',
308 visibility='private')
309 self.image_client.store_image_file(image2['id'],
310 io.BytesIO(file_content))
311
312 # Create and attempt to upload a third 1MiB image. This should fail to
313 # upload (but not create) because we are over quota.
314 image3 = self.create_image(name='third',
315 container_format='bare',
316 disk_format='raw',
317 visibility='private')
318 self.assertRaises(lib_exc.OverLimit,
319 self.image_client.store_image_file,
320 image3['id'], io.BytesIO(file_content))
321
322 # Increase our size quota to 2MiB.
323 self._update_limit('image_size_total', 2)
324
325 # Now the upload of the already-created image is allowed, but
326 # after completion, we are over quota again.
327 self.image_client.store_image_file(image3['id'],
328 io.BytesIO(file_content))
329
330 # Create and attempt to upload a fourth 1MiB image. This should
331 # fail to upload (but not create) because we are over quota.
332 image4 = self.create_image(name='fourth',
333 container_format='bare',
334 disk_format='raw',
335 visibility='private')
336 self.assertRaises(lib_exc.OverLimit,
337 self.image_client.store_image_file,
338 image4['id'], io.BytesIO(file_content))
339
340 # Delete our first image to make space in our existing 2MiB quota.
341 self.image_client.delete_image(image1['id'])
342
343 # Now the upload of the already-created image is allowed.
344 self.image_client.store_image_file(image4['id'],
345 io.BytesIO(file_content))
346
347 # Delete all the images we created before the next test runs,
348 # so that it starts with full quota.
349 self._cleanup_images()
350
351 @decorators.idempotent_id('fc76b8d9-aae5-46fb-9285-099e37f311f7')
352 @utils.services('image', 'identity')
353 def test_image_stage_quota(self):
354 if not CONF.image_feature_enabled.import_image:
355 skip_msg = (
356 "%s skipped as image import is not available" % __name__)
357 raise self.skipException(skip_msg)
358
359 self.check_quotas_enabled()
360
361 # Create a staging quota of 1MiB, allow 10MiB of active
362 # images, and a total of ten images.
363 self._update_limit('image_stage_total', 1)
364 self._update_limit('image_size_total', 10)
365 self._update_limit('image_count_total', 10)
366 self._update_limit('image_count_uploading', 10)
367
368 file_content = data_utils.random_bytes(1 * units.Mi)
369
370 # Create and stage a 1MiB image.
371 image1 = self.create_image(name='first',
372 container_format='bare',
373 disk_format='raw',
374 visibility='private')
375 self.image_client.stage_image_file(image1['id'],
376 io.BytesIO(file_content))
377
378 # Create and stage a second 1MiB image. This succeeds, but
379 # after completion, we are over quota.
380 image2 = self.create_image(name='second',
381 container_format='bare',
382 disk_format='raw',
383 visibility='private')
384 self.image_client.stage_image_file(image2['id'],
385 io.BytesIO(file_content))
386
387 # Create and attempt to stage a third 1MiB image. This should fail to
388 # stage (but not create) because we are over quota.
389 image3 = self.create_image(name='third',
390 container_format='bare',
391 disk_format='raw',
392 visibility='private')
393 self.assertRaises(lib_exc.OverLimit,
394 self.image_client.stage_image_file,
395 image3['id'], io.BytesIO(file_content))
396
397 # Make sure that even though we are over our stage quota, we
398 # can still create and upload an image the regular way.
399 image_upload = self.create_image(name='uploaded',
400 container_format='bare',
401 disk_format='raw',
402 visibility='private')
403 self.image_client.store_image_file(image_upload['id'],
404 io.BytesIO(file_content))
405
406 # Increase our stage quota to two MiB.
407 self._update_limit('image_stage_total', 2)
408
409 # Now the upload of the already-created image is allowed, but
410 # after completion, we are over quota again.
411 self.image_client.stage_image_file(image3['id'],
412 io.BytesIO(file_content))
413
414 # Create and attempt to stage a fourth 1MiB image. This should
415 # fail to stage (but not create) because we are over quota.
416 image4 = self.create_image(name='fourth',
417 container_format='bare',
418 disk_format='raw',
419 visibility='private')
420 self.assertRaises(lib_exc.OverLimit,
421 self.image_client.stage_image_file,
422 image4['id'], io.BytesIO(file_content))
423
424 # Finish our import of image1 to make space in our stage quota.
425 self.image_client.image_import(image1['id'], method='glance-direct')
426 waiters.wait_for_image_imported_to_stores(self.image_client,
427 image1['id'])
428
429 # Now the upload of the already-created image is allowed.
430 self.image_client.stage_image_file(image4['id'],
431 io.BytesIO(file_content))
432
433 # Delete all the images we created before the next test runs,
434 # so that it starts with full quota.
435 self._cleanup_images()