Merge tag '2013.2_rc2' into debian/havana
Glance havana-rc2 milestone (2013.2.rc2)
Thomas Goirand
10 years ago
28 | 28 | import webob |
29 | 29 | |
30 | 30 | from glance.api.common import size_checked_iter |
31 | from glance.api import policy | |
31 | 32 | from glance.api.v1 import images |
32 | 33 | from glance.common import exception |
33 | 34 | from glance.common import utils |
53 | 54 | def __init__(self, app): |
54 | 55 | self.cache = image_cache.ImageCache() |
55 | 56 | self.serializer = images.ImageSerializer() |
57 | self.policy = policy.Enforcer() | |
56 | 58 | LOG.info(_("Initialized image cache middleware")) |
57 | 59 | super(CacheFilter, self).__init__(app) |
58 | 60 | |
90 | 92 | else: |
91 | 93 | return (version, method, image_id) |
92 | 94 | |
95 | def _enforce(self, req, action): | |
96 | """Authorize an action against our policies""" | |
97 | try: | |
98 | self.policy.enforce(req.context, action, {}) | |
99 | except exception.Forbidden as e: | |
100 | raise webob.exc.HTTPForbidden(explanation=unicode(e), request=req) | |
101 | ||
93 | 102 | def process_request(self, request): |
94 | 103 | """ |
95 | 104 | For requests for an image file, we check the local image |
107 | 116 | self._stash_request_info(request, image_id, method) |
108 | 117 | |
109 | 118 | if request.method != 'GET' or not self.cache.is_cached(image_id): |
119 | return None | |
120 | ||
121 | try: | |
122 | self._enforce(request, 'download_image') | |
123 | except webob.exc.HTTPForbidden: | |
110 | 124 | return None |
111 | 125 | |
112 | 126 | LOG.debug(_("Cache hit for image '%s'"), image_id) |
229 | 243 | if not image_checksum: |
230 | 244 | LOG.error(_("Checksum header is missing.")) |
231 | 245 | |
246 | # NOTE(zhiyan): image_cache return a generator object and set to | |
247 | # response.app_iter, it will be called by eventlet.wsgi later. | |
248 | # So we need enforce policy firstly but do it by application | |
249 | # since eventlet.wsgi could not catch webob.exc.HTTPForbidden and | |
250 | # return 403 error to client then. | |
251 | self._enforce(resp.request, 'download_image') | |
252 | ||
232 | 253 | resp.app_iter = self.cache.get_caching_iter(image_id, image_checksum, |
233 | 254 | resp.app_iter) |
234 | 255 | return resp |
68 | 68 | req.environ['api.version'] = version |
69 | 69 | req.path_info = ''.join(('/v', str(version), req.path_info)) |
70 | 70 | LOG.debug(_("Matched version: v%d"), version) |
71 | LOG.debug('new uri %s' % req.path_info) | |
71 | LOG.debug('new path %s' % req.path_info) | |
72 | 72 | return None |
73 | 73 | |
74 | 74 | def _match_version_string(self, subject): |
123 | 123 | image_repo = self.gateway.get_repo(req.context) |
124 | 124 | try: |
125 | 125 | image = image_repo.get(image_id) |
126 | if not image.locations: | |
127 | reason = _("No image data could be found") | |
128 | raise exception.NotFound(reason) | |
126 | 129 | except exception.NotFound as e: |
127 | 130 | raise webob.exc.HTTPNotFound(explanation=unicode(e)) |
128 | 131 | except exception.Forbidden as e: |
129 | 132 | raise webob.exc.HTTPForbidden(explanation=unicode(e)) |
130 | 133 | |
131 | if not image.locations: | |
132 | reason = _("No image data could be found") | |
133 | raise webob.exc.HTTPNotFound(reason) | |
134 | 134 | return image |
135 | 135 | |
136 | 136 | |
148 | 148 | class ResponseSerializer(wsgi.JSONResponseSerializer): |
149 | 149 | def download(self, response, image): |
150 | 150 | response.headers['Content-Type'] = 'application/octet-stream' |
151 | # NOTE(markwash): filesystem store (and maybe others?) cause a problem | |
152 | # with the caching middleware if they are not wrapped in an iterator | |
153 | # very strange | |
154 | response.app_iter = iter(image.get_data()) | |
151 | try: | |
152 | # NOTE(markwash): filesystem store (and maybe others?) cause a | |
153 | # problem with the caching middleware if they are not wrapped in | |
154 | # an iterator very strange | |
155 | response.app_iter = iter(image.get_data()) | |
156 | except exception.Forbidden as e: | |
157 | raise webob.exc.HTTPForbidden(unicode(e)) | |
155 | 158 | #NOTE(saschpe): "response.app_iter = ..." currently resets Content-MD5 |
156 | 159 | # (https://github.com/Pylons/webob/issues/86), so it should be set |
157 | 160 | # afterwards for the time being. |
71 | 71 | raise webob.exc.HTTPNotFound(explanation=unicode(e)) |
72 | 72 | except exception.Forbidden as e: |
73 | 73 | raise webob.exc.HTTPForbidden(explanation=unicode(e)) |
74 | except exception.Duplicate as e: | |
75 | raise webob.exc.HTTPConflict(explanation=unicode(e)) | |
74 | 76 | |
75 | 77 | @utils.mutating |
76 | 78 | def update(self, req, image_id, member_id, status): |
549 | 549 | return base_href |
550 | 550 | |
551 | 551 | def _format_image(self, image): |
552 | image_view = dict(image.extra_properties) | |
553 | attributes = ['name', 'disk_format', 'container_format', 'visibility', | |
554 | 'size', 'status', 'checksum', 'protected', | |
555 | 'min_ram', 'min_disk'] | |
556 | for key in attributes: | |
557 | image_view[key] = getattr(image, key) | |
558 | image_view['id'] = image.image_id | |
559 | image_view['created_at'] = timeutils.isotime(image.created_at) | |
560 | image_view['updated_at'] = timeutils.isotime(image.updated_at) | |
561 | if image.locations: | |
552 | image_view = dict() | |
553 | try: | |
554 | image_view = dict(image.extra_properties) | |
555 | attributes = ['name', 'disk_format', 'container_format', | |
556 | 'visibility', 'size', 'status', 'checksum', | |
557 | 'protected', 'min_ram', 'min_disk'] | |
558 | for key in attributes: | |
559 | image_view[key] = getattr(image, key) | |
560 | image_view['id'] = image.image_id | |
561 | image_view['created_at'] = timeutils.isotime(image.created_at) | |
562 | image_view['updated_at'] = timeutils.isotime(image.updated_at) | |
563 | ||
562 | 564 | if CONF.show_multiple_locations: |
563 | image_view['locations'] = list(image.locations) | |
564 | if CONF.show_image_direct_url: | |
565 | if image.locations: | |
566 | image_view['locations'] = list(image.locations) | |
567 | else: | |
568 | # NOTE (flwang): We will still show "locations": [] if | |
569 | # image.locations is None to indicate it's allowed to show | |
570 | # locations but it's just non-existent. | |
571 | image_view['locations'] = [] | |
572 | ||
573 | if CONF.show_image_direct_url and image.locations: | |
565 | 574 | image_view['direct_url'] = image.locations[0]['url'] |
566 | image_view['tags'] = list(image.tags) | |
567 | image_view['self'] = self._get_image_href(image) | |
568 | image_view['file'] = self._get_image_href(image, 'file') | |
569 | image_view['schema'] = '/v2/schemas/image' | |
570 | image_view = self.schema.filter(image_view) # domain | |
575 | ||
576 | image_view['tags'] = list(image.tags) | |
577 | image_view['self'] = self._get_image_href(image) | |
578 | image_view['file'] = self._get_image_href(image, 'file') | |
579 | image_view['schema'] = '/v2/schemas/image' | |
580 | image_view = self.schema.filter(image_view) # domain | |
581 | except exception.Forbidden as e: | |
582 | raise webob.exc.HTTPForbidden(unicode(e)) | |
571 | 583 | return image_view |
572 | 584 | |
573 | 585 | def create(self, response, image): |
375 | 375 | return response |
376 | 376 | response = req.get_response(self.application) |
377 | 377 | response.request = req |
378 | return self.process_response(response) | |
378 | try: | |
379 | return self.process_response(response) | |
380 | except webob.exc.HTTPException as e: | |
381 | return e | |
379 | 382 | |
380 | 383 | |
381 | 384 | class Debug(Middleware): |
608 | 611 | self.dispatch(self.serializer, action, response, action_result) |
609 | 612 | return response |
610 | 613 | |
614 | except webob.exc.HTTPException as e: | |
615 | return e | |
611 | 616 | # return unserializable result (typically a webob exc) |
612 | 617 | except Exception: |
613 | 618 | return action_result |
228 | 228 | return image_members |
229 | 229 | |
230 | 230 | def add(self, image_member): |
231 | try: | |
232 | self.get(image_member.member_id) | |
233 | except exception.NotFound: | |
234 | pass | |
235 | else: | |
236 | msg = _('The target member %(member_id)s is already ' | |
237 | 'associated with image %(image_id)s.' % | |
238 | dict(member_id=image_member.member_id, | |
239 | image_id=self.image.image_id)) | |
240 | raise exception.Duplicate(msg) | |
241 | ||
231 | 242 | image_member_values = self._format_image_member_to_db(image_member) |
232 | 243 | new_values = self.db_api.image_member_create(self.context, |
233 | 244 | image_member_values) |
52 | 52 | 'swift')).execute()) |
53 | 53 | |
54 | 54 | for image in images: |
55 | fixed_uri = legacy_parse_uri(image['location'], to_quoted) | |
55 | fixed_uri = legacy_parse_uri(image['location'], to_quoted, | |
56 | image['id']) | |
56 | 57 | images_table.update()\ |
57 | 58 | .where(images_table.c.id == image['id'])\ |
58 | 59 | .values(location=fixed_uri).execute() |
59 | 60 | |
60 | 61 | |
61 | def legacy_parse_uri(uri, to_quote): | |
62 | def legacy_parse_uri(uri, to_quote, image_id): | |
62 | 63 | """ |
63 | 64 | Parse URLs. This method fixes an issue where credentials specified |
64 | 65 | in the URL are interpreted differently in Python 2.6.1+ than prior |
86 | 87 | "like so: " |
87 | 88 | "swift+http://user:pass@authurl.com/v1/container/obj") |
88 | 89 | |
89 | LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals()) | |
90 | LOG.error(_("Invalid store uri for image %s: %s") % (image_id, reason)) | |
90 | 91 | raise exception.BadStoreUri(message=reason) |
91 | 92 | |
92 | 93 | pieces = urlparse.urlparse(uri) |
79 | 79 | |
80 | 80 | for image in images: |
81 | 81 | try: |
82 | fixed_uri = fix_uri_credentials(image['location'], to_quoted) | |
82 | fixed_uri = fix_uri_credentials(image['location'], to_quoted, | |
83 | image['id']) | |
83 | 84 | images_table.update()\ |
84 | 85 | .where(images_table.c.id == image['id'])\ |
85 | 86 | .values(location=fixed_uri).execute() |
96 | 97 | return crypt.urlsafe_encrypt(CONF.metadata_encryption_key, uri, 64) |
97 | 98 | |
98 | 99 | |
99 | def fix_uri_credentials(uri, to_quoted): | |
100 | def fix_uri_credentials(uri, to_quoted, image_id): | |
100 | 101 | """ |
101 | 102 | Fix the given uri's embedded credentials by round-tripping with |
102 | 103 | StoreLocation. |
118 | 119 | except (TypeError, ValueError) as e: |
119 | 120 | raise exception.Invalid(str(e)) |
120 | 121 | |
121 | return legacy_parse_uri(decrypted_uri, to_quoted) | |
122 | ||
123 | ||
124 | def legacy_parse_uri(uri, to_quote): | |
122 | return legacy_parse_uri(decrypted_uri, to_quoted, image_id) | |
123 | ||
124 | ||
125 | def legacy_parse_uri(uri, to_quote, image_id): | |
125 | 126 | """ |
126 | 127 | Parse URLs. This method fixes an issue where credentials specified |
127 | 128 | in the URL are interpreted differently in Python 2.6.1+ than prior |
149 | 150 | "like so: " |
150 | 151 | "swift+http://user:pass@authurl.com/v1/container/obj") |
151 | 152 | |
152 | LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals()) | |
153 | LOG.error(_("Invalid store uri for image %s: %s") % (image_id, reason)) | |
153 | 154 | raise exception.BadStoreUri(message=reason) |
154 | 155 | |
155 | 156 | pieces = urlparse.urlparse(uri) |
271 | 271 | try: |
272 | 272 | return delete_from_backend(context, uri, **kwargs) |
273 | 273 | except exception.NotFound: |
274 | msg = _('Failed to delete image in store at URI: %s') | |
275 | LOG.warn(msg % uri) | |
274 | msg = _('Failed to delete image %s in store from URI') | |
275 | LOG.warn(msg % image_id) | |
276 | 276 | except exception.StoreDeleteNotSupported as e: |
277 | 277 | LOG.warn(str(e)) |
278 | 278 | except UnsupportedBackend: |
279 | 279 | exc_type = sys.exc_info()[0].__name__ |
280 | msg = (_('Failed to delete image at %s from store (%s)') % | |
281 | (uri, exc_type)) | |
280 | msg = (_('Failed to delete image %s from store (%s)') % | |
281 | (image_id, exc_type)) | |
282 | 282 | LOG.error(msg) |
283 | 283 | |
284 | 284 |
37 | 37 | import rados |
38 | 38 | import rbd |
39 | 39 | except ImportError: |
40 | pass | |
40 | rados = None | |
41 | rbd = None | |
41 | 42 | |
42 | 43 | DEFAULT_POOL = 'rbd' |
43 | 44 | DEFAULT_CONFFILE = '' # librados will locate the default conf file |
248 | 249 | librbd.create(ioctx, image_name, size, order, old_format=True) |
249 | 250 | return StoreLocation({'image': image_name}) |
250 | 251 | |
251 | def _delete_image(self, image_name, snapshot_name): | |
252 | """ | |
253 | Find the image file to delete. | |
252 | def _delete_image(self, image_name, snapshot_name=None): | |
253 | """ | |
254 | Delete RBD image and snapshot. | |
254 | 255 | |
255 | 256 | :param image_name Image's name |
256 | 257 | :param snapshot_name Image snapshot's name |
260 | 261 | """ |
261 | 262 | with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn: |
262 | 263 | with conn.open_ioctx(self.pool) as ioctx: |
263 | if snapshot_name: | |
264 | with rbd.Image(ioctx, image_name) as image: | |
265 | try: | |
266 | image.unprotect_snap(snapshot_name) | |
267 | except rbd.ImageBusy: | |
268 | log_msg = _("snapshot %s@%s could not be " | |
269 | "unprotected because it is in use") | |
270 | LOG.debug(log_msg % (image_name, snapshot_name)) | |
271 | raise exception.InUseByStore() | |
272 | image.remove_snap(snapshot_name) | |
273 | 264 | try: |
265 | # First remove snapshot. | |
266 | if snapshot_name is not None: | |
267 | with rbd.Image(ioctx, image_name) as image: | |
268 | try: | |
269 | image.unprotect_snap(snapshot_name) | |
270 | except rbd.ImageBusy: | |
271 | log_msg = _("snapshot %(image)s@%(snap)s " | |
272 | "could not be unprotected because " | |
273 | "it is in use") | |
274 | LOG.debug(log_msg % | |
275 | {'image': image_name, | |
276 | 'snap': snapshot_name}) | |
277 | raise exception.InUseByStore() | |
278 | image.remove_snap(snapshot_name) | |
279 | ||
280 | # Then delete image. | |
274 | 281 | rbd.RBD().remove(ioctx, image_name) |
275 | 282 | except rbd.ImageNotFound: |
276 | 283 | raise exception.NotFound( |
339 | 346 | if loc.snapshot: |
340 | 347 | image.create_snap(loc.snapshot) |
341 | 348 | image.protect_snap(loc.snapshot) |
342 | except: | |
343 | # Note(zhiyan): clean up already received data when | |
344 | # error occurs such as ImageSizeLimitExceeded exception. | |
345 | with excutils.save_and_reraise_exception(): | |
349 | except Exception as exc: | |
350 | # Delete image if one was created | |
351 | try: | |
346 | 352 | self._delete_image(loc.image, loc.snapshot) |
353 | except exception.NotFound: | |
354 | pass | |
355 | ||
356 | raise exc | |
347 | 357 | |
348 | 358 | return (loc.get_uri(), image_size, checksum.hexdigest(), {}) |
349 | 359 |
121 | 121 | "s3+https:// scheme, like so: " |
122 | 122 | "s3+https://accesskey:secretkey@" |
123 | 123 | "s3.amazonaws.com/bucket/key-id") |
124 | LOG.debug(_("Invalid store uri %(uri)s: %(reason)s") % locals()) | |
124 | LOG.debug(_("Invalid store uri: %s") % reason) | |
125 | 125 | raise exception.BadStoreUri(message=reason) |
126 | 126 | |
127 | 127 | pieces = urlparse.urlparse(uri) |
441 | 441 | uri = crypt.urlsafe_decrypt(CONF.metadata_encryption_key, uri) |
442 | 442 | |
443 | 443 | try: |
444 | LOG.debug(_("Deleting %(uri)s from image %(image_id)s.") % | |
445 | {'image_id': image_id, 'uri': uri}) | |
444 | LOG.debug(_("Deleting URI from image %(image_id)s.") % | |
445 | {'image_id': image_id}) | |
446 | 446 | |
447 | 447 | # Here we create a request context with credentials to support |
448 | 448 | # delayed delete when using multi-tenant backend storage |
454 | 454 | |
455 | 455 | self.store_api.delete_from_backend(admin_context, uri) |
456 | 456 | except Exception: |
457 | msg = _("Failed to delete image %(image_id)s from %(uri)s.") | |
458 | LOG.error(msg % {'image_id': image_id, 'uri': uri}) | |
457 | msg = _("Failed to delete URI from image %(image_id)s") | |
458 | LOG.error(msg % {'image_id': image_id}) | |
459 | 459 | |
460 | 460 | def _read_cleanup_file(self, file_path): |
461 | 461 | """Reading cleanup to get latest cleanup timestamp. |
195 | 195 | image_size = 5242880 # 5 MB |
196 | 196 | image_data = StringIO.StringIO('X' * image_size) |
197 | 197 | image_checksum = 'eb7f8c3716b9f059cee7617a4ba9d0d3' |
198 | uri, add_size, add_checksum = store.add(image_id, | |
199 | image_data, | |
200 | image_size) | |
198 | uri, add_size, add_checksum, _ = store.add(image_id, | |
199 | image_data, | |
200 | image_size) | |
201 | 201 | |
202 | 202 | self.assertEqual(image_size, add_size) |
203 | 203 | self.assertEqual(image_checksum, add_checksum) |
334 | 334 | |
335 | 335 | image_id = uuidutils.generate_uuid() |
336 | 336 | image_data = StringIO.StringIO('XXX') |
337 | uri, _, _ = store.add(image_id, image_data, 3) | |
337 | uri, _, _, _ = store.add(image_id, image_data, 3) | |
338 | 338 | |
339 | 339 | location = glance.store.location.Location( |
340 | 340 | self.store_name, |
351 | 351 | |
352 | 352 | container_name = location.store_location.container |
353 | 353 | container, _ = swift_get_container(self.swift_client, container_name) |
354 | self.assertEqual(read_tenant, container.get('x-container-read')) | |
355 | self.assertEqual(write_tenant, container.get('x-container-write')) | |
354 | self.assertEqual(read_tenant + ':*', | |
355 | container.get('x-container-read')) | |
356 | self.assertEqual(write_tenant + ':*', | |
357 | container.get('x-container-write')) | |
356 | 358 | |
357 | 359 | store.set_acls(location, public=True, read_tenants=[read_tenant]) |
358 | 360 | |
359 | 361 | container_name = location.store_location.container |
360 | 362 | container, _ = swift_get_container(self.swift_client, container_name) |
361 | self.assertEqual('.r:*', container.get('x-container-read')) | |
363 | self.assertEqual('.r:*,.rlistings', container.get('x-container-read')) | |
362 | 364 | self.assertEqual('', container.get('x-container-write', '')) |
363 | 365 | |
364 | 366 | (get_iter, get_size) = store.get(location) |
213 | 213 | response, content = http.request(path, 'GET') |
214 | 214 | self.assertEqual(response.status, 200) |
215 | 215 | self.assertEqual(int(response['content-length']), FIVE_KB) |
216 | ||
217 | self.stop_servers() | |
218 | ||
219 | @skip_if_disabled | |
220 | def test_cache_middleware_trans_v1_without_download_image_policy(self): | |
221 | """ | |
222 | Ensure the image v1 API image transfer applied 'download_image' | |
223 | policy enforcement. | |
224 | """ | |
225 | self.cleanup() | |
226 | self.start_servers(**self.__dict__.copy()) | |
227 | ||
228 | # Add an image and verify a 200 OK is returned | |
229 | image_data = "*" * FIVE_KB | |
230 | headers = minimal_headers('Image1') | |
231 | path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) | |
232 | http = httplib2.Http() | |
233 | response, content = http.request(path, 'POST', headers=headers, | |
234 | body=image_data) | |
235 | self.assertEqual(response.status, 201) | |
236 | data = json.loads(content) | |
237 | self.assertEqual(data['image']['checksum'], | |
238 | hashlib.md5(image_data).hexdigest()) | |
239 | self.assertEqual(data['image']['size'], FIVE_KB) | |
240 | self.assertEqual(data['image']['name'], "Image1") | |
241 | self.assertEqual(data['image']['is_public'], True) | |
242 | ||
243 | image_id = data['image']['id'] | |
244 | ||
245 | # Verify image not in cache | |
246 | image_cached_path = os.path.join(self.api_server.image_cache_dir, | |
247 | image_id) | |
248 | self.assertFalse(os.path.exists(image_cached_path)) | |
249 | ||
250 | rules = {"context_is_admin": "role:admin", "default": "", | |
251 | "download_image": "!"} | |
252 | self.set_policy_rules(rules) | |
253 | ||
254 | # Grab the image | |
255 | path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port, | |
256 | image_id) | |
257 | http = httplib2.Http() | |
258 | response, content = http.request(path, 'GET') | |
259 | self.assertEqual(response.status, 403) | |
260 | ||
261 | # Now, we delete the image from the server and verify that | |
262 | # the image cache no longer contains the deleted image | |
263 | path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port, | |
264 | image_id) | |
265 | http = httplib2.Http() | |
266 | response, content = http.request(path, 'DELETE') | |
267 | self.assertEqual(response.status, 200) | |
268 | ||
269 | self.assertFalse(os.path.exists(image_cached_path)) | |
270 | ||
271 | self.stop_servers() | |
272 | ||
273 | @skip_if_disabled | |
274 | def test_cache_middleware_trans_v2_without_download_image_policy(self): | |
275 | """ | |
276 | Ensure the image v2 API image transfer applied 'download_image' | |
277 | policy enforcement. | |
278 | """ | |
279 | self.cleanup() | |
280 | self.start_servers(**self.__dict__.copy()) | |
281 | ||
282 | # Add an image and verify success | |
283 | path = "http://%s:%d/v2/images" % ("0.0.0.0", self.api_port) | |
284 | http = httplib2.Http() | |
285 | headers = {'content-type': 'application/json'} | |
286 | image_entity = { | |
287 | 'name': 'Image1', | |
288 | 'visibility': 'public', | |
289 | 'container_format': 'bare', | |
290 | 'disk_format': 'raw', | |
291 | } | |
292 | response, content = http.request(path, 'POST', | |
293 | headers=headers, | |
294 | body=json.dumps(image_entity)) | |
295 | self.assertEqual(response.status, 201) | |
296 | data = json.loads(content) | |
297 | image_id = data['id'] | |
298 | ||
299 | path = "http://%s:%d/v2/images/%s/file" % ("0.0.0.0", self.api_port, | |
300 | image_id) | |
301 | headers = {'content-type': 'application/octet-stream'} | |
302 | image_data = "*" * FIVE_KB | |
303 | response, content = http.request(path, 'PUT', | |
304 | headers=headers, | |
305 | body=image_data) | |
306 | self.assertEqual(response.status, 204) | |
307 | ||
308 | # Verify image not in cache | |
309 | image_cached_path = os.path.join(self.api_server.image_cache_dir, | |
310 | image_id) | |
311 | self.assertFalse(os.path.exists(image_cached_path)) | |
312 | ||
313 | rules = {"context_is_admin": "role:admin", "default": "", | |
314 | "download_image": "!"} | |
315 | self.set_policy_rules(rules) | |
316 | ||
317 | # Grab the image | |
318 | http = httplib2.Http() | |
319 | response, content = http.request(path, 'GET') | |
320 | self.assertEqual(response.status, 403) | |
321 | ||
322 | # Now, we delete the image from the server and verify that | |
323 | # the image cache no longer contains the deleted image | |
324 | path = "http://%s:%d/v2/images/%s" % ("0.0.0.0", self.api_port, | |
325 | image_id) | |
326 | http = httplib2.Http() | |
327 | response, content = http.request(path, 'DELETE') | |
328 | self.assertEqual(response.status, 204) | |
329 | ||
330 | self.assertFalse(os.path.exists(image_cached_path)) | |
216 | 331 | |
217 | 332 | self.stop_servers() |
218 | 333 |
905 | 905 | response = requests.get(path, headers=headers) |
906 | 906 | self.assertEqual(200, response.status_code) |
907 | 907 | image = json.loads(response.text) |
908 | self.assertFalse('locations' in image) | |
908 | self.assertTrue('locations' in image) | |
909 | self.assertTrue(image["locations"] == []) | |
909 | 910 | |
910 | 911 | # Upload some image data, setting the image location |
911 | 912 | path = self._url('/v2/images/%s/file' % image_id) |
150 | 150 | self.assertRaises(AttributeError, resource.dispatch, Controller(), |
151 | 151 | 'index', 'on', pants='off') |
152 | 152 | |
153 | def test_call(self): | |
154 | class FakeController(object): | |
155 | def index(self, shirt, pants=None): | |
156 | return (shirt, pants) | |
157 | ||
158 | resource = wsgi.Resource(FakeController(), None, None) | |
159 | ||
160 | def dispatch(self, obj, action, *args, **kwargs): | |
161 | if isinstance(obj, wsgi.JSONRequestDeserializer): | |
162 | return [] | |
163 | if isinstance(obj, wsgi.JSONResponseSerializer): | |
164 | raise webob.exc.HTTPForbidden() | |
165 | ||
166 | self.stubs.Set(wsgi.Resource, 'dispatch', dispatch) | |
167 | ||
168 | request = wsgi.Request.blank('/') | |
169 | ||
170 | response = resource.__call__(request) | |
171 | ||
172 | self.assertIsInstance(response, webob.exc.HTTPForbidden) | |
173 | self.assertEqual(response.status_code, 403) | |
174 | ||
153 | 175 | |
154 | 176 | class JSONResponseSerializerTest(test_utils.BaseTestCase): |
155 | 177 |
0 | # Copyright 2013 Canonical Ltd. | |
1 | # All Rights Reserved. | |
2 | # | |
3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | |
4 | # not use this file except in compliance with the License. You may obtain | |
5 | # a copy of the License at | |
6 | # | |
7 | # http://www.apache.org/licenses/LICENSE-2.0 | |
8 | # | |
9 | # Unless required by applicable law or agreed to in writing, software | |
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | # License for the specific language governing permissions and limitations | |
13 | # under the License. | |
14 | ||
15 | ||
16 | class mock_rados(object): | |
17 | ||
18 | class ioctx(object): | |
19 | def __init__(self, *args, **kwargs): | |
20 | pass | |
21 | ||
22 | def __enter__(self, *args, **kwargs): | |
23 | return self | |
24 | ||
25 | def __exit__(self, *args, **kwargs): | |
26 | return False | |
27 | ||
28 | def close(self, *args, **kwargs): | |
29 | pass | |
30 | ||
31 | class Rados(object): | |
32 | ||
33 | def __init__(self, *args, **kwargs): | |
34 | pass | |
35 | ||
36 | def __enter__(self, *args, **kwargs): | |
37 | return self | |
38 | ||
39 | def __exit__(self, *args, **kwargs): | |
40 | return False | |
41 | ||
42 | def connect(self, *args, **kwargs): | |
43 | pass | |
44 | ||
45 | def open_ioctx(self, *args, **kwargs): | |
46 | return mock_rados.ioctx() | |
47 | ||
48 | def shutdown(self, *args, **kwargs): | |
49 | pass | |
50 | ||
51 | ||
52 | class mock_rbd(object): | |
53 | ||
54 | class ImageExists(Exception): | |
55 | pass | |
56 | ||
57 | class ImageBusy(Exception): | |
58 | pass | |
59 | ||
60 | class ImageNotFound(Exception): | |
61 | pass | |
62 | ||
63 | class Image(object): | |
64 | ||
65 | def __init__(self, *args, **kwargs): | |
66 | pass | |
67 | ||
68 | def __enter__(self, *args, **kwargs): | |
69 | return self | |
70 | ||
71 | def __exit__(self, *args, **kwargs): | |
72 | pass | |
73 | ||
74 | def create_snap(self, *args, **kwargs): | |
75 | pass | |
76 | ||
77 | def remove_snap(self, *args, **kwargs): | |
78 | pass | |
79 | ||
80 | def protect_snap(self, *args, **kwargs): | |
81 | pass | |
82 | ||
83 | def unprotect_snap(self, *args, **kwargs): | |
84 | pass | |
85 | ||
86 | def read(self, *args, **kwargs): | |
87 | raise NotImplementedError() | |
88 | ||
89 | def write(self, *args, **kwargs): | |
90 | raise NotImplementedError() | |
91 | ||
92 | def resize(self, *args, **kwargs): | |
93 | raise NotImplementedError() | |
94 | ||
95 | def discard(self, offset, length): | |
96 | raise NotImplementedError() | |
97 | ||
98 | def close(self): | |
99 | pass | |
100 | ||
101 | def list_snaps(self): | |
102 | raise NotImplementedError() | |
103 | ||
104 | def parent_info(self): | |
105 | raise NotImplementedError() | |
106 | ||
107 | def size(self): | |
108 | raise NotImplementedError() | |
109 | ||
110 | class RBD(object): | |
111 | ||
112 | def __init__(self, *args, **kwargs): | |
113 | pass | |
114 | ||
115 | def __enter__(self, *args, **kwargs): | |
116 | return self | |
117 | ||
118 | def __exit__(self, *args, **kwargs): | |
119 | return False | |
120 | ||
121 | def create(self, *args, **kwargs): | |
122 | pass | |
123 | ||
124 | def remove(self, *args, **kwargs): | |
125 | pass | |
126 | ||
127 | def list(self, *args, **kwargs): | |
128 | raise NotImplementedError() | |
129 | ||
130 | def clone(self, *args, **kwargs): | |
131 | raise NotImplementedError() |
21 | 21 | from glance import context |
22 | 22 | import glance.db.sqlalchemy.api as db |
23 | 23 | import glance.registry.client.v1.api as registry |
24 | from glance.tests import utils | |
24 | from glance.tests.unit import base | |
25 | from glance.tests.unit import utils as unit_test_utils | |
25 | 26 | |
26 | 27 | |
27 | 28 | class TestCacheMiddlewareURLMatching(testtools.TestCase): |
86 | 87 | self.image_checksum = image_checksum |
87 | 88 | |
88 | 89 | self.cache = DummyCache() |
89 | ||
90 | ||
91 | class TestCacheMiddlewareChecksumVerification(testtools.TestCase): | |
90 | self.policy = unit_test_utils.FakePolicyEnforcer() | |
91 | ||
92 | ||
93 | class TestCacheMiddlewareChecksumVerification(base.IsolatedUnitTest): | |
94 | def setUp(self): | |
95 | super(TestCacheMiddlewareChecksumVerification, self).setUp() | |
96 | self.context = context.RequestContext(is_admin=True) | |
97 | self.request = webob.Request.blank('') | |
98 | self.request.context = self.context | |
99 | ||
92 | 100 | def test_checksum_v1_header(self): |
93 | 101 | cache_filter = ChecksumTestCacheFilter() |
94 | 102 | headers = {"x-image-meta-checksum": "1234567890"} |
95 | resp = webob.Response(headers=headers) | |
103 | resp = webob.Response(request=self.request, headers=headers) | |
96 | 104 | cache_filter._process_GET_response(resp, None) |
97 | 105 | |
98 | 106 | self.assertEqual("1234567890", cache_filter.cache.image_checksum) |
103 | 111 | "x-image-meta-checksum": "1234567890", |
104 | 112 | "Content-MD5": "abcdefghi" |
105 | 113 | } |
106 | resp = webob.Response(headers=headers) | |
114 | resp = webob.Response(request=self.request, headers=headers) | |
107 | 115 | cache_filter._process_GET_response(resp, None) |
108 | 116 | |
109 | 117 | self.assertEqual("abcdefghi", cache_filter.cache.image_checksum) |
110 | 118 | |
111 | 119 | def test_checksum_missing_header(self): |
112 | 120 | cache_filter = ChecksumTestCacheFilter() |
113 | resp = webob.Response() | |
121 | resp = webob.Response(request=self.request) | |
114 | 122 | cache_filter._process_GET_response(resp, None) |
115 | 123 | |
116 | 124 | self.assertEqual(None, cache_filter.cache.image_checksum) |
142 | 150 | pass |
143 | 151 | |
144 | 152 | self.cache = DummyCache() |
145 | ||
146 | ||
147 | class TestCacheMiddlewareProcessRequest(utils.BaseTestCase): | |
148 | def setUp(self): | |
149 | super(TestCacheMiddlewareProcessRequest, self).setUp() | |
150 | self.stubs = stubout.StubOutForTesting() | |
151 | self.addCleanup(self.stubs.UnsetAll) | |
152 | ||
153 | self.policy = unit_test_utils.FakePolicyEnforcer() | |
154 | ||
155 | ||
156 | class TestCacheMiddlewareProcessRequest(base.IsolatedUnitTest): | |
153 | 157 | def test_v1_deleted_image_fetch(self): |
154 | 158 | """ |
155 | 159 | Test for determining that when an admin tries to download a deleted |
181 | 185 | |
182 | 186 | image_id = 'test1' |
183 | 187 | request = webob.Request.blank('/v1/images/%s' % image_id) |
188 | request.context = context.RequestContext() | |
184 | 189 | |
185 | 190 | cache_filter = ProcessRequestTestCacheFilter() |
186 | 191 | self.stubs.Set(cache_filter, '_process_v1_request', |
306 | 311 | self.assertEqual(response.headers['Content-Length'], |
307 | 312 | '123456789') |
308 | 313 | |
309 | ||
310 | class TestProcessResponse(utils.BaseTestCase): | |
311 | def setUp(self): | |
312 | super(TestProcessResponse, self).setUp() | |
313 | self.stubs = stubout.StubOutForTesting() | |
314 | ||
315 | def tearDown(self): | |
316 | super(TestProcessResponse, self).tearDown() | |
317 | self.stubs.UnsetAll() | |
318 | ||
314 | def test_process_request_without_download_image_policy(self): | |
315 | """ | |
316 | Test for cache middleware skip processing when request | |
317 | context has not 'download_image' role. | |
318 | """ | |
319 | image_id = 'test1' | |
320 | request = webob.Request.blank('/v1/images/%s' % image_id) | |
321 | request.context = context.RequestContext() | |
322 | ||
323 | cache_filter = ProcessRequestTestCacheFilter() | |
324 | ||
325 | rules = {'download_image': '!'} | |
326 | self.set_policy_rules(rules) | |
327 | cache_filter.policy = glance.api.policy.Enforcer() | |
328 | ||
329 | self.assertEqual(None, cache_filter.process_request(request)) | |
330 | ||
331 | ||
332 | class TestCacheMiddlewareProcessResponse(base.IsolatedUnitTest): | |
319 | 333 | def test_process_v1_DELETE_response(self): |
320 | 334 | image_id = 'test1' |
321 | 335 | request = webob.Request.blank('/v1/images/%s' % image_id) |
322 | 336 | request.context = context.RequestContext() |
323 | 337 | cache_filter = ProcessRequestTestCacheFilter() |
324 | 338 | headers = {"x-image-meta-deleted": True} |
325 | resp = webob.Response(headers=headers) | |
339 | resp = webob.Response(request=request, headers=headers) | |
326 | 340 | actual = cache_filter._process_DELETE_response(resp, image_id) |
327 | 341 | self.assertEqual(actual, resp) |
328 | 342 | |
334 | 348 | self.assertEqual(200, actual) |
335 | 349 | |
336 | 350 | def test_process_response(self): |
337 | def fake_fetch_request_info(): | |
351 | def fake_fetch_request_info(*args, **kwargs): | |
338 | 352 | return ('test1', 'GET') |
339 | 353 | |
340 | 354 | cache_filter = ProcessRequestTestCacheFilter() |
343 | 357 | request = webob.Request.blank('/v1/images/%s' % image_id) |
344 | 358 | request.context = context.RequestContext() |
345 | 359 | headers = {"x-image-meta-deleted": True} |
346 | resp1 = webob.Response(headers=headers) | |
347 | actual = cache_filter.process_response(resp1) | |
348 | self.assertEqual(actual, resp1) | |
360 | resp = webob.Response(request=request, headers=headers) | |
361 | actual = cache_filter.process_response(resp) | |
362 | self.assertEqual(actual, resp) | |
363 | ||
364 | def test_process_response_without_download_image_policy(self): | |
365 | """ | |
366 | Test for cache middleware raise webob.exc.HTTPForbidden directly | |
367 | when request context has not 'download_image' role. | |
368 | """ | |
369 | def fake_fetch_request_info(*args, **kwargs): | |
370 | return ('test1', 'GET') | |
371 | ||
372 | cache_filter = ProcessRequestTestCacheFilter() | |
373 | cache_filter._fetch_request_info = fake_fetch_request_info | |
374 | rules = {'download_image': '!'} | |
375 | self.set_policy_rules(rules) | |
376 | cache_filter.policy = glance.api.policy.Enforcer() | |
377 | ||
378 | image_id = 'test1' | |
379 | request = webob.Request.blank('/v1/images/%s' % image_id) | |
380 | request.context = context.RequestContext() | |
381 | resp = webob.Response(request=request) | |
382 | self.assertRaises(webob.exc.HTTPForbidden, | |
383 | cache_filter.process_response, resp) | |
384 | self.assertEqual([''], resp.app_iter) |
423 | 423 | self.assertEqual(retreived_image_member.status, |
424 | 424 | 'pending') |
425 | 425 | |
426 | def test_add_duplicate_image_member(self): | |
427 | image = self.image_repo.get(UUID1) | |
428 | image_member = self.image_member_factory.new_image_member(image, | |
429 | TENANT4) | |
430 | self.assertTrue(image_member.id is None) | |
431 | retreived_image_member = self.image_member_repo.add(image_member) | |
432 | self.assertEqual(retreived_image_member.id, image_member.id) | |
433 | self.assertEqual(retreived_image_member.image_id, | |
434 | image_member.image_id) | |
435 | self.assertEqual(retreived_image_member.member_id, | |
436 | image_member.member_id) | |
437 | self.assertEqual(retreived_image_member.status, | |
438 | 'pending') | |
439 | ||
440 | self.assertRaises(exception.Duplicate, self.image_member_repo.add, | |
441 | image_member) | |
442 | ||
426 | 443 | def test_remove_image_member(self): |
427 | 444 | image_member = self.image_member_repo.get(TENANT2) |
428 | 445 | self.image_member_repo.remove(image_member) |
12 | 12 | # License for the specific language governing permissions and limitations |
13 | 13 | # under the License. |
14 | 14 | |
15 | import contextlib | |
16 | 15 | import StringIO |
17 | ||
18 | import stubout | |
19 | ||
20 | 16 | from glance.common import exception |
21 | 17 | from glance.common import utils |
22 | from glance.store.rbd import Store | |
18 | import glance.store.rbd as rbd_store | |
19 | from glance.store.location import Location | |
23 | 20 | from glance.store.rbd import StoreLocation |
24 | 21 | from glance.tests.unit import base |
25 | try: | |
26 | import rados | |
27 | import rbd | |
28 | except ImportError: | |
29 | rbd = None | |
30 | ||
31 | ||
32 | RBD_CONF = {'verbose': True, | |
33 | 'debug': True, | |
34 | 'default_store': 'rbd'} | |
35 | FAKE_CHUNKSIZE = 1 | |
22 | from glance.tests.unit.fake_rados import mock_rados | |
23 | from glance.tests.unit.fake_rados import mock_rbd | |
36 | 24 | |
37 | 25 | |
38 | 26 | class TestStore(base.StoreClearingUnitTest): |
39 | 27 | def setUp(self): |
40 | 28 | """Establish a clean test environment""" |
41 | self.config(**RBD_CONF) | |
42 | 29 | super(TestStore, self).setUp() |
43 | self.stubs = stubout.StubOutForTesting() | |
44 | self.store = Store() | |
45 | self.store.chunk_size = FAKE_CHUNKSIZE | |
46 | self.addCleanup(self.stubs.UnsetAll) | |
30 | self.stubs.Set(rbd_store, 'rados', mock_rados) | |
31 | self.stubs.Set(rbd_store, 'rbd', mock_rbd) | |
32 | self.store = rbd_store.Store() | |
33 | self.store.chunk_size = 2 | |
34 | self.called_commands_actual = [] | |
35 | self.called_commands_expected = [] | |
36 | self.store_specs = {'image': 'fake_image', | |
37 | 'snapshot': 'fake_snapshot'} | |
38 | self.location = StoreLocation(self.store_specs) | |
47 | 39 | |
48 | def test_cleanup_when_add_image_exception(self): | |
49 | if rbd is None: | |
50 | msg = 'RBD store can not add images, skip test.' | |
51 | self.skipTest(msg) | |
52 | ||
53 | called_commands = [] | |
54 | ||
55 | class FakeConnection(object): | |
56 | @contextlib.contextmanager | |
57 | def open_ioctx(self, *args, **kwargs): | |
58 | yield None | |
59 | ||
60 | class FakeImage(object): | |
61 | def write(self, *args, **kwargs): | |
62 | called_commands.append('write') | |
63 | return FAKE_CHUNKSIZE | |
64 | ||
65 | @contextlib.contextmanager | |
66 | def _fake_rados(*args, **kwargs): | |
67 | yield FakeConnection() | |
68 | ||
69 | @contextlib.contextmanager | |
70 | def _fake_image(*args, **kwargs): | |
71 | yield FakeImage() | |
72 | ||
40 | def test_add_w_rbd_image_exception(self): | |
73 | 41 | def _fake_create_image(*args, **kwargs): |
74 | called_commands.append('create') | |
75 | return StoreLocation({'image': 'fake_image', | |
76 | 'snapshot': 'fake_snapshot'}) | |
42 | self.called_commands_actual.append('create') | |
43 | return self.location | |
77 | 44 | |
78 | 45 | def _fake_delete_image(*args, **kwargs): |
79 | called_commands.append('delete') | |
46 | self.called_commands_actual.append('delete') | |
80 | 47 | |
81 | self.stubs.Set(rados, 'Rados', _fake_rados) | |
82 | self.stubs.Set(rbd, 'Image', _fake_image) | |
48 | def _fake_enter(*args, **kwargs): | |
49 | raise exception.NotFound("") | |
50 | ||
83 | 51 | self.stubs.Set(self.store, '_create_image', _fake_create_image) |
84 | 52 | self.stubs.Set(self.store, '_delete_image', _fake_delete_image) |
53 | self.stubs.Set(mock_rbd.Image, '__enter__', _fake_enter) | |
85 | 54 | |
55 | self.assertRaises(exception.NotFound, self.store.add, | |
56 | 'fake_image_id', StringIO.StringIO('xx'), 2) | |
57 | ||
58 | self.called_commands_expected = ['create', 'delete'] | |
59 | ||
60 | def test_add_duplicate_image(self): | |
61 | def _fake_create_image(*args, **kwargs): | |
62 | self.called_commands_actual.append('create') | |
63 | raise mock_rbd.ImageExists() | |
64 | ||
65 | self.stubs.Set(self.store, '_create_image', _fake_create_image) | |
66 | self.assertRaises(exception.Duplicate, self.store.add, | |
67 | 'fake_image_id', StringIO.StringIO('xx'), 2) | |
68 | self.called_commands_expected = ['create'] | |
69 | ||
70 | def test_delete(self): | |
71 | def _fake_remove(*args, **kwargs): | |
72 | self.called_commands_actual.append('remove') | |
73 | ||
74 | self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) | |
75 | self.store.delete(Location('test_rbd_store', StoreLocation, | |
76 | self.location.get_uri())) | |
77 | self.called_commands_expected = ['remove'] | |
78 | ||
79 | def test__delete_image(self): | |
80 | def _fake_remove(*args, **kwargs): | |
81 | self.called_commands_actual.append('remove') | |
82 | ||
83 | self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) | |
84 | self.store._delete_image(self.location) | |
85 | self.called_commands_expected = ['remove'] | |
86 | ||
87 | def test__delete_image_w_snap(self): | |
88 | def _fake_unprotect_snap(*args, **kwargs): | |
89 | self.called_commands_actual.append('unprotect_snap') | |
90 | ||
91 | def _fake_remove_snap(*args, **kwargs): | |
92 | self.called_commands_actual.append('remove_snap') | |
93 | ||
94 | def _fake_remove(*args, **kwargs): | |
95 | self.called_commands_actual.append('remove') | |
96 | ||
97 | self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) | |
98 | self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) | |
99 | self.stubs.Set(mock_rbd.Image, 'remove_snap', _fake_remove_snap) | |
100 | self.store._delete_image(self.location, snapshot_name='snap') | |
101 | ||
102 | self.called_commands_expected = ['unprotect_snap', 'remove_snap', | |
103 | 'remove'] | |
104 | ||
105 | def test__delete_image_w_snap_exc_image_not_found(self): | |
106 | def _fake_unprotect_snap(*args, **kwargs): | |
107 | self.called_commands_actual.append('unprotect_snap') | |
108 | raise mock_rbd.ImageNotFound() | |
109 | ||
110 | self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) | |
111 | self.assertRaises(exception.NotFound, self.store._delete_image, | |
112 | self.location, snapshot_name='snap') | |
113 | ||
114 | self.called_commands_expected = ['unprotect_snap'] | |
115 | ||
116 | def test__delete_image_exc_image_not_found(self): | |
117 | def _fake_remove(*args, **kwargs): | |
118 | self.called_commands_actual.append('remove') | |
119 | raise mock_rbd.ImageNotFound() | |
120 | ||
121 | self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) | |
122 | self.assertRaises(exception.NotFound, self.store._delete_image, | |
123 | self.location, snapshot_name='snap') | |
124 | ||
125 | self.called_commands_expected = ['remove'] | |
126 | ||
127 | def test_image_size_exceeded_exception(self): | |
128 | def _fake_write(*args, **kwargs): | |
129 | if 'write' not in self.called_commands_actual: | |
130 | self.called_commands_actual.append('write') | |
131 | raise exception.ImageSizeLimitExceeded | |
132 | ||
133 | def _fake_delete_image(*args, **kwargs): | |
134 | self.called_commands_actual.append('delete') | |
135 | ||
136 | self.stubs.Set(mock_rbd.Image, 'write', _fake_write) | |
137 | self.stubs.Set(self.store, '_delete_image', _fake_delete_image) | |
138 | data = utils.LimitingReader(StringIO.StringIO('abcd'), 4) | |
86 | 139 | self.assertRaises(exception.ImageSizeLimitExceeded, |
87 | self.store.add, | |
88 | 'fake_image_id', | |
89 | utils.LimitingReader(StringIO.StringIO('xx'), 1), | |
90 | 2) | |
91 | self.assertEqual(called_commands, ['create', 'write', 'delete']) | |
140 | self.store.add, 'fake_image_id', data, 5) | |
141 | ||
142 | self.called_commands_expected = ['write', 'delete'] | |
143 | ||
144 | def tearDown(self): | |
145 | self.assertEqual(self.called_commands_actual, | |
146 | self.called_commands_expected) | |
147 | super(TestStore, self).tearDown() |
834 | 834 | store = self.mox.CreateMockAnything() |
835 | 835 | store.add(self.image_id, mox.IgnoreArg(), self.size).AndReturn( |
836 | 836 | (self.location, self.size, self.checksum, in_metadata)) |
837 | store.__str__().AndReturn(('hello')) | |
837 | store.__str__ = lambda: "hello" | |
838 | 838 | |
839 | 839 | self.mox.ReplayAll() |
840 | 840 | |
909 | 909 | store = self.mox.CreateMockAnything() |
910 | 910 | store.add(self.image_id, mox.IgnoreArg(), self.size).AndReturn( |
911 | 911 | (self.location, self.size, self.checksum, [])) |
912 | store.__str__().AndReturn(('hello')) | |
912 | store.__str__ = lambda: "hello" | |
913 | 913 | |
914 | 914 | self.mox.ReplayAll() |
915 | 915 |
119 | 119 | def test_download_forbidden(self): |
120 | 120 | request = unit_test_utils.get_fake_request() |
121 | 121 | self.image_repo.result = exception.Forbidden() |
122 | self.assertRaises(webob.exc.HTTPForbidden, self.controller.download, | |
123 | request, uuidutils.generate_uuid()) | |
124 | ||
125 | def test_download_get_image_location_forbidden(self): | |
126 | class ImageLocations(object): | |
127 | def __len__(self): | |
128 | raise exception.Forbidden() | |
129 | ||
130 | request = unit_test_utils.get_fake_request() | |
131 | image = FakeImage('abcd') | |
132 | self.image_repo.result = image | |
133 | image.locations = ImageLocations() | |
122 | 134 | self.assertRaises(webob.exc.HTTPForbidden, self.controller.download, |
123 | 135 | request, uuidutils.generate_uuid()) |
124 | 136 | |
362 | 374 | self.assertEqual('application/octet-stream', |
363 | 375 | response.headers['Content-Type']) |
364 | 376 | |
377 | def test_download_forbidden(self): | |
378 | """Make sure the serializer can return 403 forbidden error instead of | |
379 | 500 internal server error. | |
380 | """ | |
381 | def get_data(): | |
382 | raise exception.Forbidden() | |
383 | ||
384 | self.stubs.Set(glance.api.policy.ImageProxy, | |
385 | 'get_data', | |
386 | get_data) | |
387 | request = webob.Request.blank('/') | |
388 | request.environ = {} | |
389 | response = webob.Response() | |
390 | response.request = request | |
391 | image = FakeImage(size=3, data=iter('ZZZ')) | |
392 | image.get_data = get_data | |
393 | self.assertRaises(webob.exc.HTTPForbidden, | |
394 | self.serializer.download, | |
395 | response, image) | |
396 | ||
365 | 397 | def test_upload(self): |
366 | 398 | request = webob.Request.blank('/') |
367 | 399 | request.environ = {} |
211 | 211 | request = unit_test_utils.get_fake_request() |
212 | 212 | self.assertRaises(webob.exc.HTTPForbidden, self.controller.create, |
213 | 213 | request, image_id=UUID2, member_id=TENANT3) |
214 | ||
215 | def test_create_duplicate_member(self): | |
216 | request = unit_test_utils.get_fake_request() | |
217 | image_id = UUID2 | |
218 | member_id = TENANT3 | |
219 | output = self.controller.create(request, image_id=image_id, | |
220 | member_id=member_id) | |
221 | self.assertEqual(UUID2, output.image_id) | |
222 | self.assertEqual(TENANT3, output.member_id) | |
223 | ||
224 | self.assertRaises(webob.exc.HTTPConflict, self.controller.create, | |
225 | request, image_id=image_id, member_id=member_id) | |
214 | 226 | |
215 | 227 | def test_update_done_by_member(self): |
216 | 228 | request = unit_test_utils.get_fake_request(tenant=TENANT4) |
20 | 20 | import webob |
21 | 21 | |
22 | 22 | import glance.api.v2.images |
23 | from glance.common import exception | |
23 | 24 | from glance.openstack.common import uuidutils |
24 | 25 | import glance.schema |
25 | 26 | import glance.store |
1946 | 1947 | expect_next = '/v2/images?sort_key=id&sort_dir=asc&limit=10&marker=%s' |
1947 | 1948 | self.assertEqual(expect_next % UUID2, output['next']) |
1948 | 1949 | |
1950 | def test_index_forbidden_get_image_location(self): | |
1951 | """Make sure the serializer works fine no mater if current user is | |
1952 | authorized to get image location if the show_multiple_locations is | |
1953 | False. | |
1954 | """ | |
1955 | class ImageLocations(object): | |
1956 | def __len__(self): | |
1957 | raise exception.Forbidden() | |
1958 | ||
1959 | self.config(show_multiple_locations=False) | |
1960 | self.config(show_image_direct_url=False) | |
1961 | url = '/v2/images?limit=10&sort_key=id&sort_dir=asc' | |
1962 | request = webob.Request.blank(url) | |
1963 | response = webob.Response(request=request) | |
1964 | result = {'images': self.fixtures} | |
1965 | self.assertEquals(response.status_int, 200) | |
1966 | ||
1967 | # The image index should work though the user is forbidden | |
1968 | result['images'][0].locations = ImageLocations() | |
1969 | self.serializer.index(response, result) | |
1970 | self.assertEquals(response.status_int, 200) | |
1971 | ||
1949 | 1972 | def test_show_full_fixture(self): |
1950 | 1973 | expected = { |
1951 | 1974 | 'id': UUID1, |