Codebase list glance / ce17d0d
Merge tag '2013.2_rc2' into debian/havana Glance havana-rc2 milestone (2013.2.rc2) Thomas Goirand 10 years ago
25 changed file(s) with 674 addition(s) and 160 deletion(s). Raw diff Collapse all Expand all
2828 import webob
2929
3030 from glance.api.common import size_checked_iter
31 from glance.api import policy
3132 from glance.api.v1 import images
3233 from glance.common import exception
3334 from glance.common import utils
5354 def __init__(self, app):
5455 self.cache = image_cache.ImageCache()
5556 self.serializer = images.ImageSerializer()
57 self.policy = policy.Enforcer()
5658 LOG.info(_("Initialized image cache middleware"))
5759 super(CacheFilter, self).__init__(app)
5860
9092 else:
9193 return (version, method, image_id)
9294
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
93102 def process_request(self, request):
94103 """
95104 For requests for an image file, we check the local image
107116 self._stash_request_info(request, image_id, method)
108117
109118 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:
110124 return None
111125
112126 LOG.debug(_("Cache hit for image '%s'"), image_id)
229243 if not image_checksum:
230244 LOG.error(_("Checksum header is missing."))
231245
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
232253 resp.app_iter = self.cache.get_caching_iter(image_id, image_checksum,
233254 resp.app_iter)
234255 return resp
6868 req.environ['api.version'] = version
6969 req.path_info = ''.join(('/v', str(version), req.path_info))
7070 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)
7272 return None
7373
7474 def _match_version_string(self, subject):
123123 image_repo = self.gateway.get_repo(req.context)
124124 try:
125125 image = image_repo.get(image_id)
126 if not image.locations:
127 reason = _("No image data could be found")
128 raise exception.NotFound(reason)
126129 except exception.NotFound as e:
127130 raise webob.exc.HTTPNotFound(explanation=unicode(e))
128131 except exception.Forbidden as e:
129132 raise webob.exc.HTTPForbidden(explanation=unicode(e))
130133
131 if not image.locations:
132 reason = _("No image data could be found")
133 raise webob.exc.HTTPNotFound(reason)
134134 return image
135135
136136
148148 class ResponseSerializer(wsgi.JSONResponseSerializer):
149149 def download(self, response, image):
150150 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))
155158 #NOTE(saschpe): "response.app_iter = ..." currently resets Content-MD5
156159 # (https://github.com/Pylons/webob/issues/86), so it should be set
157160 # afterwards for the time being.
7171 raise webob.exc.HTTPNotFound(explanation=unicode(e))
7272 except exception.Forbidden as e:
7373 raise webob.exc.HTTPForbidden(explanation=unicode(e))
74 except exception.Duplicate as e:
75 raise webob.exc.HTTPConflict(explanation=unicode(e))
7476
7577 @utils.mutating
7678 def update(self, req, image_id, member_id, status):
549549 return base_href
550550
551551 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
562564 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:
565574 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))
571583 return image_view
572584
573585 def create(self, response, image):
375375 return response
376376 response = req.get_response(self.application)
377377 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
379382
380383
381384 class Debug(Middleware):
608611 self.dispatch(self.serializer, action, response, action_result)
609612 return response
610613
614 except webob.exc.HTTPException as e:
615 return e
611616 # return unserializable result (typically a webob exc)
612617 except Exception:
613618 return action_result
228228 return image_members
229229
230230 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
231242 image_member_values = self._format_image_member_to_db(image_member)
232243 new_values = self.db_api.image_member_create(self.context,
233244 image_member_values)
5252 'swift')).execute())
5353
5454 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'])
5657 images_table.update()\
5758 .where(images_table.c.id == image['id'])\
5859 .values(location=fixed_uri).execute()
5960
6061
61 def legacy_parse_uri(uri, to_quote):
62 def legacy_parse_uri(uri, to_quote, image_id):
6263 """
6364 Parse URLs. This method fixes an issue where credentials specified
6465 in the URL are interpreted differently in Python 2.6.1+ than prior
8687 "like so: "
8788 "swift+http://user:pass@authurl.com/v1/container/obj")
8889
89 LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals())
90 LOG.error(_("Invalid store uri for image %s: %s") % (image_id, reason))
9091 raise exception.BadStoreUri(message=reason)
9192
9293 pieces = urlparse.urlparse(uri)
7979
8080 for image in images:
8181 try:
82 fixed_uri = fix_uri_credentials(image['location'], to_quoted)
82 fixed_uri = fix_uri_credentials(image['location'], to_quoted,
83 image['id'])
8384 images_table.update()\
8485 .where(images_table.c.id == image['id'])\
8586 .values(location=fixed_uri).execute()
9697 return crypt.urlsafe_encrypt(CONF.metadata_encryption_key, uri, 64)
9798
9899
99 def fix_uri_credentials(uri, to_quoted):
100 def fix_uri_credentials(uri, to_quoted, image_id):
100101 """
101102 Fix the given uri's embedded credentials by round-tripping with
102103 StoreLocation.
118119 except (TypeError, ValueError) as e:
119120 raise exception.Invalid(str(e))
120121
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):
125126 """
126127 Parse URLs. This method fixes an issue where credentials specified
127128 in the URL are interpreted differently in Python 2.6.1+ than prior
149150 "like so: "
150151 "swift+http://user:pass@authurl.com/v1/container/obj")
151152
152 LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals())
153 LOG.error(_("Invalid store uri for image %s: %s") % (image_id, reason))
153154 raise exception.BadStoreUri(message=reason)
154155
155156 pieces = urlparse.urlparse(uri)
271271 try:
272272 return delete_from_backend(context, uri, **kwargs)
273273 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)
276276 except exception.StoreDeleteNotSupported as e:
277277 LOG.warn(str(e))
278278 except UnsupportedBackend:
279279 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))
282282 LOG.error(msg)
283283
284284
3737 import rados
3838 import rbd
3939 except ImportError:
40 pass
40 rados = None
41 rbd = None
4142
4243 DEFAULT_POOL = 'rbd'
4344 DEFAULT_CONFFILE = '' # librados will locate the default conf file
248249 librbd.create(ioctx, image_name, size, order, old_format=True)
249250 return StoreLocation({'image': image_name})
250251
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.
254255
255256 :param image_name Image's name
256257 :param snapshot_name Image snapshot's name
260261 """
261262 with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn:
262263 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)
273264 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.
274281 rbd.RBD().remove(ioctx, image_name)
275282 except rbd.ImageNotFound:
276283 raise exception.NotFound(
339346 if loc.snapshot:
340347 image.create_snap(loc.snapshot)
341348 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:
346352 self._delete_image(loc.image, loc.snapshot)
353 except exception.NotFound:
354 pass
355
356 raise exc
347357
348358 return (loc.get_uri(), image_size, checksum.hexdigest(), {})
349359
121121 "s3+https:// scheme, like so: "
122122 "s3+https://accesskey:secretkey@"
123123 "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)
125125 raise exception.BadStoreUri(message=reason)
126126
127127 pieces = urlparse.urlparse(uri)
441441 uri = crypt.urlsafe_decrypt(CONF.metadata_encryption_key, uri)
442442
443443 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})
446446
447447 # Here we create a request context with credentials to support
448448 # delayed delete when using multi-tenant backend storage
454454
455455 self.store_api.delete_from_backend(admin_context, uri)
456456 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})
459459
460460 def _read_cleanup_file(self, file_path):
461461 """Reading cleanup to get latest cleanup timestamp.
195195 image_size = 5242880 # 5 MB
196196 image_data = StringIO.StringIO('X' * image_size)
197197 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)
201201
202202 self.assertEqual(image_size, add_size)
203203 self.assertEqual(image_checksum, add_checksum)
334334
335335 image_id = uuidutils.generate_uuid()
336336 image_data = StringIO.StringIO('XXX')
337 uri, _, _ = store.add(image_id, image_data, 3)
337 uri, _, _, _ = store.add(image_id, image_data, 3)
338338
339339 location = glance.store.location.Location(
340340 self.store_name,
351351
352352 container_name = location.store_location.container
353353 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'))
356358
357359 store.set_acls(location, public=True, read_tenants=[read_tenant])
358360
359361 container_name = location.store_location.container
360362 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'))
362364 self.assertEqual('', container.get('x-container-write', ''))
363365
364366 (get_iter, get_size) = store.get(location)
213213 response, content = http.request(path, 'GET')
214214 self.assertEqual(response.status, 200)
215215 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))
216331
217332 self.stop_servers()
218333
905905 response = requests.get(path, headers=headers)
906906 self.assertEqual(200, response.status_code)
907907 image = json.loads(response.text)
908 self.assertFalse('locations' in image)
908 self.assertTrue('locations' in image)
909 self.assertTrue(image["locations"] == [])
909910
910911 # Upload some image data, setting the image location
911912 path = self._url('/v2/images/%s/file' % image_id)
150150 self.assertRaises(AttributeError, resource.dispatch, Controller(),
151151 'index', 'on', pants='off')
152152
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
153175
154176 class JSONResponseSerializerTest(test_utils.BaseTestCase):
155177
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()
2121 from glance import context
2222 import glance.db.sqlalchemy.api as db
2323 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
2526
2627
2728 class TestCacheMiddlewareURLMatching(testtools.TestCase):
8687 self.image_checksum = image_checksum
8788
8889 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
92100 def test_checksum_v1_header(self):
93101 cache_filter = ChecksumTestCacheFilter()
94102 headers = {"x-image-meta-checksum": "1234567890"}
95 resp = webob.Response(headers=headers)
103 resp = webob.Response(request=self.request, headers=headers)
96104 cache_filter._process_GET_response(resp, None)
97105
98106 self.assertEqual("1234567890", cache_filter.cache.image_checksum)
103111 "x-image-meta-checksum": "1234567890",
104112 "Content-MD5": "abcdefghi"
105113 }
106 resp = webob.Response(headers=headers)
114 resp = webob.Response(request=self.request, headers=headers)
107115 cache_filter._process_GET_response(resp, None)
108116
109117 self.assertEqual("abcdefghi", cache_filter.cache.image_checksum)
110118
111119 def test_checksum_missing_header(self):
112120 cache_filter = ChecksumTestCacheFilter()
113 resp = webob.Response()
121 resp = webob.Response(request=self.request)
114122 cache_filter._process_GET_response(resp, None)
115123
116124 self.assertEqual(None, cache_filter.cache.image_checksum)
142150 pass
143151
144152 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):
153157 def test_v1_deleted_image_fetch(self):
154158 """
155159 Test for determining that when an admin tries to download a deleted
181185
182186 image_id = 'test1'
183187 request = webob.Request.blank('/v1/images/%s' % image_id)
188 request.context = context.RequestContext()
184189
185190 cache_filter = ProcessRequestTestCacheFilter()
186191 self.stubs.Set(cache_filter, '_process_v1_request',
306311 self.assertEqual(response.headers['Content-Length'],
307312 '123456789')
308313
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):
319333 def test_process_v1_DELETE_response(self):
320334 image_id = 'test1'
321335 request = webob.Request.blank('/v1/images/%s' % image_id)
322336 request.context = context.RequestContext()
323337 cache_filter = ProcessRequestTestCacheFilter()
324338 headers = {"x-image-meta-deleted": True}
325 resp = webob.Response(headers=headers)
339 resp = webob.Response(request=request, headers=headers)
326340 actual = cache_filter._process_DELETE_response(resp, image_id)
327341 self.assertEqual(actual, resp)
328342
334348 self.assertEqual(200, actual)
335349
336350 def test_process_response(self):
337 def fake_fetch_request_info():
351 def fake_fetch_request_info(*args, **kwargs):
338352 return ('test1', 'GET')
339353
340354 cache_filter = ProcessRequestTestCacheFilter()
343357 request = webob.Request.blank('/v1/images/%s' % image_id)
344358 request.context = context.RequestContext()
345359 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)
423423 self.assertEqual(retreived_image_member.status,
424424 'pending')
425425
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
426443 def test_remove_image_member(self):
427444 image_member = self.image_member_repo.get(TENANT2)
428445 self.image_member_repo.remove(image_member)
1212 # License for the specific language governing permissions and limitations
1313 # under the License.
1414
15 import contextlib
1615 import StringIO
17
18 import stubout
19
2016 from glance.common import exception
2117 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
2320 from glance.store.rbd import StoreLocation
2421 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
3624
3725
3826 class TestStore(base.StoreClearingUnitTest):
3927 def setUp(self):
4028 """Establish a clean test environment"""
41 self.config(**RBD_CONF)
4229 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)
4739
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):
7341 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
7744
7845 def _fake_delete_image(*args, **kwargs):
79 called_commands.append('delete')
46 self.called_commands_actual.append('delete')
8047
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
8351 self.stubs.Set(self.store, '_create_image', _fake_create_image)
8452 self.stubs.Set(self.store, '_delete_image', _fake_delete_image)
53 self.stubs.Set(mock_rbd.Image, '__enter__', _fake_enter)
8554
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)
86139 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()
834834 store = self.mox.CreateMockAnything()
835835 store.add(self.image_id, mox.IgnoreArg(), self.size).AndReturn(
836836 (self.location, self.size, self.checksum, in_metadata))
837 store.__str__().AndReturn(('hello'))
837 store.__str__ = lambda: "hello"
838838
839839 self.mox.ReplayAll()
840840
909909 store = self.mox.CreateMockAnything()
910910 store.add(self.image_id, mox.IgnoreArg(), self.size).AndReturn(
911911 (self.location, self.size, self.checksum, []))
912 store.__str__().AndReturn(('hello'))
912 store.__str__ = lambda: "hello"
913913
914914 self.mox.ReplayAll()
915915
119119 def test_download_forbidden(self):
120120 request = unit_test_utils.get_fake_request()
121121 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()
122134 self.assertRaises(webob.exc.HTTPForbidden, self.controller.download,
123135 request, uuidutils.generate_uuid())
124136
362374 self.assertEqual('application/octet-stream',
363375 response.headers['Content-Type'])
364376
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
365397 def test_upload(self):
366398 request = webob.Request.blank('/')
367399 request.environ = {}
211211 request = unit_test_utils.get_fake_request()
212212 self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
213213 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)
214226
215227 def test_update_done_by_member(self):
216228 request = unit_test_utils.get_fake_request(tenant=TENANT4)
2020 import webob
2121
2222 import glance.api.v2.images
23 from glance.common import exception
2324 from glance.openstack.common import uuidutils
2425 import glance.schema
2526 import glance.store
19461947 expect_next = '/v2/images?sort_key=id&sort_dir=asc&limit=10&marker=%s'
19471948 self.assertEqual(expect_next % UUID2, output['next'])
19481949
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
19491972 def test_show_full_fixture(self):
19501973 expected = {
19511974 'id': UUID1,