Merge lp:~ttx/glance/d2-merge into lp:~hudson-openstack/glance/milestone-proposed
- d2-merge
- Merge into milestone-proposed
Proposed by
Thierry Carrez
Status: | Merged |
---|---|
Approved by: | Brian Waldon |
Approved revision: | 140 |
Merged at revision: | 140 |
Proposed branch: | lp:~ttx/glance/d2-merge |
Merge into: | lp:~hudson-openstack/glance/milestone-proposed |
Diff against target: |
3550 lines (+2184/-485) 23 files modified
Authors (+2/-0) bin/glance (+1/-1) doc/source/client.rst (+30/-1) doc/source/glanceapi.rst (+14/-0) doc/source/registries.rst (+14/-0) glance/api/v1/__init__.py (+4/-4) glance/api/v1/images.py (+172/-103) glance/client.py (+13/-173) glance/common/client.py (+169/-0) glance/common/exception.py (+31/-0) glance/common/wsgi.py (+133/-101) glance/registry/client.py (+22/-21) glance/registry/db/api.py (+16/-6) glance/registry/server.py (+75/-32) glance/utils.py (+0/-18) run_tests.py (+8/-0) tests/functional/test_curl_api.py (+116/-1) tests/functional/test_httplib2_api.py (+274/-0) tests/stubs.py (+19/-10) tests/unit/test_api.py (+498/-0) tests/unit/test_clients.py (+386/-13) tests/unit/test_wsgi.py (+184/-0) tools/pip-requires (+3/-1) |
To merge this branch: | bzr merge lp:~ttx/glance/d2-merge |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Review via email: mp+66176@code.launchpad.net |
Commit message
Merge diablo-2 development work
Description of the change
Merge diablo-2 development work from trunk to milestone-proposed.
The best way to check this is actually to run:
bzr diff --old lp:glance --new lp:~ttx/glance/d2-merge
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Authors' |
2 | --- Authors 2011-04-27 18:00:49 +0000 |
3 | +++ Authors 2011-06-28 16:16:37 +0000 |
4 | @@ -9,6 +9,7 @@ |
5 | Jay Pipes <jaypipes@gmail.com> |
6 | Jinwoo 'Joseph' Suh <jsuh@isi.edu> |
7 | Josh Kearney <josh@jk0.org> |
8 | +Justin Shepherd <jshepher@rackspace.com> |
9 | Ken Pepple <ken.pepple@gmail.com> |
10 | Matt Dietz <matt.dietz@rackspace.com> |
11 | Monty Taylor <mordred@inaugust.com> |
12 | @@ -18,3 +19,4 @@ |
13 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> |
14 | Thierry Carrez <thierry@openstack.org> |
15 | Vishvananda Ishaya <vishvananda@gmail.com> |
16 | +Yuriy Taraday <yorik.sar@gmail.com> |
17 | |
18 | === modified file 'bin/glance' |
19 | --- bin/glance 2011-04-27 18:00:49 +0000 |
20 | +++ bin/glance 2011-06-28 16:16:37 +0000 |
21 | @@ -194,7 +194,7 @@ |
22 | print "Returned the following metadata for the new image:" |
23 | for k, v in sorted(image_meta.items()): |
24 | print " %(k)30s => %(v)s" % locals() |
25 | - except client.ClientConnectionError, e: |
26 | + except exception.ClientConnectionError, e: |
27 | host = options.host |
28 | port = options.port |
29 | print ("Failed to connect to the Glance API server " |
30 | |
31 | === modified file 'doc/source/client.rst' |
32 | --- doc/source/client.rst 2011-05-25 20:03:00 +0000 |
33 | +++ doc/source/client.rst 2011-06-28 16:16:37 +0000 |
34 | @@ -113,7 +113,36 @@ |
35 | c = Client("glance.example.com", 9292) |
36 | |
37 | filters = {'status': 'saving', 'size_max': (5 * 1024 * 1024 * 1024)} |
38 | - print c.get_images_detailed(filters) |
39 | + print c.get_images_detailed(filters=filters) |
40 | + |
41 | +Sorting Images Returned via ``get_images()`` and ``get_images_detailed()`` |
42 | +-------------------------------------------------------------------------- |
43 | + |
44 | +Two parameters are available to sort the list of images returned by |
45 | +these methods. |
46 | + |
47 | +* ``sort_key: KEY`` |
48 | + |
49 | + Images can be ordered by the image attribute ``KEY``. Acceptable values: |
50 | + ``id``, ``name``, ``status``, ``container_format``, ``disk_format``, |
51 | + ``created_at`` (default) and ``updated_at``. |
52 | + |
53 | +* ``sort_dir: DIR`` |
54 | + |
55 | + The direction of the sort may be defined by ``DIR``. Accepted values: |
56 | + ``asc`` for ascending or ``desc`` (default) for descending. |
57 | + |
58 | +The following example will return a list of images sorted alphabetically |
59 | +by name in ascending order. |
60 | + |
61 | +.. code-block:: python |
62 | + |
63 | + from glance.client import Client |
64 | + |
65 | + c = Client("glance.example.com", 9292) |
66 | + |
67 | + print c.get_images(sort_key='name', sort_dir='asc') |
68 | + |
69 | |
70 | Requesting Detailed Metadata on a Specific Image |
71 | ------------------------------------------------ |
72 | |
73 | === modified file 'doc/source/glanceapi.rst' |
74 | --- doc/source/glanceapi.rst 2011-05-26 12:53:48 +0000 |
75 | +++ doc/source/glanceapi.rst 2011-06-28 16:16:37 +0000 |
76 | @@ -129,6 +129,20 @@ |
77 | |
78 | Filters images having a ``size`` attribute less than or equal to ``BYTES`` |
79 | |
80 | +These two resources also accept sort parameters: |
81 | + |
82 | +* ``sort_key=KEY`` |
83 | + |
84 | + Results will be ordered by the specified image attribute ``KEY``. Accepted |
85 | + values include ``id``, ``name``, ``status``, ``disk_format``, |
86 | + ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``. |
87 | + |
88 | +* ``sort_dir=DIR`` |
89 | + |
90 | + Results will be sorted in the direction ``DIR``. Accepted values are ``asc`` |
91 | + for ascending or ``desc`` (default) for descending. |
92 | + |
93 | + |
94 | Requesting Detailed Metadata on a Specific Image |
95 | ------------------------------------------------ |
96 | |
97 | |
98 | === modified file 'doc/source/registries.rst' |
99 | --- doc/source/registries.rst 2011-05-26 12:53:48 +0000 |
100 | +++ doc/source/registries.rst 2011-06-28 16:16:37 +0000 |
101 | @@ -83,6 +83,20 @@ |
102 | |
103 | Filters images having a ``size`` attribute less than or equal to ``BYTES`` |
104 | |
105 | +These two resources also accept sort parameters: |
106 | + |
107 | +* ``sort_key=KEY`` |
108 | + |
109 | + Results will be ordered by the specified image attribute ``KEY``. Accepted |
110 | + values include ``id``, ``name``, ``status``, ``disk_format``, |
111 | + ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``. |
112 | + |
113 | +* ``sort_dir=DIR`` |
114 | + |
115 | + Results will be sorted in the direction ``DIR``. Accepted values are ``asc`` |
116 | + for ascending or ``desc`` (default) for descending. |
117 | + |
118 | + |
119 | ``POST /images`` |
120 | ---------------- |
121 | |
122 | |
123 | === modified file 'glance/api/v1/__init__.py' |
124 | --- glance/api/v1/__init__.py 2011-05-11 23:03:51 +0000 |
125 | +++ glance/api/v1/__init__.py 2011-06-28 16:16:37 +0000 |
126 | @@ -32,11 +32,11 @@ |
127 | def __init__(self, options): |
128 | self.options = options |
129 | mapper = routes.Mapper() |
130 | - controller = images.Controller(options) |
131 | - mapper.resource("image", "images", controller=controller, |
132 | + resource = images.create_resource(options) |
133 | + mapper.resource("image", "images", controller=resource, |
134 | collection={'detail': 'GET'}) |
135 | - mapper.connect("/", controller=controller, action="index") |
136 | - mapper.connect("/images/{id}", controller=controller, action="meta", |
137 | + mapper.connect("/", controller=resource, action="index") |
138 | + mapper.connect("/images/{id}", controller=resource, action="meta", |
139 | conditions=dict(method=["HEAD"])) |
140 | super(API, self).__init__(mapper) |
141 | |
142 | |
143 | === modified file 'glance/api/v1/images.py' |
144 | --- glance/api/v1/images.py 2011-05-17 13:34:42 +0000 |
145 | +++ glance/api/v1/images.py 2011-06-28 16:16:37 +0000 |
146 | @@ -24,7 +24,7 @@ |
147 | import logging |
148 | import sys |
149 | |
150 | -from webob import Response |
151 | +import webob |
152 | from webob.exc import (HTTPNotFound, |
153 | HTTPConflict, |
154 | HTTPBadRequest) |
155 | @@ -45,8 +45,10 @@ |
156 | SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', |
157 | 'size_min', 'size_max'] |
158 | |
159 | - |
160 | -class Controller(wsgi.Controller): |
161 | +SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') |
162 | + |
163 | + |
164 | +class Controller(object): |
165 | |
166 | """ |
167 | WSGI controller for images resource in Glance v1 API |
168 | @@ -80,7 +82,7 @@ |
169 | * checksum -- MD5 checksum of the image data |
170 | * size -- Size of image data in bytes |
171 | |
172 | - :param request: The WSGI/Webob Request object |
173 | + :param req: The WSGI/Webob Request object |
174 | :retval The response body is a mapping of the following form:: |
175 | |
176 | {'images': [ |
177 | @@ -92,14 +94,7 @@ |
178 | 'size': <SIZE>}, ... |
179 | ]} |
180 | """ |
181 | - params = {'filters': self._get_filters(req)} |
182 | - |
183 | - if 'limit' in req.str_params: |
184 | - params['limit'] = req.str_params.get('limit') |
185 | - |
186 | - if 'marker' in req.str_params: |
187 | - params['marker'] = req.str_params.get('marker') |
188 | - |
189 | + params = self._get_query_params(req) |
190 | images = registry.get_images_list(self.options, **params) |
191 | return dict(images=images) |
192 | |
193 | @@ -107,7 +102,7 @@ |
194 | """ |
195 | Returns detailed information for all public, available images |
196 | |
197 | - :param request: The WSGI/Webob Request object |
198 | + :param req: The WSGI/Webob Request object |
199 | :retval The response body is a mapping of the following form:: |
200 | |
201 | {'images': [ |
202 | @@ -125,17 +120,23 @@ |
203 | 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ... |
204 | ]} |
205 | """ |
206 | - params = {'filters': self._get_filters(req)} |
207 | - |
208 | - if 'limit' in req.str_params: |
209 | - params['limit'] = req.str_params.get('limit') |
210 | - |
211 | - if 'marker' in req.str_params: |
212 | - params['marker'] = req.str_params.get('marker') |
213 | - |
214 | + params = self._get_query_params(req) |
215 | images = registry.get_images_detail(self.options, **params) |
216 | return dict(images=images) |
217 | |
218 | + def _get_query_params(self, req): |
219 | + """ |
220 | + Extracts necessary query params from request. |
221 | + |
222 | + :param req: the WSGI Request object |
223 | + :retval dict of parameters that can be used by registry client |
224 | + """ |
225 | + params = {'filters': self._get_filters(req)} |
226 | + for PARAM in SUPPORTED_PARAMS: |
227 | + if PARAM in req.str_params: |
228 | + params[PARAM] = req.str_params.get(PARAM) |
229 | + return params |
230 | + |
231 | def _get_filters(self, req): |
232 | """ |
233 | Return a dictionary of query param filters from the request |
234 | @@ -155,29 +156,22 @@ |
235 | Returns metadata about an image in the HTTP headers of the |
236 | response object |
237 | |
238 | - :param request: The WSGI/Webob Request object |
239 | + :param req: The WSGI/Webob Request object |
240 | :param id: The opaque image identifier |
241 | + :retval similar to 'show' method but without image_data |
242 | |
243 | :raises HTTPNotFound if image metadata is not available to user |
244 | """ |
245 | - image = self.get_image_meta_or_404(req, id) |
246 | - |
247 | - res = Response(request=req) |
248 | - utils.inject_image_meta_into_headers(res, image) |
249 | - res.headers.add('Location', "/v1/images/%s" % id) |
250 | - res.headers.add('ETag', image['checksum']) |
251 | - |
252 | - return req.get_response(res) |
253 | + return { |
254 | + 'image_meta': self.get_image_meta_or_404(req, id), |
255 | + } |
256 | |
257 | def show(self, req, id): |
258 | """ |
259 | - Returns an iterator as a Response object that |
260 | - can be used to retrieve an image's data. The |
261 | - content-type of the response is the content-type |
262 | - of the image, or application/octet-stream if none |
263 | - is known or found. |
264 | + Returns an iterator that can be used to retrieve an image's |
265 | + data along with the image metadata. |
266 | |
267 | - :param request: The WSGI/Webob Request object |
268 | + :param req: The WSGI/Webob Request object |
269 | :param id: The opaque image identifier |
270 | |
271 | :raises HTTPNotFound if image is not available to user |
272 | @@ -192,31 +186,26 @@ |
273 | for chunk in chunks: |
274 | yield chunk |
275 | |
276 | - res = Response(app_iter=image_iterator(), |
277 | - content_type="application/octet-stream") |
278 | - # Using app_iter blanks content-length, so we set it here... |
279 | - res.headers.add('Content-Length', image['size']) |
280 | - utils.inject_image_meta_into_headers(res, image) |
281 | - res.headers.add('Location', "/v1/images/%s" % id) |
282 | - res.headers.add('ETag', image['checksum']) |
283 | - return req.get_response(res) |
284 | + return { |
285 | + 'image_iterator': image_iterator(), |
286 | + 'image_meta': image, |
287 | + } |
288 | |
289 | - def _reserve(self, req): |
290 | + def _reserve(self, req, image_meta): |
291 | """ |
292 | Adds the image metadata to the registry and assigns |
293 | an image identifier if one is not supplied in the request |
294 | - headers. Sets the image's status to `queued` |
295 | + headers. Sets the image's status to `queued`. |
296 | |
297 | - :param request: The WSGI/Webob Request object |
298 | + :param req: The WSGI/Webob Request object |
299 | :param id: The opaque image identifier |
300 | |
301 | :raises HTTPConflict if image already exists |
302 | :raises HTTPBadRequest if image metadata is not valid |
303 | """ |
304 | - image_meta = utils.get_image_meta_from_headers(req) |
305 | - |
306 | - if 'location' in image_meta: |
307 | - store = get_store_from_location(image_meta['location']) |
308 | + location = image_meta.get('location') |
309 | + if location: |
310 | + store = get_store_from_location(location) |
311 | # check the store exists before we hit the registry, but we |
312 | # don't actually care what it is at this point |
313 | self.get_store_or_400(req, store) |
314 | @@ -250,28 +239,27 @@ |
315 | will attempt to use that store, if not, Glance will use the |
316 | store set by the flag `default_store`. |
317 | |
318 | - :param request: The WSGI/Webob Request object |
319 | + :param req: The WSGI/Webob Request object |
320 | :param image_meta: Mapping of metadata about image |
321 | |
322 | :raises HTTPConflict if image already exists |
323 | :retval The location where the image was stored |
324 | """ |
325 | - image_id = image_meta['id'] |
326 | - content_type = req.headers.get('content-type', 'notset') |
327 | - if content_type != 'application/octet-stream': |
328 | - self._safe_kill(req, image_id) |
329 | - msg = ("Content-Type must be application/octet-stream") |
330 | + try: |
331 | + req.get_content_type('application/octet-stream') |
332 | + except exception.InvalidContentType: |
333 | + self._safe_kill(req, image_meta['id']) |
334 | + msg = "Content-Type must be application/octet-stream" |
335 | logger.error(msg) |
336 | - raise HTTPBadRequest(msg, content_type="text/plain", |
337 | - request=req) |
338 | + raise HTTPBadRequest(explanation=msg) |
339 | |
340 | - store_name = req.headers.get( |
341 | - 'x-image-meta-store', self.options['default_store']) |
342 | + store_name = req.headers.get('x-image-meta-store', |
343 | + self.options['default_store']) |
344 | |
345 | store = self.get_store_or_400(req, store_name) |
346 | |
347 | - logger.debug("Setting image %s to status 'saving'" |
348 | - % image_id) |
349 | + image_id = image_meta['id'] |
350 | + logger.debug("Setting image %s to status 'saving'" % image_id) |
351 | registry.update_image_metadata(self.options, image_id, |
352 | {'status': 'saving'}) |
353 | try: |
354 | @@ -304,11 +292,13 @@ |
355 | 'size': size}) |
356 | |
357 | return location |
358 | + |
359 | except exception.Duplicate, e: |
360 | msg = ("Attempt to upload duplicate image: %s") % str(e) |
361 | logger.error(msg) |
362 | self._safe_kill(req, image_id) |
363 | raise HTTPConflict(msg, request=req) |
364 | + |
365 | except Exception, e: |
366 | msg = ("Error uploading image: %s") % str(e) |
367 | logger.error(msg) |
368 | @@ -320,8 +310,8 @@ |
369 | Sets the image status to `active` and the image's location |
370 | attribute. |
371 | |
372 | - :param request: The WSGI/Webob Request object |
373 | - :param image_meta: Mapping of metadata about image |
374 | + :param req: The WSGI/Webob Request object |
375 | + :param image_id: Opaque image identifier |
376 | :param location: Location of where Glance stored this image |
377 | """ |
378 | image_meta = {} |
379 | @@ -333,9 +323,9 @@ |
380 | |
381 | def _kill(self, req, image_id): |
382 | """ |
383 | - Marks the image status to `killed` |
384 | + Marks the image status to `killed`. |
385 | |
386 | - :param request: The WSGI/Webob Request object |
387 | + :param req: The WSGI/Webob Request object |
388 | :param image_id: Opaque image identifier |
389 | """ |
390 | registry.update_image_metadata(self.options, |
391 | @@ -349,7 +339,7 @@ |
392 | Since _kill is meant to be called from exceptions handlers, it should |
393 | not raise itself, rather it should just log its error. |
394 | |
395 | - :param request: The WSGI/Webob Request object |
396 | + :param req: The WSGI/Webob Request object |
397 | :param image_id: Opaque image identifier |
398 | """ |
399 | try: |
400 | @@ -364,7 +354,7 @@ |
401 | and activates the image in the registry after a successful |
402 | upload. |
403 | |
404 | - :param request: The WSGI/Webob Request object |
405 | + :param req: The WSGI/Webob Request object |
406 | :param image_meta: Mapping of metadata about image |
407 | |
408 | :retval Mapping of updated image data |
409 | @@ -373,7 +363,7 @@ |
410 | location = self._upload(req, image_meta) |
411 | return self._activate(req, image_id, location) |
412 | |
413 | - def create(self, req): |
414 | + def create(self, req, image_meta, image_data): |
415 | """ |
416 | Adds a new image to Glance. Three scenarios exist when creating an |
417 | image: |
418 | @@ -399,32 +389,27 @@ |
419 | containing metadata about the image is returned, including its |
420 | opaque identifier. |
421 | |
422 | - :param request: The WSGI/Webob Request object |
423 | + :param req: The WSGI/Webob Request object |
424 | + :param image_meta: Mapping of metadata about image |
425 | + :param image_data: Actual image data that is to be stored |
426 | |
427 | - :raises HTTPBadRequest if no x-image-meta-location is missing |
428 | + :raises HTTPBadRequest if x-image-meta-location is missing |
429 | and the request body is not application/octet-stream |
430 | image data. |
431 | """ |
432 | - image_meta = self._reserve(req) |
433 | + image_meta = self._reserve(req, image_meta) |
434 | image_id = image_meta['id'] |
435 | |
436 | - if utils.has_body(req): |
437 | + if image_data is not None: |
438 | image_meta = self._upload_and_activate(req, image_meta) |
439 | else: |
440 | - if 'x-image-meta-location' in req.headers: |
441 | - location = req.headers['x-image-meta-location'] |
442 | + location = image_meta.get('location') |
443 | + if location: |
444 | image_meta = self._activate(req, image_id, location) |
445 | |
446 | - # APP states we should return a Location: header with the edit |
447 | - # URI of the resource newly-created. |
448 | - res = Response(request=req, body=json.dumps(dict(image=image_meta)), |
449 | - status=httplib.CREATED, content_type="text/plain") |
450 | - res.headers.add('Location', "/v1/images/%s" % image_id) |
451 | - res.headers.add('ETag', image_meta['checksum']) |
452 | - |
453 | - return req.get_response(res) |
454 | - |
455 | - def update(self, req, id): |
456 | + return {'image_meta': image_meta} |
457 | + |
458 | + def update(self, req, id, image_meta, image_data): |
459 | """ |
460 | Updates an existing image with the registry. |
461 | |
462 | @@ -433,29 +418,17 @@ |
463 | |
464 | :retval Returns the updated image information as a mapping |
465 | """ |
466 | - has_body = utils.has_body(req) |
467 | - |
468 | orig_image_meta = self.get_image_meta_or_404(req, id) |
469 | orig_status = orig_image_meta['status'] |
470 | |
471 | - if has_body and orig_status != 'queued': |
472 | + if image_data is not None and orig_status != 'queued': |
473 | raise HTTPConflict("Cannot upload to an unqueued image") |
474 | |
475 | - new_image_meta = utils.get_image_meta_from_headers(req) |
476 | try: |
477 | - image_meta = registry.update_image_metadata(self.options, |
478 | - id, |
479 | - new_image_meta, |
480 | - True) |
481 | - if has_body: |
482 | + image_meta = registry.update_image_metadata(self.options, id, |
483 | + image_meta, True) |
484 | + if image_data is not None: |
485 | image_meta = self._upload_and_activate(req, image_meta) |
486 | - |
487 | - res = Response(request=req, |
488 | - body=json.dumps(dict(image=image_meta)), |
489 | - content_type="text/plain") |
490 | - res.headers.add('Location', "/images/%s" % id) |
491 | - res.headers.add('ETag', image_meta['checksum']) |
492 | - return res |
493 | except exception.Invalid, e: |
494 | msg = ("Failed to update image metadata. Got error: %(e)s" |
495 | % locals()) |
496 | @@ -463,11 +436,13 @@ |
497 | logger.error(line) |
498 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") |
499 | |
500 | + return {'image_meta': image_meta} |
501 | + |
502 | def delete(self, req, id): |
503 | """ |
504 | Deletes the image and all its chunks from the Glance |
505 | |
506 | - :param request: The WSGI/Webob Request object |
507 | + :param req: The WSGI/Webob Request object |
508 | :param id: The opaque image identifier |
509 | |
510 | :raises HttpBadRequest if image registry is invalid |
511 | @@ -527,3 +502,97 @@ |
512 | logger.error(msg) |
513 | raise HTTPBadRequest(msg, request=request, |
514 | content_type='text/plain') |
515 | + |
516 | + |
517 | +class ImageDeserializer(wsgi.JSONRequestDeserializer): |
518 | + """Handles deserialization of specific controller method requests.""" |
519 | + |
520 | + def _deserialize(self, request): |
521 | + result = {} |
522 | + result['image_meta'] = utils.get_image_meta_from_headers(request) |
523 | + data = request.body_file if self.has_body(request) else None |
524 | + result['image_data'] = data |
525 | + return result |
526 | + |
527 | + def create(self, request): |
528 | + return self._deserialize(request) |
529 | + |
530 | + def update(self, request): |
531 | + return self._deserialize(request) |
532 | + |
533 | + |
534 | +class ImageSerializer(wsgi.JSONResponseSerializer): |
535 | + """Handles serialization of specific controller method responses.""" |
536 | + |
537 | + def _inject_location_header(self, response, image_meta): |
538 | + location = self._get_image_location(image_meta) |
539 | + response.headers['Location'] = location |
540 | + |
541 | + def _inject_checksum_header(self, response, image_meta): |
542 | + response.headers['ETag'] = image_meta['checksum'] |
543 | + |
544 | + def _inject_image_meta_headers(self, response, image_meta): |
545 | + """ |
546 | + Given a response and mapping of image metadata, injects |
547 | + the Response with a set of HTTP headers for the image |
548 | + metadata. Each main image metadata field is injected |
549 | + as a HTTP header with key 'x-image-meta-<FIELD>' except |
550 | + for the properties field, which is further broken out |
551 | + into a set of 'x-image-meta-property-<KEY>' headers |
552 | + |
553 | + :param response: The Webob Response object |
554 | + :param image_meta: Mapping of image metadata |
555 | + """ |
556 | + headers = utils.image_meta_to_http_headers(image_meta) |
557 | + |
558 | + for k, v in headers.items(): |
559 | + response.headers[k] = v |
560 | + |
561 | + def _get_image_location(self, image_meta): |
562 | + """Build a relative url to reach the image defined by image_meta.""" |
563 | + return "/v1/images/%s" % image_meta['id'] |
564 | + |
565 | + def meta(self, response, result): |
566 | + image_meta = result['image_meta'] |
567 | + self._inject_image_meta_headers(response, image_meta) |
568 | + self._inject_location_header(response, image_meta) |
569 | + self._inject_checksum_header(response, image_meta) |
570 | + return response |
571 | + |
572 | + def show(self, response, result): |
573 | + image_meta = result['image_meta'] |
574 | + |
575 | + response.app_iter = result['image_iterator'] |
576 | + # Using app_iter blanks content-length, so we set it here... |
577 | + response.headers['Content-Length'] = image_meta['size'] |
578 | + response.headers['Content-Type'] = 'application/octet-stream' |
579 | + |
580 | + self._inject_image_meta_headers(response, image_meta) |
581 | + self._inject_location_header(response, image_meta) |
582 | + self._inject_checksum_header(response, image_meta) |
583 | + |
584 | + return response |
585 | + |
586 | + def update(self, response, result): |
587 | + image_meta = result['image_meta'] |
588 | + response.body = self.to_json(dict(image=image_meta)) |
589 | + response.headers['Content-Type'] = 'application/json' |
590 | + self._inject_location_header(response, image_meta) |
591 | + self._inject_checksum_header(response, image_meta) |
592 | + return response |
593 | + |
594 | + def create(self, response, result): |
595 | + image_meta = result['image_meta'] |
596 | + response.status = httplib.CREATED |
597 | + response.headers['Content-Type'] = 'application/json' |
598 | + response.body = self.to_json(dict(image=image_meta)) |
599 | + self._inject_location_header(response, image_meta) |
600 | + self._inject_checksum_header(response, image_meta) |
601 | + return response |
602 | + |
603 | + |
604 | +def create_resource(options): |
605 | + """Images resource factory method""" |
606 | + deserializer = ImageDeserializer() |
607 | + serializer = ImageSerializer() |
608 | + return wsgi.Resource(Controller(options), deserializer, serializer) |
609 | |
610 | === modified file 'glance/client.py' |
611 | --- glance/client.py 2011-05-31 13:21:16 +0000 |
612 | +++ glance/client.py 2011-06-28 16:16:37 +0000 |
613 | @@ -19,172 +19,17 @@ |
614 | Client classes for callers of a Glance system |
615 | """ |
616 | |
617 | -import httplib |
618 | import json |
619 | -import logging |
620 | -import urlparse |
621 | -import socket |
622 | -import sys |
623 | -import urllib |
624 | |
625 | +from glance.api.v1 import images as v1_images |
626 | +from glance.common import client as base_client |
627 | +from glance.common import exception |
628 | from glance import utils |
629 | -from glance.common import exception |
630 | |
631 | #TODO(jaypipes) Allow a logger param for client classes |
632 | |
633 | |
634 | -class ClientConnectionError(Exception): |
635 | - """Error resulting from a client connecting to a server""" |
636 | - pass |
637 | - |
638 | - |
639 | -class ImageBodyIterator(object): |
640 | - |
641 | - """ |
642 | - A class that acts as an iterator over an image file's |
643 | - chunks of data. This is returned as part of the result |
644 | - tuple from `glance.client.Client.get_image` |
645 | - """ |
646 | - |
647 | - CHUNKSIZE = 65536 |
648 | - |
649 | - def __init__(self, response): |
650 | - """ |
651 | - Constructs the object from an HTTPResponse object |
652 | - """ |
653 | - self.response = response |
654 | - |
655 | - def __iter__(self): |
656 | - """ |
657 | - Exposes an iterator over the chunks of data in the |
658 | - image file. |
659 | - """ |
660 | - while True: |
661 | - chunk = self.response.read(ImageBodyIterator.CHUNKSIZE) |
662 | - if chunk: |
663 | - yield chunk |
664 | - else: |
665 | - break |
666 | - |
667 | - |
668 | -class BaseClient(object): |
669 | - |
670 | - """A base client class""" |
671 | - |
672 | - CHUNKSIZE = 65536 |
673 | - |
674 | - def __init__(self, host, port, use_ssl): |
675 | - """ |
676 | - Creates a new client to some service. |
677 | - |
678 | - :param host: The host where service resides |
679 | - :param port: The port where service resides |
680 | - :param use_ssl: Should we use HTTPS? |
681 | - """ |
682 | - self.host = host |
683 | - self.port = port |
684 | - self.use_ssl = use_ssl |
685 | - self.connection = None |
686 | - |
687 | - def get_connection_type(self): |
688 | - """ |
689 | - Returns the proper connection type |
690 | - """ |
691 | - if self.use_ssl: |
692 | - return httplib.HTTPSConnection |
693 | - else: |
694 | - return httplib.HTTPConnection |
695 | - |
696 | - def do_request(self, method, action, body=None, headers=None, |
697 | - params=None): |
698 | - """ |
699 | - Connects to the server and issues a request. Handles converting |
700 | - any returned HTTP error status codes to OpenStack/Glance exceptions |
701 | - and closing the server connection. Returns the result data, or |
702 | - raises an appropriate exception. |
703 | - |
704 | - :param method: HTTP method ("GET", "POST", "PUT", etc...) |
705 | - :param action: part of URL after root netloc |
706 | - :param body: string of data to send, or None (default) |
707 | - :param headers: mapping of key/value pairs to add as headers |
708 | - :param params: dictionary of key/value pairs to add to append |
709 | - to action |
710 | - |
711 | - :note |
712 | - |
713 | - If the body param has a read attribute, and method is either |
714 | - POST or PUT, this method will automatically conduct a chunked-transfer |
715 | - encoding and use the body as a file object, transferring chunks |
716 | - of data using the connection's send() method. This allows large |
717 | - objects to be transferred efficiently without buffering the entire |
718 | - body in memory. |
719 | - """ |
720 | - if type(params) is dict: |
721 | - action += '?' + urllib.urlencode(params) |
722 | - |
723 | - try: |
724 | - connection_type = self.get_connection_type() |
725 | - headers = headers or {} |
726 | - c = connection_type(self.host, self.port) |
727 | - |
728 | - # Do a simple request or a chunked request, depending |
729 | - # on whether the body param is a file-like object and |
730 | - # the method is PUT or POST |
731 | - if hasattr(body, 'read') and method.lower() in ('post', 'put'): |
732 | - # Chunk it, baby... |
733 | - c.putrequest(method, action) |
734 | - |
735 | - for header, value in headers.items(): |
736 | - c.putheader(header, value) |
737 | - c.putheader('Transfer-Encoding', 'chunked') |
738 | - c.endheaders() |
739 | - |
740 | - chunk = body.read(self.CHUNKSIZE) |
741 | - while chunk: |
742 | - c.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
743 | - chunk = body.read(self.CHUNKSIZE) |
744 | - c.send('0\r\n\r\n') |
745 | - else: |
746 | - # Simple request... |
747 | - c.request(method, action, body, headers) |
748 | - res = c.getresponse() |
749 | - status_code = self.get_status_code(res) |
750 | - if status_code in (httplib.OK, |
751 | - httplib.CREATED, |
752 | - httplib.ACCEPTED, |
753 | - httplib.NO_CONTENT): |
754 | - return res |
755 | - elif status_code == httplib.UNAUTHORIZED: |
756 | - raise exception.NotAuthorized |
757 | - elif status_code == httplib.FORBIDDEN: |
758 | - raise exception.NotAuthorized |
759 | - elif status_code == httplib.NOT_FOUND: |
760 | - raise exception.NotFound |
761 | - elif status_code == httplib.CONFLICT: |
762 | - raise exception.Duplicate(res.read()) |
763 | - elif status_code == httplib.BAD_REQUEST: |
764 | - raise exception.Invalid(res.read()) |
765 | - elif status_code == httplib.INTERNAL_SERVER_ERROR: |
766 | - raise Exception("Internal Server error: %s" % res.read()) |
767 | - else: |
768 | - raise Exception("Unknown error occurred! %s" % res.read()) |
769 | - |
770 | - except (socket.error, IOError), e: |
771 | - raise ClientConnectionError("Unable to connect to " |
772 | - "server. Got error: %s" % e) |
773 | - |
774 | - def get_status_code(self, response): |
775 | - """ |
776 | - Returns the integer status code from the response, which |
777 | - can be either a Webob.Response (used in testing) or httplib.Response |
778 | - """ |
779 | - if hasattr(response, 'status_int'): |
780 | - return response.status_int |
781 | - else: |
782 | - return response.status |
783 | - |
784 | - |
785 | -class V1Client(BaseClient): |
786 | +class V1Client(base_client.BaseClient): |
787 | |
788 | """Main client class for accessing Glance resources""" |
789 | |
790 | @@ -209,7 +54,7 @@ |
791 | return super(V1Client, self).do_request(method, action, body, |
792 | headers, params) |
793 | |
794 | - def get_images(self, filters=None, marker=None, limit=None): |
795 | + def get_images(self, **kwargs): |
796 | """ |
797 | Returns a list of image id/name mappings from Registry |
798 | |
799 | @@ -217,18 +62,15 @@ |
800 | collection of images should be filtered |
801 | :param marker: id after which to start the page of images |
802 | :param limit: maximum number of items to return |
803 | + :param sort_key: results will be ordered by this image attribute |
804 | + :param sort_dir: direction in which to to order results (asc, desc) |
805 | """ |
806 | - |
807 | - params = filters or {} |
808 | - if marker: |
809 | - params['marker'] = marker |
810 | - if limit: |
811 | - params['limit'] = limit |
812 | + params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS) |
813 | res = self.do_request("GET", "/images", params=params) |
814 | data = json.loads(res.read())['images'] |
815 | return data |
816 | |
817 | - def get_images_detailed(self, filters=None, marker=None, limit=None): |
818 | + def get_images_detailed(self, **kwargs): |
819 | """ |
820 | Returns a list of detailed image data mappings from Registry |
821 | |
822 | @@ -236,13 +78,11 @@ |
823 | collection of images should be filtered |
824 | :param marker: id after which to start the page of images |
825 | :param limit: maximum number of items to return |
826 | + :param sort_key: results will be ordered by this image attribute |
827 | + :param sort_dir: direction in which to to order results (asc, desc) |
828 | """ |
829 | |
830 | - params = filters or {} |
831 | - if marker: |
832 | - params['marker'] = marker |
833 | - if limit: |
834 | - params['limit'] = limit |
835 | + params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS) |
836 | res = self.do_request("GET", "/images/detail", params=params) |
837 | data = json.loads(res.read())['images'] |
838 | return data |
839 | @@ -260,7 +100,7 @@ |
840 | res = self.do_request("GET", "/images/%s" % image_id) |
841 | |
842 | image = utils.get_image_meta_from_headers(res) |
843 | - return image, ImageBodyIterator(res) |
844 | + return image, base_client.ImageBodyIterator(res) |
845 | |
846 | def get_image_meta(self, image_id): |
847 | """ |
848 | |
849 | === added file 'glance/common/client.py' |
850 | --- glance/common/client.py 1970-01-01 00:00:00 +0000 |
851 | +++ glance/common/client.py 2011-06-28 16:16:37 +0000 |
852 | @@ -0,0 +1,169 @@ |
853 | +import httplib |
854 | +import logging |
855 | +import socket |
856 | +import urllib |
857 | + |
858 | +from glance.common import exception |
859 | + |
860 | + |
861 | +class ImageBodyIterator(object): |
862 | + |
863 | + """ |
864 | + A class that acts as an iterator over an image file's |
865 | + chunks of data. This is returned as part of the result |
866 | + tuple from `glance.client.Client.get_image` |
867 | + """ |
868 | + |
869 | + CHUNKSIZE = 65536 |
870 | + |
871 | + def __init__(self, response): |
872 | + """ |
873 | + Constructs the object from an HTTPResponse object |
874 | + """ |
875 | + self.response = response |
876 | + |
877 | + def __iter__(self): |
878 | + """ |
879 | + Exposes an iterator over the chunks of data in the |
880 | + image file. |
881 | + """ |
882 | + while True: |
883 | + chunk = self.response.read(ImageBodyIterator.CHUNKSIZE) |
884 | + if chunk: |
885 | + yield chunk |
886 | + else: |
887 | + break |
888 | + |
889 | + |
890 | +class BaseClient(object): |
891 | + |
892 | + """A base client class""" |
893 | + |
894 | + CHUNKSIZE = 65536 |
895 | + |
896 | + def __init__(self, host, port, use_ssl): |
897 | + """ |
898 | + Creates a new client to some service. |
899 | + |
900 | + :param host: The host where service resides |
901 | + :param port: The port where service resides |
902 | + :param use_ssl: Should we use HTTPS? |
903 | + """ |
904 | + self.host = host |
905 | + self.port = port |
906 | + self.use_ssl = use_ssl |
907 | + self.connection = None |
908 | + |
909 | + def get_connection_type(self): |
910 | + """ |
911 | + Returns the proper connection type |
912 | + """ |
913 | + if self.use_ssl: |
914 | + return httplib.HTTPSConnection |
915 | + else: |
916 | + return httplib.HTTPConnection |
917 | + |
918 | + def do_request(self, method, action, body=None, headers=None, |
919 | + params=None): |
920 | + """ |
921 | + Connects to the server and issues a request. Handles converting |
922 | + any returned HTTP error status codes to OpenStack/Glance exceptions |
923 | + and closing the server connection. Returns the result data, or |
924 | + raises an appropriate exception. |
925 | + |
926 | + :param method: HTTP method ("GET", "POST", "PUT", etc...) |
927 | + :param action: part of URL after root netloc |
928 | + :param body: string of data to send, or None (default) |
929 | + :param headers: mapping of key/value pairs to add as headers |
930 | + :param params: dictionary of key/value pairs to add to append |
931 | + to action |
932 | + |
933 | + :note |
934 | + |
935 | + If the body param has a read attribute, and method is either |
936 | + POST or PUT, this method will automatically conduct a chunked-transfer |
937 | + encoding and use the body as a file object, transferring chunks |
938 | + of data using the connection's send() method. This allows large |
939 | + objects to be transferred efficiently without buffering the entire |
940 | + body in memory. |
941 | + """ |
942 | + if type(params) is dict: |
943 | + action += '?' + urllib.urlencode(params) |
944 | + |
945 | + try: |
946 | + connection_type = self.get_connection_type() |
947 | + headers = headers or {} |
948 | + c = connection_type(self.host, self.port) |
949 | + |
950 | + # Do a simple request or a chunked request, depending |
951 | + # on whether the body param is a file-like object and |
952 | + # the method is PUT or POST |
953 | + if hasattr(body, 'read') and method.lower() in ('post', 'put'): |
954 | + # Chunk it, baby... |
955 | + c.putrequest(method, action) |
956 | + |
957 | + for header, value in headers.items(): |
958 | + c.putheader(header, value) |
959 | + c.putheader('Transfer-Encoding', 'chunked') |
960 | + c.endheaders() |
961 | + |
962 | + chunk = body.read(self.CHUNKSIZE) |
963 | + while chunk: |
964 | + c.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
965 | + chunk = body.read(self.CHUNKSIZE) |
966 | + c.send('0\r\n\r\n') |
967 | + else: |
968 | + # Simple request... |
969 | + c.request(method, action, body, headers) |
970 | + res = c.getresponse() |
971 | + status_code = self.get_status_code(res) |
972 | + if status_code in (httplib.OK, |
973 | + httplib.CREATED, |
974 | + httplib.ACCEPTED, |
975 | + httplib.NO_CONTENT): |
976 | + return res |
977 | + elif status_code == httplib.UNAUTHORIZED: |
978 | + raise exception.NotAuthorized |
979 | + elif status_code == httplib.FORBIDDEN: |
980 | + raise exception.NotAuthorized |
981 | + elif status_code == httplib.NOT_FOUND: |
982 | + raise exception.NotFound |
983 | + elif status_code == httplib.CONFLICT: |
984 | + raise exception.Duplicate(res.read()) |
985 | + elif status_code == httplib.BAD_REQUEST: |
986 | + raise exception.Invalid(res.read()) |
987 | + elif status_code == httplib.INTERNAL_SERVER_ERROR: |
988 | + raise Exception("Internal Server error: %s" % res.read()) |
989 | + else: |
990 | + raise Exception("Unknown error occurred! %s" % res.read()) |
991 | + |
992 | + except (socket.error, IOError), e: |
993 | + raise exception.ClientConnectionError("Unable to connect to " |
994 | + "server. Got error: %s" % e) |
995 | + |
996 | + def get_status_code(self, response): |
997 | + """ |
998 | + Returns the integer status code from the response, which |
999 | + can be either a Webob.Response (used in testing) or httplib.Response |
1000 | + """ |
1001 | + if hasattr(response, 'status_int'): |
1002 | + return response.status_int |
1003 | + else: |
1004 | + return response.status |
1005 | + |
1006 | + def _extract_params(self, actual_params, allowed_params): |
1007 | + """ |
1008 | + Extract a subset of keys from a dictionary. The filters key |
1009 | + will also be extracted, and each of its values will be returned |
1010 | + as an individual param. |
1011 | + |
1012 | + :param actual_params: dict of keys to filter |
1013 | + :param allowed_params: list of keys that 'actual_params' will be |
1014 | + reduced to |
1015 | + :retval subset of 'params' dict |
1016 | + """ |
1017 | + result = actual_params.get('filters', {}) |
1018 | + for allowed_param in allowed_params: |
1019 | + if allowed_param in actual_params: |
1020 | + result[allowed_param] = actual_params[allowed_param] |
1021 | + return result |
1022 | |
1023 | === modified file 'glance/common/exception.py' |
1024 | --- glance/common/exception.py 2011-02-02 01:43:16 +0000 |
1025 | +++ glance/common/exception.py 2011-06-28 16:16:37 +0000 |
1026 | @@ -83,6 +83,11 @@ |
1027 | pass |
1028 | |
1029 | |
1030 | +class ClientConnectionError(Exception): |
1031 | + """Error resulting from a client connecting to a server""" |
1032 | + pass |
1033 | + |
1034 | + |
1035 | def wrap_exception(f): |
1036 | def _wrap(*args, **kw): |
1037 | try: |
1038 | @@ -96,3 +101,29 @@ |
1039 | raise |
1040 | _wrap.func_name = f.func_name |
1041 | return _wrap |
1042 | + |
1043 | + |
1044 | +class GlanceException(Exception): |
1045 | + """ |
1046 | + Base Glance Exception |
1047 | + |
1048 | + To correctly use this class, inherit from it and define |
1049 | + a 'message' property. That message will get printf'd |
1050 | + with the keyword arguments provided to the constructor. |
1051 | + """ |
1052 | + message = "An unknown exception occurred" |
1053 | + |
1054 | + def __init__(self, **kwargs): |
1055 | + try: |
1056 | + self._error_string = self.message % kwargs |
1057 | + |
1058 | + except Exception: |
1059 | + # at least get the core message out if something happened |
1060 | + self._error_string = self.message |
1061 | + |
1062 | + def __str__(self): |
1063 | + return self._error_string |
1064 | + |
1065 | + |
1066 | +class InvalidContentType(GlanceException): |
1067 | + message = "Invalid content type %(content_type)s" |
1068 | |
1069 | === modified file 'glance/common/wsgi.py' |
1070 | --- glance/common/wsgi.py 2011-05-18 00:11:46 +0000 |
1071 | +++ glance/common/wsgi.py 2011-06-28 16:16:37 +0000 |
1072 | @@ -34,6 +34,8 @@ |
1073 | import webob.dec |
1074 | import webob.exc |
1075 | |
1076 | +from glance.common import exception |
1077 | + |
1078 | |
1079 | class WritableLogger(object): |
1080 | """A thin wrapper that responds to `write` and logs.""" |
1081 | @@ -205,73 +207,55 @@ |
1082 | return app |
1083 | |
1084 | |
1085 | -class Controller(object): |
1086 | - """ |
1087 | - WSGI app that reads routing information supplied by RoutesMiddleware |
1088 | - and calls the requested action method upon itself. All action methods |
1089 | - must, in addition to their normal parameters, accept a 'req' argument |
1090 | - which is the incoming webob.Request. They raise a webob.exc exception, |
1091 | - or return a dict which will be serialized by requested content type. |
1092 | - """ |
1093 | - |
1094 | - @webob.dec.wsgify |
1095 | - def __call__(self, req): |
1096 | - """ |
1097 | - Call the method specified in req.environ by RoutesMiddleware. |
1098 | - """ |
1099 | - arg_dict = req.environ['wsgiorg.routing_args'][1] |
1100 | - action = arg_dict['action'] |
1101 | - method = getattr(self, action) |
1102 | - del arg_dict['controller'] |
1103 | - del arg_dict['action'] |
1104 | - arg_dict['req'] = req |
1105 | - result = method(**arg_dict) |
1106 | - if type(result) is dict: |
1107 | - return self._serialize(result, req) |
1108 | - else: |
1109 | - return result |
1110 | - |
1111 | - def _serialize(self, data, request): |
1112 | - """ |
1113 | - Serialize the given dict to the response type requested in request. |
1114 | - Uses self._serialization_metadata if it exists, which is a dict mapping |
1115 | - MIME types to information needed to serialize to that type. |
1116 | - """ |
1117 | - _metadata = getattr(type(self), "_serialization_metadata", {}) |
1118 | - serializer = Serializer(request.environ, _metadata) |
1119 | - return serializer.to_content_type(data) |
1120 | - |
1121 | - |
1122 | -class Serializer(object): |
1123 | - """ |
1124 | - Serializes a dictionary to a Content Type specified by a WSGI environment. |
1125 | - """ |
1126 | - |
1127 | - def __init__(self, environ, metadata=None): |
1128 | - """ |
1129 | - Create a serializer based on the given WSGI environment. |
1130 | - 'metadata' is an optional dict mapping MIME types to information |
1131 | - needed to serialize a dictionary to that type. |
1132 | - """ |
1133 | - self.environ = environ |
1134 | - self.metadata = metadata or {} |
1135 | - self._methods = { |
1136 | - 'application/json': self._to_json, |
1137 | - 'application/xml': self._to_xml} |
1138 | - |
1139 | - def to_content_type(self, data): |
1140 | - """ |
1141 | - Serialize a dictionary into a string. The format of the string |
1142 | - will be decided based on the Content Type requested in self.environ: |
1143 | - by Accept: header, or by URL suffix. |
1144 | - """ |
1145 | - # FIXME(sirp): for now, supporting json only |
1146 | - #mimetype = 'application/xml' |
1147 | - mimetype = 'application/json' |
1148 | - # TODO(gundlach): determine mimetype from request |
1149 | - return self._methods.get(mimetype, repr)(data) |
1150 | - |
1151 | - def _to_json(self, data): |
1152 | +class Request(webob.Request): |
1153 | + """Add some Openstack API-specific logic to the base webob.Request.""" |
1154 | + |
1155 | + def best_match_content_type(self): |
1156 | + """Determine the requested response content-type.""" |
1157 | + supported = ('application/json',) |
1158 | + bm = self.accept.best_match(supported) |
1159 | + return bm or 'application/json' |
1160 | + |
1161 | + def get_content_type(self, allowed_content_types): |
1162 | + """Determine content type of the request body.""" |
1163 | + if not "Content-Type" in self.headers: |
1164 | + raise exception.InvalidContentType(content_type=None) |
1165 | + |
1166 | + content_type = self.content_type |
1167 | + |
1168 | + if content_type not in allowed_content_types: |
1169 | + raise exception.InvalidContentType(content_type=content_type) |
1170 | + else: |
1171 | + return content_type |
1172 | + |
1173 | + |
1174 | +class JSONRequestDeserializer(object): |
1175 | + def has_body(self, request): |
1176 | + """ |
1177 | + Returns whether a Webob.Request object will possess an entity body. |
1178 | + |
1179 | + :param request: Webob.Request object |
1180 | + """ |
1181 | + if 'transfer-encoding' in request.headers: |
1182 | + return True |
1183 | + elif request.content_length > 0: |
1184 | + return True |
1185 | + |
1186 | + return False |
1187 | + |
1188 | + def from_json(self, datastring): |
1189 | + return json.loads(datastring) |
1190 | + |
1191 | + def default(self, request): |
1192 | + if self.has_body(request): |
1193 | + return {'body': self.from_json(request.body)} |
1194 | + else: |
1195 | + return {} |
1196 | + |
1197 | + |
1198 | +class JSONResponseSerializer(object): |
1199 | + |
1200 | + def to_json(self, data): |
1201 | def sanitizer(obj): |
1202 | if isinstance(obj, datetime.datetime): |
1203 | return obj.isoformat() |
1204 | @@ -279,37 +263,85 @@ |
1205 | |
1206 | return json.dumps(data, default=sanitizer) |
1207 | |
1208 | - def _to_xml(self, data): |
1209 | - metadata = self.metadata.get('application/xml', {}) |
1210 | - # We expect data to contain a single key which is the XML root. |
1211 | - root_key = data.keys()[0] |
1212 | - from xml.dom import minidom |
1213 | - doc = minidom.Document() |
1214 | - node = self._to_xml_node(doc, metadata, root_key, data[root_key]) |
1215 | - return node.toprettyxml(indent=' ') |
1216 | - |
1217 | - def _to_xml_node(self, doc, metadata, nodename, data): |
1218 | - """Recursive method to convert data members to XML nodes.""" |
1219 | - result = doc.createElement(nodename) |
1220 | - if type(data) is list: |
1221 | - singular = metadata.get('plurals', {}).get(nodename, None) |
1222 | - if singular is None: |
1223 | - if nodename.endswith('s'): |
1224 | - singular = nodename[:-1] |
1225 | - else: |
1226 | - singular = 'item' |
1227 | - for item in data: |
1228 | - node = self._to_xml_node(doc, metadata, singular, item) |
1229 | - result.appendChild(node) |
1230 | - elif type(data) is dict: |
1231 | - attrs = metadata.get('attributes', {}).get(nodename, {}) |
1232 | - for k, v in data.items(): |
1233 | - if k in attrs: |
1234 | - result.setAttribute(k, str(v)) |
1235 | - else: |
1236 | - node = self._to_xml_node(doc, metadata, k, v) |
1237 | - result.appendChild(node) |
1238 | - else: # atom |
1239 | - node = doc.createTextNode(str(data)) |
1240 | - result.appendChild(node) |
1241 | - return result |
1242 | + def default(self, response, result): |
1243 | + response.headers.add('Content-Type', 'application/json') |
1244 | + response.body = self.to_json(result) |
1245 | + |
1246 | + |
1247 | +class Resource(object): |
1248 | + """ |
1249 | + WSGI app that handles (de)serialization and controller dispatch. |
1250 | + |
1251 | + Reads routing information supplied by RoutesMiddleware and calls |
1252 | + the requested action method upon its deserializer, controller, |
1253 | + and serializer. Those three objects may implement any of the basic |
1254 | + controller action methods (create, update, show, index, delete) |
1255 | + along with any that may be specified in the api router. A 'default' |
1256 | + method may also be implemented to be used in place of any |
1257 | + non-implemented actions. Deserializer methods must accept a request |
1258 | + argument and return a dictionary. Controller methods must accept a |
1259 | + request argument. Additionally, they must also accept keyword |
1260 | + arguments that represent the keys returned by the Deserializer. They |
1261 | + may raise a webob.exc exception or return a dict, which will be |
1262 | + serialized by requested content type. |
1263 | + """ |
1264 | + def __init__(self, controller, deserializer, serializer): |
1265 | + """ |
1266 | + :param controller: object that implement methods created by routes lib |
1267 | + :param deserializer: object that supports webob request deserialization |
1268 | + through controller-like actions |
1269 | + :param serializer: object that supports webob response serialization |
1270 | + through controller-like actions |
1271 | + """ |
1272 | + self.controller = controller |
1273 | + self.serializer = serializer |
1274 | + self.deserializer = deserializer |
1275 | + |
1276 | + @webob.dec.wsgify(RequestClass=Request) |
1277 | + def __call__(self, request): |
1278 | + """WSGI method that controls (de)serialization and method dispatch.""" |
1279 | + action_args = self.get_action_args(request.environ) |
1280 | + action = action_args.pop('action', None) |
1281 | + |
1282 | + deserialized_request = self.dispatch(self.deserializer, |
1283 | + action, request) |
1284 | + action_args.update(deserialized_request) |
1285 | + |
1286 | + action_result = self.dispatch(self.controller, action, |
1287 | + request, **action_args) |
1288 | + try: |
1289 | + response = webob.Response() |
1290 | + self.dispatch(self.serializer, action, response, action_result) |
1291 | + return response |
1292 | + |
1293 | + # return unserializable result (typically a webob exc) |
1294 | + except Exception: |
1295 | + return action_result |
1296 | + |
1297 | + def dispatch(self, obj, action, *args, **kwargs): |
1298 | + """Find action-specific method on self and call it.""" |
1299 | + try: |
1300 | + method = getattr(obj, action) |
1301 | + except AttributeError: |
1302 | + method = getattr(obj, 'default') |
1303 | + |
1304 | + return method(*args, **kwargs) |
1305 | + |
1306 | + def get_action_args(self, request_environment): |
1307 | + """Parse dictionary created by routes library.""" |
1308 | + try: |
1309 | + args = request_environment['wsgiorg.routing_args'][1].copy() |
1310 | + except Exception: |
1311 | + return {} |
1312 | + |
1313 | + try: |
1314 | + del args['controller'] |
1315 | + except KeyError: |
1316 | + pass |
1317 | + |
1318 | + try: |
1319 | + del args['format'] |
1320 | + except KeyError: |
1321 | + pass |
1322 | + |
1323 | + return args |
1324 | |
1325 | === modified file 'glance/registry/client.py' |
1326 | --- glance/registry/client.py 2011-05-27 19:48:51 +0000 |
1327 | +++ glance/registry/client.py 2011-06-28 16:16:37 +0000 |
1328 | @@ -23,7 +23,8 @@ |
1329 | import json |
1330 | import urllib |
1331 | |
1332 | -from glance.client import BaseClient |
1333 | +from glance.common.client import BaseClient |
1334 | +from glance.registry import server |
1335 | |
1336 | |
1337 | class RegistryClient(BaseClient): |
1338 | @@ -44,42 +45,32 @@ |
1339 | port = port or self.DEFAULT_PORT |
1340 | super(RegistryClient, self).__init__(host, port, use_ssl) |
1341 | |
1342 | - def get_images(self, filters=None, marker=None, limit=None): |
1343 | + def get_images(self, **kwargs): |
1344 | """ |
1345 | Returns a list of image id/name mappings from Registry |
1346 | |
1347 | :param filters: dict of keys & expected values to filter results |
1348 | :param marker: image id after which to start page |
1349 | :param limit: max number of images to return |
1350 | + :param sort_key: results will be ordered by this image attribute |
1351 | + :param sort_dir: direction in which to to order results (asc, desc) |
1352 | """ |
1353 | - params = filters or {} |
1354 | - |
1355 | - if marker != None: |
1356 | - params['marker'] = marker |
1357 | - |
1358 | - if limit != None: |
1359 | - params['limit'] = limit |
1360 | - |
1361 | + params = self._extract_params(kwargs, server.SUPPORTED_PARAMS) |
1362 | res = self.do_request("GET", "/images", params=params) |
1363 | data = json.loads(res.read())['images'] |
1364 | return data |
1365 | |
1366 | - def get_images_detailed(self, filters=None, marker=None, limit=None): |
1367 | + def get_images_detailed(self, **kwargs): |
1368 | """ |
1369 | Returns a list of detailed image data mappings from Registry |
1370 | |
1371 | :param filters: dict of keys & expected values to filter results |
1372 | :param marker: image id after which to start page |
1373 | :param limit: max number of images to return |
1374 | + :param sort_key: results will be ordered by this image attribute |
1375 | + :param sort_dir: direction in which to to order results (asc, desc) |
1376 | """ |
1377 | - params = filters or {} |
1378 | - |
1379 | - if marker != None: |
1380 | - params['marker'] = marker |
1381 | - |
1382 | - if limit != None: |
1383 | - params['limit'] = limit |
1384 | - |
1385 | + params = self._extract_params(kwargs, server.SUPPORTED_PARAMS) |
1386 | res = self.do_request("GET", "/images/detail", params=params) |
1387 | data = json.loads(res.read())['images'] |
1388 | return data |
1389 | @@ -94,10 +85,16 @@ |
1390 | """ |
1391 | Tells registry about an image's metadata |
1392 | """ |
1393 | + headers = { |
1394 | + 'Content-Type': 'application/json', |
1395 | + } |
1396 | + |
1397 | if 'image' not in image_metadata.keys(): |
1398 | image_metadata = dict(image=image_metadata) |
1399 | + |
1400 | body = json.dumps(image_metadata) |
1401 | - res = self.do_request("POST", "/images", body) |
1402 | + |
1403 | + res = self.do_request("POST", "/images", body, headers=headers) |
1404 | # Registry returns a JSONified dict(image=image_info) |
1405 | data = json.loads(res.read()) |
1406 | return data['image'] |
1407 | @@ -111,9 +108,13 @@ |
1408 | |
1409 | body = json.dumps(image_metadata) |
1410 | |
1411 | - headers = {} |
1412 | + headers = { |
1413 | + 'Content-Type': 'application/json', |
1414 | + } |
1415 | + |
1416 | if purge_props: |
1417 | headers["X-Glance-Registry-Purge-Props"] = "true" |
1418 | + |
1419 | res = self.do_request("PUT", "/images/%s" % image_id, body, headers) |
1420 | data = json.loads(res.read()) |
1421 | image = data['image'] |
1422 | |
1423 | === modified file 'glance/registry/db/api.py' |
1424 | --- glance/registry/db/api.py 2011-05-31 19:32:17 +0000 |
1425 | +++ glance/registry/db/api.py 2011-06-28 16:16:37 +0000 |
1426 | @@ -23,7 +23,7 @@ |
1427 | |
1428 | import logging |
1429 | |
1430 | -from sqlalchemy import create_engine, desc |
1431 | +from sqlalchemy import asc, create_engine, desc |
1432 | from sqlalchemy.ext.declarative import declarative_base |
1433 | from sqlalchemy.orm import exc |
1434 | from sqlalchemy.orm import joinedload |
1435 | @@ -129,7 +129,8 @@ |
1436 | raise exception.NotFound("No image found with ID %s" % image_id) |
1437 | |
1438 | |
1439 | -def image_get_all_public(context, filters=None, marker=None, limit=None): |
1440 | +def image_get_all_public(context, filters=None, marker=None, limit=None, |
1441 | + sort_key='created_at', sort_dir='desc'): |
1442 | """Get all public images that match zero or more filters. |
1443 | |
1444 | :param filters: dict of filter keys and values. If a 'properties' |
1445 | @@ -137,7 +138,8 @@ |
1446 | filters on the image properties attribute |
1447 | :param marker: image id after which to start page |
1448 | :param limit: maximum number of images to return |
1449 | - |
1450 | + :param sort_key: image attribute by which results should be sorted |
1451 | + :param sort_dir: direction in which results should be sorted (asc, desc) |
1452 | """ |
1453 | filters = filters or {} |
1454 | |
1455 | @@ -146,9 +148,17 @@ |
1456 | options(joinedload(models.Image.properties)).\ |
1457 | filter_by(deleted=_deleted(context)).\ |
1458 | filter_by(is_public=True).\ |
1459 | - filter(models.Image.status != 'killed').\ |
1460 | - order_by(desc(models.Image.created_at)).\ |
1461 | - order_by(desc(models.Image.id)) |
1462 | + filter(models.Image.status != 'killed') |
1463 | + |
1464 | + sort_dir_func = { |
1465 | + 'asc': asc, |
1466 | + 'desc': desc, |
1467 | + }[sort_dir] |
1468 | + |
1469 | + sort_key_attr = getattr(models.Image, sort_key) |
1470 | + |
1471 | + query = query.order_by(sort_dir_func(sort_key_attr)).\ |
1472 | + order_by(sort_dir_func(models.Image.id)) |
1473 | |
1474 | if 'size_min' in filters: |
1475 | query = query.filter(models.Image.size >= filters['size_min']) |
1476 | |
1477 | === modified file 'glance/registry/server.py' |
1478 | --- glance/registry/server.py 2011-05-18 00:19:44 +0000 |
1479 | +++ glance/registry/server.py 2011-06-28 16:16:37 +0000 |
1480 | @@ -39,10 +39,17 @@ |
1481 | SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', |
1482 | 'size_min', 'size_max'] |
1483 | |
1484 | +SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format', |
1485 | + 'size', 'id', 'created_at', 'updated_at') |
1486 | + |
1487 | +SUPPORTED_SORT_DIRS = ('asc', 'desc') |
1488 | + |
1489 | MAX_ITEM_LIMIT = 25 |
1490 | |
1491 | - |
1492 | -class Controller(wsgi.Controller): |
1493 | +SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') |
1494 | + |
1495 | + |
1496 | +class Controller(object): |
1497 | """Controller for the reference implementation registry server""" |
1498 | |
1499 | def __init__(self, options): |
1500 | @@ -69,14 +76,7 @@ |
1501 | } |
1502 | |
1503 | """ |
1504 | - params = { |
1505 | - 'filters': self._get_filters(req), |
1506 | - 'limit': self._get_limit(req), |
1507 | - } |
1508 | - |
1509 | - if 'marker' in req.str_params: |
1510 | - params['marker'] = self._get_marker(req) |
1511 | - |
1512 | + params = self._get_query_params(req) |
1513 | images = db_api.image_get_all_public(None, **params) |
1514 | |
1515 | results = [] |
1516 | @@ -99,19 +99,33 @@ |
1517 | all image model fields. |
1518 | |
1519 | """ |
1520 | - params = { |
1521 | - 'filters': self._get_filters(req), |
1522 | - 'limit': self._get_limit(req), |
1523 | - } |
1524 | - |
1525 | - if 'marker' in req.str_params: |
1526 | - params['marker'] = self._get_marker(req) |
1527 | - |
1528 | + params = self._get_query_params(req) |
1529 | images = db_api.image_get_all_public(None, **params) |
1530 | |
1531 | image_dicts = [make_image_dict(i) for i in images] |
1532 | return dict(images=image_dicts) |
1533 | |
1534 | + def _get_query_params(self, req): |
1535 | + """ |
1536 | + Extract necessary query parameters from http request. |
1537 | + |
1538 | + :param req: the Request object coming from the wsgi layer |
1539 | + :retval dictionary of filters to apply to list of images |
1540 | + """ |
1541 | + params = { |
1542 | + 'filters': self._get_filters(req), |
1543 | + 'limit': self._get_limit(req), |
1544 | + 'sort_key': self._get_sort_key(req), |
1545 | + 'sort_dir': self._get_sort_dir(req), |
1546 | + 'marker': self._get_marker(req), |
1547 | + } |
1548 | + |
1549 | + for key, value in params.items(): |
1550 | + if value is None: |
1551 | + del params[key] |
1552 | + |
1553 | + return params |
1554 | + |
1555 | def _get_filters(self, req): |
1556 | """Return a dictionary of query param filters from the request |
1557 | |
1558 | @@ -148,12 +162,35 @@ |
1559 | |
1560 | def _get_marker(self, req): |
1561 | """Parse a marker query param into something usable.""" |
1562 | + marker = req.str_params.get('marker', None) |
1563 | + |
1564 | + if marker is None: |
1565 | + return None |
1566 | + |
1567 | try: |
1568 | - marker = int(req.str_params.get('marker', None)) |
1569 | + marker = int(marker) |
1570 | except ValueError: |
1571 | raise exc.HTTPBadRequest("marker param must be an integer") |
1572 | return marker |
1573 | |
1574 | + def _get_sort_key(self, req): |
1575 | + """Parse a sort key query param from the request object.""" |
1576 | + sort_key = req.str_params.get('sort_key', None) |
1577 | + if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS: |
1578 | + _keys = ', '.join(SUPPORTED_SORT_KEYS) |
1579 | + msg = "Unsupported sort_key. Acceptable values: %s" % (_keys,) |
1580 | + raise exc.HTTPBadRequest(explanation=msg) |
1581 | + return sort_key |
1582 | + |
1583 | + def _get_sort_dir(self, req): |
1584 | + """Parse a sort direction query param from the request object.""" |
1585 | + sort_dir = req.str_params.get('sort_dir', None) |
1586 | + if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS: |
1587 | + _keys = ', '.join(SUPPORTED_SORT_DIRS) |
1588 | + msg = "Unsupported sort_dir. Acceptable values: %s" % (_keys,) |
1589 | + raise exc.HTTPBadRequest(explanation=msg) |
1590 | + return sort_dir |
1591 | + |
1592 | def show(self, req, id): |
1593 | """Return data about the given image id.""" |
1594 | try: |
1595 | @@ -167,7 +204,7 @@ |
1596 | """ |
1597 | Deletes an existing image with the registry. |
1598 | |
1599 | - :param req: Request body. Ignored. |
1600 | + :param req: wsgi Request object |
1601 | :param id: The opaque internal identifier for the image |
1602 | |
1603 | :retval Returns 200 if delete was successful, a fault if not. |
1604 | @@ -179,19 +216,19 @@ |
1605 | except exception.NotFound: |
1606 | return exc.HTTPNotFound() |
1607 | |
1608 | - def create(self, req): |
1609 | + def create(self, req, body): |
1610 | """ |
1611 | Registers a new image with the registry. |
1612 | |
1613 | - :param req: Request body. A JSON-ified dict of information about |
1614 | - the image. |
1615 | + :param req: wsgi Request object |
1616 | + :param body: Dictionary of information about the image |
1617 | |
1618 | :retval Returns the newly-created image information as a mapping, |
1619 | which will include the newly-created image's internal id |
1620 | in the 'id' field |
1621 | |
1622 | """ |
1623 | - image_data = json.loads(req.body)['image'] |
1624 | + image_data = body['image'] |
1625 | |
1626 | # Ensure the image has a status set |
1627 | image_data.setdefault('status', 'active') |
1628 | @@ -209,18 +246,17 @@ |
1629 | logger.error(msg) |
1630 | return exc.HTTPBadRequest(msg) |
1631 | |
1632 | - def update(self, req, id): |
1633 | + def update(self, req, id, body): |
1634 | """Updates an existing image with the registry. |
1635 | |
1636 | - :param req: Request body. A JSON-ified dict of information about |
1637 | - the image. This will replace the information in the |
1638 | - registry about this image |
1639 | + :param req: wsgi Request object |
1640 | + :param body: Dictionary of information about the image |
1641 | :param id: The opaque internal identifier for the image |
1642 | |
1643 | :retval Returns the updated image information as a mapping, |
1644 | |
1645 | """ |
1646 | - image_data = json.loads(req.body)['image'] |
1647 | + image_data = body['image'] |
1648 | |
1649 | purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false") |
1650 | context = None |
1651 | @@ -244,15 +280,22 @@ |
1652 | content_type='text/plain') |
1653 | |
1654 | |
1655 | +def create_resource(options): |
1656 | + """Images resource factory method.""" |
1657 | + deserializer = wsgi.JSONRequestDeserializer() |
1658 | + serializer = wsgi.JSONResponseSerializer() |
1659 | + return wsgi.Resource(Controller(options), deserializer, serializer) |
1660 | + |
1661 | + |
1662 | class API(wsgi.Router): |
1663 | """WSGI entry point for all Registry requests.""" |
1664 | |
1665 | def __init__(self, options): |
1666 | mapper = routes.Mapper() |
1667 | - controller = Controller(options) |
1668 | - mapper.resource("image", "images", controller=controller, |
1669 | + resource = create_resource(options) |
1670 | + mapper.resource("image", "images", controller=resource, |
1671 | collection={'detail': 'GET'}) |
1672 | - mapper.connect("/", controller=controller, action="index") |
1673 | + mapper.connect("/", controller=resource, action="index") |
1674 | super(API, self).__init__(mapper) |
1675 | |
1676 | |
1677 | |
1678 | === modified file 'glance/utils.py' |
1679 | --- glance/utils.py 2011-04-12 21:45:16 +0000 |
1680 | +++ glance/utils.py 2011-06-28 16:16:37 +0000 |
1681 | @@ -43,24 +43,6 @@ |
1682 | return headers |
1683 | |
1684 | |
1685 | -def inject_image_meta_into_headers(response, image_meta): |
1686 | - """ |
1687 | - Given a response and mapping of image metadata, injects |
1688 | - the Response with a set of HTTP headers for the image |
1689 | - metadata. Each main image metadata field is injected |
1690 | - as a HTTP header with key 'x-image-meta-<FIELD>' except |
1691 | - for the properties field, which is further broken out |
1692 | - into a set of 'x-image-meta-property-<KEY>' headers |
1693 | - |
1694 | - :param response: The Webob Response object |
1695 | - :param image_meta: Mapping of image metadata |
1696 | - """ |
1697 | - headers = image_meta_to_http_headers(image_meta) |
1698 | - |
1699 | - for k, v in headers.items(): |
1700 | - response.headers.add(k, v) |
1701 | - |
1702 | - |
1703 | def get_image_meta_from_headers(response): |
1704 | """ |
1705 | Processes HTTP headers from a supplied response that |
1706 | |
1707 | === modified file 'run_tests.py' |
1708 | --- run_tests.py 2011-03-16 19:27:26 +0000 |
1709 | +++ run_tests.py 2011-06-28 16:16:37 +0000 |
1710 | @@ -51,6 +51,7 @@ |
1711 | """ |
1712 | |
1713 | import gettext |
1714 | +import logging |
1715 | import os |
1716 | import unittest |
1717 | import sys |
1718 | @@ -269,6 +270,13 @@ |
1719 | |
1720 | |
1721 | if __name__ == '__main__': |
1722 | + logger = logging.getLogger() |
1723 | + hdlr = logging.StreamHandler() |
1724 | + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') |
1725 | + hdlr.setFormatter(formatter) |
1726 | + logger.addHandler(hdlr) |
1727 | + logger.setLevel(logging.DEBUG) |
1728 | + |
1729 | c = config.Config(stream=sys.stdout, |
1730 | env=os.environ, |
1731 | verbosity=3) |
1732 | |
1733 | === modified file 'tests/functional/test_curl_api.py' |
1734 | --- tests/functional/test_curl_api.py 2011-05-27 19:48:51 +0000 |
1735 | +++ tests/functional/test_curl_api.py 2011-06-28 16:16:37 +0000 |
1736 | @@ -773,7 +773,7 @@ |
1737 | with tempfile.NamedTemporaryFile() as test_data_file: |
1738 | test_data_file.write("XXX") |
1739 | test_data_file.flush() |
1740 | - cmd = ("curl -i -X POST --upload-file %s " |
1741 | + cmd = ("curl -i -X POST --upload-file %s -H 'Expect: ' " |
1742 | "http://0.0.0.0:%d/v1/images") % (test_data_file.name, |
1743 | api_port) |
1744 | |
1745 | @@ -1117,3 +1117,118 @@ |
1746 | self.assertEqual(int(images[0]['id']), 2) |
1747 | |
1748 | self.stop_servers() |
1749 | + |
1750 | + def test_ordered_images(self): |
1751 | + """ |
1752 | + Set up three test images and ensure each query param filter works |
1753 | + """ |
1754 | + self.cleanup() |
1755 | + self.start_servers() |
1756 | + |
1757 | + api_port = self.api_port |
1758 | + registry_port = self.registry_port |
1759 | + |
1760 | + # 0. GET /images |
1761 | + # Verify no public images |
1762 | + cmd = "curl http://0.0.0.0:%d/v1/images" % api_port |
1763 | + |
1764 | + exitcode, out, err = execute(cmd) |
1765 | + |
1766 | + self.assertEqual(0, exitcode) |
1767 | + self.assertEqual('{"images": []}', out.strip()) |
1768 | + |
1769 | + # 1. POST /images with three public images with various attributes |
1770 | + cmd = ("curl -i -X POST " |
1771 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
1772 | + "-H 'X-Image-Meta-Name: Image1' " |
1773 | + "-H 'X-Image-Meta-Status: active' " |
1774 | + "-H 'X-Image-Meta-Container-Format: ovf' " |
1775 | + "-H 'X-Image-Meta-Disk-Format: vdi' " |
1776 | + "-H 'X-Image-Meta-Size: 19' " |
1777 | + "-H 'X-Image-Meta-Is-Public: True' " |
1778 | + "http://0.0.0.0:%d/v1/images") % api_port |
1779 | + |
1780 | + exitcode, out, err = execute(cmd) |
1781 | + self.assertEqual(0, exitcode) |
1782 | + |
1783 | + lines = out.split("\r\n") |
1784 | + status_line = lines[0] |
1785 | + |
1786 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
1787 | + |
1788 | + cmd = ("curl -i -X POST " |
1789 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
1790 | + "-H 'X-Image-Meta-Name: ASDF' " |
1791 | + "-H 'X-Image-Meta-Status: active' " |
1792 | + "-H 'X-Image-Meta-Container-Format: bare' " |
1793 | + "-H 'X-Image-Meta-Disk-Format: iso' " |
1794 | + "-H 'X-Image-Meta-Size: 2' " |
1795 | + "-H 'X-Image-Meta-Is-Public: True' " |
1796 | + "http://0.0.0.0:%d/v1/images") % api_port |
1797 | + |
1798 | + exitcode, out, err = execute(cmd) |
1799 | + self.assertEqual(0, exitcode) |
1800 | + |
1801 | + lines = out.split("\r\n") |
1802 | + status_line = lines[0] |
1803 | + |
1804 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
1805 | + cmd = ("curl -i -X POST " |
1806 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
1807 | + "-H 'X-Image-Meta-Name: XYZ' " |
1808 | + "-H 'X-Image-Meta-Status: saving' " |
1809 | + "-H 'X-Image-Meta-Container-Format: ami' " |
1810 | + "-H 'X-Image-Meta-Disk-Format: ami' " |
1811 | + "-H 'X-Image-Meta-Size: 5' " |
1812 | + "-H 'X-Image-Meta-Is-Public: True' " |
1813 | + "http://0.0.0.0:%d/v1/images") % api_port |
1814 | + |
1815 | + exitcode, out, err = execute(cmd) |
1816 | + self.assertEqual(0, exitcode) |
1817 | + |
1818 | + lines = out.split("\r\n") |
1819 | + status_line = lines[0] |
1820 | + |
1821 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
1822 | + |
1823 | + # 2. GET /images with no query params |
1824 | + # Verify three public images sorted by created_at desc |
1825 | + cmd = "curl http://0.0.0.0:%d/v1/images" % api_port |
1826 | + |
1827 | + exitcode, out, err = execute(cmd) |
1828 | + |
1829 | + self.assertEqual(0, exitcode) |
1830 | + images = json.loads(out.strip())['images'] |
1831 | + |
1832 | + self.assertEqual(len(images), 3) |
1833 | + self.assertEqual(images[0]['id'], 3) |
1834 | + self.assertEqual(images[1]['id'], 2) |
1835 | + self.assertEqual(images[2]['id'], 1) |
1836 | + |
1837 | + # 3. GET /images sorted by name asc |
1838 | + params = 'sort_key=name&sort_dir=asc' |
1839 | + cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params) |
1840 | + |
1841 | + exitcode, out, err = execute(cmd) |
1842 | + |
1843 | + self.assertEqual(0, exitcode) |
1844 | + images = json.loads(out.strip())['images'] |
1845 | + |
1846 | + self.assertEqual(len(images), 3) |
1847 | + self.assertEqual(images[0]['id'], 2) |
1848 | + self.assertEqual(images[1]['id'], 1) |
1849 | + self.assertEqual(images[2]['id'], 3) |
1850 | + |
1851 | + # 4. GET /images sorted by size desc |
1852 | + params = 'sort_key=size&sort_dir=desc' |
1853 | + cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params) |
1854 | + |
1855 | + exitcode, out, err = execute(cmd) |
1856 | + |
1857 | + self.assertEqual(0, exitcode) |
1858 | + images = json.loads(out.strip())['images'] |
1859 | + |
1860 | + self.assertEqual(len(images), 3) |
1861 | + self.assertEqual(images[0]['id'], 1) |
1862 | + self.assertEqual(images[1]['id'], 3) |
1863 | + self.assertEqual(images[2]['id'], 2) |
1864 | |
1865 | === added file 'tests/functional/test_httplib2_api.py' |
1866 | --- tests/functional/test_httplib2_api.py 1970-01-01 00:00:00 +0000 |
1867 | +++ tests/functional/test_httplib2_api.py 2011-06-28 16:16:37 +0000 |
1868 | @@ -0,0 +1,274 @@ |
1869 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1870 | + |
1871 | +# Copyright 2011 OpenStack, LLC |
1872 | +# All Rights Reserved. |
1873 | +# |
1874 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1875 | +# not use this file except in compliance with the License. You may obtain |
1876 | +# a copy of the License at |
1877 | +# |
1878 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1879 | +# |
1880 | +# Unless required by applicable law or agreed to in writing, software |
1881 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1882 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1883 | +# License for the specific language governing permissions and limitations |
1884 | +# under the License. |
1885 | + |
1886 | +"""Functional test case that utilizes httplib2 against the API server""" |
1887 | + |
1888 | +import hashlib |
1889 | +import httplib2 |
1890 | +import json |
1891 | +import os |
1892 | + |
1893 | +from tests import functional |
1894 | +from tests.utils import execute |
1895 | + |
1896 | +FIVE_KB = 5 * 1024 |
1897 | +FIVE_GB = 5 * 1024 * 1024 * 1024 |
1898 | + |
1899 | + |
1900 | +class TestApiHttplib2(functional.FunctionalTest): |
1901 | + |
1902 | + """Functional tests using httplib2 against the API server""" |
1903 | + |
1904 | + def test_001_get_head_simple_post(self): |
1905 | + """ |
1906 | + We test the following sequential series of actions: |
1907 | + |
1908 | + 0. GET /images |
1909 | + - Verify no public images |
1910 | + 1. GET /images/detail |
1911 | + - Verify no public images |
1912 | + 2. HEAD /images/1 |
1913 | + - Verify 404 returned |
1914 | + 3. POST /images with public image named Image1 with a location |
1915 | + attribute and no custom properties |
1916 | + - Verify 201 returned |
1917 | + 4. HEAD /images/1 |
1918 | + - Verify HTTP headers have correct information we just added |
1919 | + 5. GET /images/1 |
1920 | + - Verify all information on image we just added is correct |
1921 | + 6. GET /images |
1922 | + - Verify the image we just added is returned |
1923 | + 7. GET /images/detail |
1924 | + - Verify the image we just added is returned |
1925 | + 8. PUT /images/1 with custom properties of "distro" and "arch" |
1926 | + - Verify 200 returned |
1927 | + 9. GET /images/1 |
1928 | + - Verify updated information about image was stored |
1929 | + 10. PUT /images/1 |
1930 | + - Remove a previously existing property. |
1931 | + 11. PUT /images/1 |
1932 | + - Add a previously deleted property. |
1933 | + """ |
1934 | + |
1935 | + self.cleanup() |
1936 | + self.start_servers() |
1937 | + |
1938 | + # 0. GET /images |
1939 | + # Verify no public images |
1940 | + path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port) |
1941 | + http = httplib2.Http() |
1942 | + response, content = http.request(path, 'GET') |
1943 | + self.assertEqual(response.status, 200) |
1944 | + self.assertEqual(content, '{"images": []}') |
1945 | + |
1946 | + # 1. GET /images/detail |
1947 | + # Verify no public images |
1948 | + path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port) |
1949 | + http = httplib2.Http() |
1950 | + response, content = http.request(path, 'GET') |
1951 | + self.assertEqual(response.status, 200) |
1952 | + self.assertEqual(content, '{"images": []}') |
1953 | + |
1954 | + # 2. HEAD /images/1 |
1955 | + # Verify 404 returned |
1956 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
1957 | + http = httplib2.Http() |
1958 | + response, content = http.request(path, 'HEAD') |
1959 | + self.assertEqual(response.status, 404) |
1960 | + |
1961 | + # 3. POST /images with public image named Image1 |
1962 | + # attribute and no custom properties. Verify a 200 OK is returned |
1963 | + image_data = "*" * FIVE_KB |
1964 | + headers = {'Content-Type': 'application/octet-stream', |
1965 | + 'X-Image-Meta-Name': 'Image1', |
1966 | + 'X-Image-Meta-Is-Public': 'True'} |
1967 | + path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port) |
1968 | + http = httplib2.Http() |
1969 | + response, content = http.request(path, 'POST', headers=headers, |
1970 | + body=image_data) |
1971 | + self.assertEqual(response.status, 201) |
1972 | + data = json.loads(content) |
1973 | + self.assertEqual(data['image']['checksum'], |
1974 | + hashlib.md5(image_data).hexdigest()) |
1975 | + self.assertEqual(data['image']['size'], FIVE_KB) |
1976 | + self.assertEqual(data['image']['name'], "Image1") |
1977 | + self.assertEqual(data['image']['is_public'], True) |
1978 | + |
1979 | + # 4. HEAD /images/1 |
1980 | + # Verify image found now |
1981 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
1982 | + http = httplib2.Http() |
1983 | + response, content = http.request(path, 'HEAD') |
1984 | + self.assertEqual(response.status, 200) |
1985 | + self.assertEqual(response['x-image-meta-name'], "Image1") |
1986 | + |
1987 | + # 5. GET /images/1 |
1988 | + # Verify all information on image we just added is correct |
1989 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
1990 | + http = httplib2.Http() |
1991 | + response, content = http.request(path, 'GET') |
1992 | + self.assertEqual(response.status, 200) |
1993 | + |
1994 | + expected_image_headers = { |
1995 | + 'x-image-meta-id': '1', |
1996 | + 'x-image-meta-name': 'Image1', |
1997 | + 'x-image-meta-is_public': 'True', |
1998 | + 'x-image-meta-status': 'active', |
1999 | + 'x-image-meta-disk_format': '', |
2000 | + 'x-image-meta-container_format': '', |
2001 | + 'x-image-meta-size': str(FIVE_KB), |
2002 | + 'x-image-meta-location': 'file://%s/1' % self.api_server.image_dir} |
2003 | + |
2004 | + expected_std_headers = { |
2005 | + 'content-length': str(FIVE_KB), |
2006 | + 'content-type': 'application/octet-stream'} |
2007 | + |
2008 | + for expected_key, expected_value in expected_image_headers.items(): |
2009 | + self.assertEqual(response[expected_key], expected_value, |
2010 | + "For key '%s' expected header value '%s'. Got '%s'" |
2011 | + % (expected_key, expected_value, |
2012 | + response[expected_key])) |
2013 | + |
2014 | + for expected_key, expected_value in expected_std_headers.items(): |
2015 | + self.assertEqual(response[expected_key], expected_value, |
2016 | + "For key '%s' expected header value '%s'. Got '%s'" |
2017 | + % (expected_key, |
2018 | + expected_value, |
2019 | + response[expected_key])) |
2020 | + |
2021 | + self.assertEqual(content, "*" * FIVE_KB) |
2022 | + self.assertEqual(hashlib.md5(content).hexdigest(), |
2023 | + hashlib.md5("*" * FIVE_KB).hexdigest()) |
2024 | + |
2025 | + # 6. GET /images |
2026 | + # Verify no public images |
2027 | + path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port) |
2028 | + http = httplib2.Http() |
2029 | + response, content = http.request(path, 'GET') |
2030 | + self.assertEqual(response.status, 200) |
2031 | + |
2032 | + expected_result = {"images": [ |
2033 | + {"container_format": None, |
2034 | + "disk_format": None, |
2035 | + "id": 1, |
2036 | + "name": "Image1", |
2037 | + "checksum": "c2e5db72bd7fd153f53ede5da5a06de3", |
2038 | + "size": 5120}]} |
2039 | + self.assertEqual(json.loads(content), expected_result) |
2040 | + |
2041 | + # 7. GET /images/detail |
2042 | + # Verify image and all its metadata |
2043 | + path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port) |
2044 | + http = httplib2.Http() |
2045 | + response, content = http.request(path, 'GET') |
2046 | + self.assertEqual(response.status, 200) |
2047 | + |
2048 | + expected_image = { |
2049 | + "status": "active", |
2050 | + "name": "Image1", |
2051 | + "deleted": False, |
2052 | + "container_format": None, |
2053 | + "disk_format": None, |
2054 | + "id": 1, |
2055 | + "location": "file://%s/1" % self.api_server.image_dir, |
2056 | + "is_public": True, |
2057 | + "deleted_at": None, |
2058 | + "properties": {}, |
2059 | + "size": 5120} |
2060 | + |
2061 | + image = json.loads(content) |
2062 | + |
2063 | + for expected_key, expected_value in expected_image.items(): |
2064 | + self.assertEqual(expected_value, expected_image[expected_key], |
2065 | + "For key '%s' expected header value '%s'. Got '%s'" |
2066 | + % (expected_key, |
2067 | + expected_value, |
2068 | + image['images'][0][expected_key])) |
2069 | + |
2070 | + # 8. PUT /images/1 with custom properties of "distro" and "arch" |
2071 | + # Verify 200 returned |
2072 | + headers = {'X-Image-Meta-Property-Distro': 'Ubuntu', |
2073 | + 'X-Image-Meta-Property-Arch': 'x86_64'} |
2074 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
2075 | + http = httplib2.Http() |
2076 | + response, content = http.request(path, 'PUT', headers=headers) |
2077 | + self.assertEqual(response.status, 200) |
2078 | + data = json.loads(content) |
2079 | + self.assertEqual(data['image']['properties']['arch'], "x86_64") |
2080 | + self.assertEqual(data['image']['properties']['distro'], "Ubuntu") |
2081 | + |
2082 | + # 9. GET /images/detail |
2083 | + # Verify image and all its metadata |
2084 | + path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port) |
2085 | + http = httplib2.Http() |
2086 | + response, content = http.request(path, 'GET') |
2087 | + self.assertEqual(response.status, 200) |
2088 | + |
2089 | + expected_image = { |
2090 | + "status": "active", |
2091 | + "name": "Image1", |
2092 | + "deleted": False, |
2093 | + "container_format": None, |
2094 | + "disk_format": None, |
2095 | + "id": 1, |
2096 | + "location": "file://%s/1" % self.api_server.image_dir, |
2097 | + "is_public": True, |
2098 | + "deleted_at": None, |
2099 | + "properties": {'distro': 'Ubuntu', 'arch': 'x86_64'}, |
2100 | + "size": 5120} |
2101 | + |
2102 | + image = json.loads(content) |
2103 | + |
2104 | + for expected_key, expected_value in expected_image.items(): |
2105 | + self.assertEqual(expected_value, expected_image[expected_key], |
2106 | + "For key '%s' expected header value '%s'. Got '%s'" |
2107 | + % (expected_key, |
2108 | + expected_value, |
2109 | + image['images'][0][expected_key])) |
2110 | + |
2111 | + # 10. PUT /images/1 and remove a previously existing property. |
2112 | + headers = {'X-Image-Meta-Property-Arch': 'x86_64'} |
2113 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
2114 | + http = httplib2.Http() |
2115 | + response, content = http.request(path, 'PUT', headers=headers) |
2116 | + self.assertEqual(response.status, 200) |
2117 | + |
2118 | + path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port) |
2119 | + response, content = http.request(path, 'GET') |
2120 | + self.assertEqual(response.status, 200) |
2121 | + data = json.loads(content)['images'][0] |
2122 | + self.assertEqual(len(data['properties']), 1) |
2123 | + self.assertEqual(data['properties']['arch'], "x86_64") |
2124 | + |
2125 | + # 11. PUT /images/1 and add a previously deleted property. |
2126 | + headers = {'X-Image-Meta-Property-Distro': 'Ubuntu', |
2127 | + 'X-Image-Meta-Property-Arch': 'x86_64'} |
2128 | + path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port) |
2129 | + http = httplib2.Http() |
2130 | + response, content = http.request(path, 'PUT', headers=headers) |
2131 | + self.assertEqual(response.status, 200) |
2132 | + data = json.loads(content) |
2133 | + |
2134 | + path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port) |
2135 | + response, content = http.request(path, 'GET') |
2136 | + self.assertEqual(response.status, 200) |
2137 | + data = json.loads(content)['images'][0] |
2138 | + self.assertEqual(len(data['properties']), 2) |
2139 | + self.assertEqual(data['properties']['arch'], "x86_64") |
2140 | + self.assertEqual(data['properties']['distro'], "Ubuntu") |
2141 | + |
2142 | + self.stop_servers() |
2143 | |
2144 | === modified file 'tests/stubs.py' |
2145 | --- tests/stubs.py 2011-05-31 19:32:17 +0000 |
2146 | +++ tests/stubs.py 2011-06-28 16:16:37 +0000 |
2147 | @@ -28,6 +28,7 @@ |
2148 | import stubout |
2149 | import webob |
2150 | |
2151 | +import glance.common.client |
2152 | from glance.common import exception |
2153 | from glance.registry import server as rserver |
2154 | from glance.api import v1 as server |
2155 | @@ -254,9 +255,9 @@ |
2156 | for i in self.response.app_iter: |
2157 | yield i |
2158 | |
2159 | - stubs.Set(glance.client.BaseClient, 'get_connection_type', |
2160 | + stubs.Set(glance.common.client.BaseClient, 'get_connection_type', |
2161 | fake_get_connection_type) |
2162 | - stubs.Set(glance.client.ImageBodyIterator, '__iter__', |
2163 | + stubs.Set(glance.common.client.ImageBodyIterator, '__iter__', |
2164 | fake_image_iter) |
2165 | |
2166 | |
2167 | @@ -388,8 +389,8 @@ |
2168 | else: |
2169 | return images[0] |
2170 | |
2171 | - def image_get_all_public(self, _context, filters=None, |
2172 | - marker=None, limit=1000): |
2173 | + def image_get_all_public(self, _context, filters=None, marker=None, |
2174 | + limit=1000, sort_key=None, sort_dir=None): |
2175 | images = [f for f in self.images if f['is_public'] == True] |
2176 | |
2177 | if 'size_min' in filters: |
2178 | @@ -414,16 +415,24 @@ |
2179 | for k, v in filters.items(): |
2180 | images = [f for f in images if f[k] == v] |
2181 | |
2182 | + # sorted func expects func that compares in descending order |
2183 | def image_cmp(x, y): |
2184 | - if x['created_at'] > y['created_at']: |
2185 | - return 1 |
2186 | - elif x['created_at'] == y['created_at']: |
2187 | + _sort_dir = sort_dir or 'desc' |
2188 | + multiplier = { |
2189 | + 'asc': -1, |
2190 | + 'desc': 1, |
2191 | + }[_sort_dir] |
2192 | + |
2193 | + _sort_key = sort_key or 'created_at' |
2194 | + if x[_sort_key] > y[_sort_key]: |
2195 | + return 1 * multiplier |
2196 | + elif x[_sort_key] == y[_sort_key]: |
2197 | if x['id'] > y['id']: |
2198 | - return 1 |
2199 | + return 1 * multiplier |
2200 | else: |
2201 | - return -1 |
2202 | + return -1 * multiplier |
2203 | else: |
2204 | - return -1 |
2205 | + return -1 * multiplier |
2206 | |
2207 | images = sorted(images, cmp=image_cmp) |
2208 | images.reverse() |
2209 | |
2210 | === modified file 'tests/unit/test_api.py' |
2211 | --- tests/unit/test_api.py 2011-05-31 19:32:17 +0000 |
2212 | +++ tests/unit/test_api.py 2011-06-28 16:16:37 +0000 |
2213 | @@ -285,6 +285,398 @@ |
2214 | for image in images: |
2215 | self.assertEqual('new name! #123', image['name']) |
2216 | |
2217 | + def test_get_index_sort_default_created_at_desc(self): |
2218 | + """ |
2219 | + Tests that the /images registry API returns list of |
2220 | + public images that conforms to a default sort key/dir |
2221 | + """ |
2222 | + time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5) |
2223 | + time2 = datetime.datetime.utcnow() |
2224 | + |
2225 | + extra_fixture = {'id': 3, |
2226 | + 'status': 'active', |
2227 | + 'is_public': True, |
2228 | + 'disk_format': 'vhd', |
2229 | + 'container_format': 'ovf', |
2230 | + 'name': 'new name! #123', |
2231 | + 'size': 19, |
2232 | + 'checksum': None, |
2233 | + 'created_at': time1} |
2234 | + |
2235 | + glance.registry.db.api.image_create(None, extra_fixture) |
2236 | + |
2237 | + extra_fixture = {'id': 4, |
2238 | + 'status': 'active', |
2239 | + 'is_public': True, |
2240 | + 'disk_format': 'vhd', |
2241 | + 'container_format': 'ovf', |
2242 | + 'name': 'new name! #123', |
2243 | + 'size': 20, |
2244 | + 'checksum': None, |
2245 | + 'created_at': time1} |
2246 | + |
2247 | + glance.registry.db.api.image_create(None, extra_fixture) |
2248 | + |
2249 | + extra_fixture = {'id': 5, |
2250 | + 'status': 'active', |
2251 | + 'is_public': True, |
2252 | + 'disk_format': 'vhd', |
2253 | + 'container_format': 'ovf', |
2254 | + 'name': 'new name! #123', |
2255 | + 'size': 20, |
2256 | + 'checksum': None, |
2257 | + 'created_at': time2} |
2258 | + |
2259 | + glance.registry.db.api.image_create(None, extra_fixture) |
2260 | + |
2261 | + req = webob.Request.blank('/images') |
2262 | + res = req.get_response(self.api) |
2263 | + res_dict = json.loads(res.body) |
2264 | + self.assertEquals(res.status_int, 200) |
2265 | + |
2266 | + images = res_dict['images'] |
2267 | + self.assertEquals(len(images), 4) |
2268 | + self.assertEquals(int(images[0]['id']), 4) |
2269 | + self.assertEquals(int(images[1]['id']), 3) |
2270 | + self.assertEquals(int(images[2]['id']), 5) |
2271 | + self.assertEquals(int(images[3]['id']), 2) |
2272 | + |
2273 | + def test_get_index_bad_sort_key(self): |
2274 | + """Ensure a 400 is returned when a bad sort_key is provided.""" |
2275 | + req = webob.Request.blank('/images?sort_key=asdf') |
2276 | + res = req.get_response(self.api) |
2277 | + self.assertEqual(400, res.status_int) |
2278 | + |
2279 | + def test_get_index_bad_sort_dir(self): |
2280 | + """Ensure a 400 is returned when a bad sort_dir is provided.""" |
2281 | + req = webob.Request.blank('/images?sort_dir=asdf') |
2282 | + res = req.get_response(self.api) |
2283 | + self.assertEqual(400, res.status_int) |
2284 | + |
2285 | + def test_get_index_sort_id_desc(self): |
2286 | + """ |
2287 | + Tests that the /images registry API returns list of |
2288 | + public images sorted by id in descending order. |
2289 | + """ |
2290 | + extra_fixture = {'id': 3, |
2291 | + 'status': 'active', |
2292 | + 'is_public': True, |
2293 | + 'disk_format': 'vhd', |
2294 | + 'container_format': 'ovf', |
2295 | + 'name': 'asdf', |
2296 | + 'size': 19, |
2297 | + 'checksum': None} |
2298 | + |
2299 | + glance.registry.db.api.image_create(None, extra_fixture) |
2300 | + |
2301 | + extra_fixture = {'id': 4, |
2302 | + 'status': 'active', |
2303 | + 'is_public': True, |
2304 | + 'disk_format': 'vhd', |
2305 | + 'container_format': 'ovf', |
2306 | + 'name': 'xyz', |
2307 | + 'size': 20, |
2308 | + 'checksum': None} |
2309 | + |
2310 | + glance.registry.db.api.image_create(None, extra_fixture) |
2311 | + |
2312 | + req = webob.Request.blank('/images?sort_key=id&sort_dir=desc') |
2313 | + res = req.get_response(self.api) |
2314 | + self.assertEquals(res.status_int, 200) |
2315 | + res_dict = json.loads(res.body) |
2316 | + |
2317 | + images = res_dict['images'] |
2318 | + self.assertEquals(len(images), 3) |
2319 | + self.assertEquals(int(images[0]['id']), 4) |
2320 | + self.assertEquals(int(images[1]['id']), 3) |
2321 | + self.assertEquals(int(images[2]['id']), 2) |
2322 | + |
2323 | + def test_get_index_sort_name_asc(self): |
2324 | + """ |
2325 | + Tests that the /images registry API returns list of |
2326 | + public images sorted alphabetically by name in |
2327 | + ascending order. |
2328 | + """ |
2329 | + extra_fixture = {'id': 3, |
2330 | + 'status': 'active', |
2331 | + 'is_public': True, |
2332 | + 'disk_format': 'vhd', |
2333 | + 'container_format': 'ovf', |
2334 | + 'name': 'asdf', |
2335 | + 'size': 19, |
2336 | + 'checksum': None} |
2337 | + |
2338 | + glance.registry.db.api.image_create(None, extra_fixture) |
2339 | + |
2340 | + extra_fixture = {'id': 4, |
2341 | + 'status': 'active', |
2342 | + 'is_public': True, |
2343 | + 'disk_format': 'vhd', |
2344 | + 'container_format': 'ovf', |
2345 | + 'name': 'xyz', |
2346 | + 'size': 20, |
2347 | + 'checksum': None} |
2348 | + |
2349 | + glance.registry.db.api.image_create(None, extra_fixture) |
2350 | + |
2351 | + req = webob.Request.blank('/images?sort_key=name&sort_dir=asc') |
2352 | + res = req.get_response(self.api) |
2353 | + self.assertEquals(res.status_int, 200) |
2354 | + res_dict = json.loads(res.body) |
2355 | + |
2356 | + images = res_dict['images'] |
2357 | + self.assertEquals(len(images), 3) |
2358 | + self.assertEquals(int(images[0]['id']), 3) |
2359 | + self.assertEquals(int(images[1]['id']), 2) |
2360 | + self.assertEquals(int(images[2]['id']), 4) |
2361 | + |
2362 | + def test_get_index_sort_status_desc(self): |
2363 | + """ |
2364 | + Tests that the /images registry API returns list of |
2365 | + public images sorted alphabetically by status in |
2366 | + descending order. |
2367 | + """ |
2368 | + extra_fixture = {'id': 3, |
2369 | + 'status': 'killed', |
2370 | + 'is_public': True, |
2371 | + 'disk_format': 'vhd', |
2372 | + 'container_format': 'ovf', |
2373 | + 'name': 'asdf', |
2374 | + 'size': 19, |
2375 | + 'checksum': None} |
2376 | + |
2377 | + glance.registry.db.api.image_create(None, extra_fixture) |
2378 | + |
2379 | + extra_fixture = {'id': 4, |
2380 | + 'status': 'active', |
2381 | + 'is_public': True, |
2382 | + 'disk_format': 'vhd', |
2383 | + 'container_format': 'ovf', |
2384 | + 'name': 'xyz', |
2385 | + 'size': 20, |
2386 | + 'checksum': None} |
2387 | + |
2388 | + glance.registry.db.api.image_create(None, extra_fixture) |
2389 | + |
2390 | + req = webob.Request.blank('/images?sort_key=status&sort_dir=desc') |
2391 | + res = req.get_response(self.api) |
2392 | + self.assertEquals(res.status_int, 200) |
2393 | + res_dict = json.loads(res.body) |
2394 | + |
2395 | + images = res_dict['images'] |
2396 | + self.assertEquals(len(images), 3) |
2397 | + self.assertEquals(int(images[0]['id']), 3) |
2398 | + self.assertEquals(int(images[1]['id']), 4) |
2399 | + self.assertEquals(int(images[2]['id']), 2) |
2400 | + |
2401 | + def test_get_index_sort_disk_format_asc(self): |
2402 | + """ |
2403 | + Tests that the /images registry API returns list of |
2404 | + public images sorted alphabetically by disk_format in |
2405 | + ascending order. |
2406 | + """ |
2407 | + extra_fixture = {'id': 3, |
2408 | + 'status': 'active', |
2409 | + 'is_public': True, |
2410 | + 'disk_format': 'ami', |
2411 | + 'container_format': 'ami', |
2412 | + 'name': 'asdf', |
2413 | + 'size': 19, |
2414 | + 'checksum': None} |
2415 | + |
2416 | + glance.registry.db.api.image_create(None, extra_fixture) |
2417 | + |
2418 | + extra_fixture = {'id': 4, |
2419 | + 'status': 'active', |
2420 | + 'is_public': True, |
2421 | + 'disk_format': 'vdi', |
2422 | + 'container_format': 'ovf', |
2423 | + 'name': 'xyz', |
2424 | + 'size': 20, |
2425 | + 'checksum': None} |
2426 | + |
2427 | + glance.registry.db.api.image_create(None, extra_fixture) |
2428 | + |
2429 | + req = webob.Request.blank('/images?sort_key=disk_format&sort_dir=asc') |
2430 | + res = req.get_response(self.api) |
2431 | + self.assertEquals(res.status_int, 200) |
2432 | + res_dict = json.loads(res.body) |
2433 | + |
2434 | + images = res_dict['images'] |
2435 | + self.assertEquals(len(images), 3) |
2436 | + self.assertEquals(int(images[0]['id']), 3) |
2437 | + self.assertEquals(int(images[1]['id']), 4) |
2438 | + self.assertEquals(int(images[2]['id']), 2) |
2439 | + |
2440 | + def test_get_index_sort_container_format_desc(self): |
2441 | + """ |
2442 | + Tests that the /images registry API returns list of |
2443 | + public images sorted alphabetically by container_format in |
2444 | + descending order. |
2445 | + """ |
2446 | + extra_fixture = {'id': 3, |
2447 | + 'status': 'active', |
2448 | + 'is_public': True, |
2449 | + 'disk_format': 'ami', |
2450 | + 'container_format': 'ami', |
2451 | + 'name': 'asdf', |
2452 | + 'size': 19, |
2453 | + 'checksum': None} |
2454 | + |
2455 | + glance.registry.db.api.image_create(None, extra_fixture) |
2456 | + |
2457 | + extra_fixture = {'id': 4, |
2458 | + 'status': 'active', |
2459 | + 'is_public': True, |
2460 | + 'disk_format': 'iso', |
2461 | + 'container_format': 'bare', |
2462 | + 'name': 'xyz', |
2463 | + 'size': 20, |
2464 | + 'checksum': None} |
2465 | + |
2466 | + glance.registry.db.api.image_create(None, extra_fixture) |
2467 | + |
2468 | + url = '/images?sort_key=container_format&sort_dir=desc' |
2469 | + req = webob.Request.blank(url) |
2470 | + res = req.get_response(self.api) |
2471 | + self.assertEquals(res.status_int, 200) |
2472 | + res_dict = json.loads(res.body) |
2473 | + |
2474 | + images = res_dict['images'] |
2475 | + self.assertEquals(len(images), 3) |
2476 | + self.assertEquals(int(images[0]['id']), 2) |
2477 | + self.assertEquals(int(images[1]['id']), 4) |
2478 | + self.assertEquals(int(images[2]['id']), 3) |
2479 | + |
2480 | + def test_get_index_sort_size_asc(self): |
2481 | + """ |
2482 | + Tests that the /images registry API returns list of |
2483 | + public images sorted by size in ascending order. |
2484 | + """ |
2485 | + extra_fixture = {'id': 3, |
2486 | + 'status': 'active', |
2487 | + 'is_public': True, |
2488 | + 'disk_format': 'ami', |
2489 | + 'container_format': 'ami', |
2490 | + 'name': 'asdf', |
2491 | + 'size': 100, |
2492 | + 'checksum': None} |
2493 | + |
2494 | + glance.registry.db.api.image_create(None, extra_fixture) |
2495 | + |
2496 | + extra_fixture = {'id': 4, |
2497 | + 'status': 'active', |
2498 | + 'is_public': True, |
2499 | + 'disk_format': 'iso', |
2500 | + 'container_format': 'bare', |
2501 | + 'name': 'xyz', |
2502 | + 'size': 2, |
2503 | + 'checksum': None} |
2504 | + |
2505 | + glance.registry.db.api.image_create(None, extra_fixture) |
2506 | + |
2507 | + url = '/images?sort_key=size&sort_dir=asc' |
2508 | + req = webob.Request.blank(url) |
2509 | + res = req.get_response(self.api) |
2510 | + self.assertEquals(res.status_int, 200) |
2511 | + res_dict = json.loads(res.body) |
2512 | + |
2513 | + images = res_dict['images'] |
2514 | + self.assertEquals(len(images), 3) |
2515 | + self.assertEquals(int(images[0]['id']), 4) |
2516 | + self.assertEquals(int(images[1]['id']), 2) |
2517 | + self.assertEquals(int(images[2]['id']), 3) |
2518 | + |
2519 | + def test_get_index_sort_created_at_asc(self): |
2520 | + """ |
2521 | + Tests that the /images registry API returns list of |
2522 | + public images sorted by created_at in ascending order. |
2523 | + """ |
2524 | + now = datetime.datetime.utcnow() |
2525 | + time1 = now + datetime.timedelta(seconds=5) |
2526 | + time2 = now |
2527 | + |
2528 | + extra_fixture = {'id': 3, |
2529 | + 'status': 'active', |
2530 | + 'is_public': True, |
2531 | + 'disk_format': 'vhd', |
2532 | + 'container_format': 'ovf', |
2533 | + 'name': 'new name! #123', |
2534 | + 'size': 19, |
2535 | + 'checksum': None, |
2536 | + 'created_at': time1} |
2537 | + |
2538 | + glance.registry.db.api.image_create(None, extra_fixture) |
2539 | + |
2540 | + extra_fixture = {'id': 4, |
2541 | + 'status': 'active', |
2542 | + 'is_public': True, |
2543 | + 'disk_format': 'vhd', |
2544 | + 'container_format': 'ovf', |
2545 | + 'name': 'new name! #123', |
2546 | + 'size': 20, |
2547 | + 'checksum': None, |
2548 | + 'created_at': time2} |
2549 | + |
2550 | + glance.registry.db.api.image_create(None, extra_fixture) |
2551 | + |
2552 | + req = webob.Request.blank('/images?sort_key=created_at&sort_dir=asc') |
2553 | + res = req.get_response(self.api) |
2554 | + self.assertEquals(res.status_int, 200) |
2555 | + res_dict = json.loads(res.body) |
2556 | + |
2557 | + images = res_dict['images'] |
2558 | + self.assertEquals(len(images), 3) |
2559 | + self.assertEquals(int(images[0]['id']), 2) |
2560 | + self.assertEquals(int(images[1]['id']), 4) |
2561 | + self.assertEquals(int(images[2]['id']), 3) |
2562 | + |
2563 | + def test_get_index_sort_updated_at_desc(self): |
2564 | + """ |
2565 | + Tests that the /images registry API returns list of |
2566 | + public images sorted by updated_at in descending order. |
2567 | + """ |
2568 | + now = datetime.datetime.utcnow() |
2569 | + time1 = now + datetime.timedelta(seconds=5) |
2570 | + time2 = now |
2571 | + |
2572 | + extra_fixture = {'id': 3, |
2573 | + 'status': 'active', |
2574 | + 'is_public': True, |
2575 | + 'disk_format': 'vhd', |
2576 | + 'container_format': 'ovf', |
2577 | + 'name': 'new name! #123', |
2578 | + 'size': 19, |
2579 | + 'checksum': None, |
2580 | + 'created_at': None, |
2581 | + 'created_at': time1} |
2582 | + |
2583 | + glance.registry.db.api.image_create(None, extra_fixture) |
2584 | + |
2585 | + extra_fixture = {'id': 4, |
2586 | + 'status': 'active', |
2587 | + 'is_public': True, |
2588 | + 'disk_format': 'vhd', |
2589 | + 'container_format': 'ovf', |
2590 | + 'name': 'new name! #123', |
2591 | + 'size': 20, |
2592 | + 'checksum': None, |
2593 | + 'created_at': None, |
2594 | + 'updated_at': time2} |
2595 | + |
2596 | + glance.registry.db.api.image_create(None, extra_fixture) |
2597 | + |
2598 | + req = webob.Request.blank('/images?sort_key=updated_at&sort_dir=desc') |
2599 | + res = req.get_response(self.api) |
2600 | + self.assertEquals(res.status_int, 200) |
2601 | + res_dict = json.loads(res.body) |
2602 | + |
2603 | + images = res_dict['images'] |
2604 | + self.assertEquals(len(images), 3) |
2605 | + self.assertEquals(int(images[0]['id']), 3) |
2606 | + self.assertEquals(int(images[1]['id']), 4) |
2607 | + self.assertEquals(int(images[2]['id']), 2) |
2608 | + |
2609 | def test_get_details(self): |
2610 | """Tests that the /images/detail registry API returns |
2611 | a mapping containing a list of detailed image information |
2612 | @@ -668,6 +1060,45 @@ |
2613 | for image in images: |
2614 | self.assertEqual('v a', image['properties']['prop_123']) |
2615 | |
2616 | + def test_get_details_sort_name_asc(self): |
2617 | + """ |
2618 | + Tests that the /images/details registry API returns list of |
2619 | + public images sorted alphabetically by name in |
2620 | + ascending order. |
2621 | + """ |
2622 | + extra_fixture = {'id': 3, |
2623 | + 'status': 'active', |
2624 | + 'is_public': True, |
2625 | + 'disk_format': 'vhd', |
2626 | + 'container_format': 'ovf', |
2627 | + 'name': 'asdf', |
2628 | + 'size': 19, |
2629 | + 'checksum': None} |
2630 | + |
2631 | + glance.registry.db.api.image_create(None, extra_fixture) |
2632 | + |
2633 | + extra_fixture = {'id': 4, |
2634 | + 'status': 'active', |
2635 | + 'is_public': True, |
2636 | + 'disk_format': 'vhd', |
2637 | + 'container_format': 'ovf', |
2638 | + 'name': 'xyz', |
2639 | + 'size': 20, |
2640 | + 'checksum': None} |
2641 | + |
2642 | + glance.registry.db.api.image_create(None, extra_fixture) |
2643 | + |
2644 | + req = webob.Request.blank('/images/detail?sort_key=name&sort_dir=asc') |
2645 | + res = req.get_response(self.api) |
2646 | + self.assertEquals(res.status_int, 200) |
2647 | + res_dict = json.loads(res.body) |
2648 | + |
2649 | + images = res_dict['images'] |
2650 | + self.assertEquals(len(images), 3) |
2651 | + self.assertEquals(int(images[0]['id']), 3) |
2652 | + self.assertEquals(int(images[1]['id']), 2) |
2653 | + self.assertEquals(int(images[2]['id']), 4) |
2654 | + |
2655 | def test_create_image(self): |
2656 | """Tests that the /images POST registry API creates the image""" |
2657 | fixture = {'name': 'fake public image', |
2658 | @@ -678,6 +1109,7 @@ |
2659 | req = webob.Request.blank('/images') |
2660 | |
2661 | req.method = 'POST' |
2662 | + req.content_type = 'application/json' |
2663 | req.body = json.dumps(dict(image=fixture)) |
2664 | |
2665 | res = req.get_response(self.api) |
2666 | @@ -706,6 +1138,7 @@ |
2667 | req = webob.Request.blank('/images') |
2668 | |
2669 | req.method = 'POST' |
2670 | + req.content_type = 'application/json' |
2671 | req.body = json.dumps(dict(image=fixture)) |
2672 | |
2673 | res = req.get_response(self.api) |
2674 | @@ -723,6 +1156,7 @@ |
2675 | req = webob.Request.blank('/images') |
2676 | |
2677 | req.method = 'POST' |
2678 | + req.content_type = 'application/json' |
2679 | req.body = json.dumps(dict(image=fixture)) |
2680 | |
2681 | res = req.get_response(self.api) |
2682 | @@ -739,6 +1173,7 @@ |
2683 | req = webob.Request.blank('/images') |
2684 | |
2685 | req.method = 'POST' |
2686 | + req.content_type = 'application/json' |
2687 | req.body = json.dumps(dict(image=fixture)) |
2688 | |
2689 | res = req.get_response(self.api) |
2690 | @@ -758,6 +1193,7 @@ |
2691 | req = webob.Request.blank('/images') |
2692 | |
2693 | req.method = 'POST' |
2694 | + req.content_type = 'application/json' |
2695 | req.body = json.dumps(dict(image=fixture)) |
2696 | |
2697 | res = req.get_response(self.api) |
2698 | @@ -772,6 +1208,7 @@ |
2699 | req = webob.Request.blank('/images/2') |
2700 | |
2701 | req.method = 'PUT' |
2702 | + req.content_type = 'application/json' |
2703 | req.body = json.dumps(dict(image=fixture)) |
2704 | |
2705 | res = req.get_response(self.api) |
2706 | @@ -791,6 +1228,7 @@ |
2707 | req = webob.Request.blank('/images/3') |
2708 | |
2709 | req.method = 'PUT' |
2710 | + req.content_type = 'application/json' |
2711 | req.body = json.dumps(dict(image=fixture)) |
2712 | |
2713 | res = req.get_response(self.api) |
2714 | @@ -804,6 +1242,7 @@ |
2715 | req = webob.Request.blank('/images/2') |
2716 | |
2717 | req.method = 'PUT' |
2718 | + req.content_type = 'application/json' |
2719 | req.body = json.dumps(dict(image=fixture)) |
2720 | |
2721 | res = req.get_response(self.api) |
2722 | @@ -817,6 +1256,7 @@ |
2723 | req = webob.Request.blank('/images/2') |
2724 | |
2725 | req.method = 'PUT' |
2726 | + req.content_type = 'application/json' |
2727 | req.body = json.dumps(dict(image=fixture)) |
2728 | |
2729 | res = req.get_response(self.api) |
2730 | @@ -830,6 +1270,7 @@ |
2731 | req = webob.Request.blank('/images/2') |
2732 | |
2733 | req.method = 'PUT' |
2734 | + req.content_type = 'application/json' |
2735 | req.body = json.dumps(dict(image=fixture)) |
2736 | |
2737 | res = req.get_response(self.api) |
2738 | @@ -844,6 +1285,7 @@ |
2739 | req = webob.Request.blank('/images/2') # Image 2 has disk format 'vhd' |
2740 | |
2741 | req.method = 'PUT' |
2742 | + req.content_type = 'application/json' |
2743 | req.body = json.dumps(dict(image=fixture)) |
2744 | |
2745 | res = req.get_response(self.api) |
2746 | @@ -972,6 +1414,21 @@ |
2747 | res_body = json.loads(res.body)['image'] |
2748 | self.assertEquals('queued', res_body['status']) |
2749 | |
2750 | + def test_add_image_no_location_no_content_type(self): |
2751 | + """Tests creates a queued image for no body and no loc header""" |
2752 | + fixture_headers = {'x-image-meta-store': 'file', |
2753 | + 'x-image-meta-disk-format': 'vhd', |
2754 | + 'x-image-meta-container-format': 'ovf', |
2755 | + 'x-image-meta-name': 'fake image #3'} |
2756 | + |
2757 | + req = webob.Request.blank("/images") |
2758 | + req.method = 'POST' |
2759 | + req.body = "chunk00000remainder" |
2760 | + for k, v in fixture_headers.iteritems(): |
2761 | + req.headers[k] = v |
2762 | + res = req.get_response(self.api) |
2763 | + self.assertEquals(res.status_int, 400) |
2764 | + |
2765 | def test_add_image_bad_store(self): |
2766 | """Tests raises BadRequest for invalid store header""" |
2767 | fixture_headers = {'x-image-meta-store': 'bad', |
2768 | @@ -1016,6 +1473,45 @@ |
2769 | "res.headerlist = %r" % res.headerlist) |
2770 | self.assertTrue('/images/3' in res.headers['location']) |
2771 | |
2772 | + def test_get_index_sort_name_asc(self): |
2773 | + """ |
2774 | + Tests that the /images registry API returns list of |
2775 | + public images sorted alphabetically by name in |
2776 | + ascending order. |
2777 | + """ |
2778 | + extra_fixture = {'id': 3, |
2779 | + 'status': 'active', |
2780 | + 'is_public': True, |
2781 | + 'disk_format': 'vhd', |
2782 | + 'container_format': 'ovf', |
2783 | + 'name': 'asdf', |
2784 | + 'size': 19, |
2785 | + 'checksum': None} |
2786 | + |
2787 | + glance.registry.db.api.image_create(None, extra_fixture) |
2788 | + |
2789 | + extra_fixture = {'id': 4, |
2790 | + 'status': 'active', |
2791 | + 'is_public': True, |
2792 | + 'disk_format': 'vhd', |
2793 | + 'container_format': 'ovf', |
2794 | + 'name': 'xyz', |
2795 | + 'size': 20, |
2796 | + 'checksum': None} |
2797 | + |
2798 | + glance.registry.db.api.image_create(None, extra_fixture) |
2799 | + |
2800 | + req = webob.Request.blank('/images?sort_key=name&sort_dir=asc') |
2801 | + res = req.get_response(self.api) |
2802 | + self.assertEquals(res.status_int, 200) |
2803 | + res_dict = json.loads(res.body) |
2804 | + |
2805 | + images = res_dict['images'] |
2806 | + self.assertEquals(len(images), 3) |
2807 | + self.assertEquals(int(images[0]['id']), 3) |
2808 | + self.assertEquals(int(images[1]['id']), 2) |
2809 | + self.assertEquals(int(images[2]['id']), 4) |
2810 | + |
2811 | def test_image_is_checksummed(self): |
2812 | """Test that the image contents are checksummed properly""" |
2813 | fixture_headers = {'x-image-meta-store': 'file', |
2814 | @@ -1122,6 +1618,8 @@ |
2815 | def test_show_image_basic(self): |
2816 | req = webob.Request.blank("/images/2") |
2817 | res = req.get_response(self.api) |
2818 | + self.assertEqual(res.status_int, 200) |
2819 | + self.assertEqual(res.content_type, 'application/octet-stream') |
2820 | self.assertEqual('chunk00000remainder', res.body) |
2821 | |
2822 | def test_show_non_exists_image(self): |
2823 | |
2824 | === modified file 'tests/unit/test_clients.py' |
2825 | --- tests/unit/test_clients.py 2011-05-31 19:32:17 +0000 |
2826 | +++ tests/unit/test_clients.py 2011-06-28 16:16:37 +0000 |
2827 | @@ -15,6 +15,7 @@ |
2828 | # License for the specific language governing permissions and limitations |
2829 | # under the License. |
2830 | |
2831 | +import datetime |
2832 | import json |
2833 | import os |
2834 | import stubout |
2835 | @@ -37,7 +38,7 @@ |
2836 | def test_bad_address(self): |
2837 | """Test ClientConnectionError raised""" |
2838 | c = client.Client("127.999.1.1") |
2839 | - self.assertRaises(client.ClientConnectionError, |
2840 | + self.assertRaises(exception.ClientConnectionError, |
2841 | c.get_image, |
2842 | 1) |
2843 | |
2844 | @@ -70,6 +71,298 @@ |
2845 | for k, v in fixture.items(): |
2846 | self.assertEquals(v, images[0][k]) |
2847 | |
2848 | + def test_get_index_sort_id_desc(self): |
2849 | + """ |
2850 | + Tests that the /images registry API returns list of |
2851 | + public images sorted by id in descending order. |
2852 | + """ |
2853 | + extra_fixture = {'id': 3, |
2854 | + 'status': 'active', |
2855 | + 'is_public': True, |
2856 | + 'disk_format': 'vhd', |
2857 | + 'container_format': 'ovf', |
2858 | + 'name': 'asdf', |
2859 | + 'size': 19, |
2860 | + 'checksum': None} |
2861 | + |
2862 | + glance.registry.db.api.image_create(None, extra_fixture) |
2863 | + |
2864 | + extra_fixture = {'id': 4, |
2865 | + 'status': 'active', |
2866 | + 'is_public': True, |
2867 | + 'disk_format': 'vhd', |
2868 | + 'container_format': 'ovf', |
2869 | + 'name': 'xyz', |
2870 | + 'size': 20, |
2871 | + 'checksum': None} |
2872 | + |
2873 | + glance.registry.db.api.image_create(None, extra_fixture) |
2874 | + |
2875 | + images = self.client.get_images(sort_key='id', sort_dir='desc') |
2876 | + |
2877 | + self.assertEquals(len(images), 3) |
2878 | + self.assertEquals(int(images[0]['id']), 4) |
2879 | + self.assertEquals(int(images[1]['id']), 3) |
2880 | + self.assertEquals(int(images[2]['id']), 2) |
2881 | + |
2882 | + def test_get_index_sort_name_asc(self): |
2883 | + """ |
2884 | + Tests that the /images registry API returns list of |
2885 | + public images sorted alphabetically by name in |
2886 | + ascending order. |
2887 | + """ |
2888 | + extra_fixture = {'id': 3, |
2889 | + 'status': 'active', |
2890 | + 'is_public': True, |
2891 | + 'disk_format': 'vhd', |
2892 | + 'container_format': 'ovf', |
2893 | + 'name': 'asdf', |
2894 | + 'size': 19, |
2895 | + 'checksum': None} |
2896 | + |
2897 | + glance.registry.db.api.image_create(None, extra_fixture) |
2898 | + |
2899 | + extra_fixture = {'id': 4, |
2900 | + 'status': 'active', |
2901 | + 'is_public': True, |
2902 | + 'disk_format': 'vhd', |
2903 | + 'container_format': 'ovf', |
2904 | + 'name': 'xyz', |
2905 | + 'size': 20, |
2906 | + 'checksum': None} |
2907 | + |
2908 | + glance.registry.db.api.image_create(None, extra_fixture) |
2909 | + |
2910 | + images = self.client.get_images(sort_key='name', sort_dir='asc') |
2911 | + |
2912 | + self.assertEquals(len(images), 3) |
2913 | + self.assertEquals(int(images[0]['id']), 3) |
2914 | + self.assertEquals(int(images[1]['id']), 2) |
2915 | + self.assertEquals(int(images[2]['id']), 4) |
2916 | + |
2917 | + def test_get_index_sort_status_desc(self): |
2918 | + """ |
2919 | + Tests that the /images registry API returns list of |
2920 | + public images sorted alphabetically by status in |
2921 | + descending order. |
2922 | + """ |
2923 | + extra_fixture = {'id': 3, |
2924 | + 'status': 'killed', |
2925 | + 'is_public': True, |
2926 | + 'disk_format': 'vhd', |
2927 | + 'container_format': 'ovf', |
2928 | + 'name': 'asdf', |
2929 | + 'size': 19, |
2930 | + 'checksum': None} |
2931 | + |
2932 | + glance.registry.db.api.image_create(None, extra_fixture) |
2933 | + |
2934 | + extra_fixture = {'id': 4, |
2935 | + 'status': 'active', |
2936 | + 'is_public': True, |
2937 | + 'disk_format': 'vhd', |
2938 | + 'container_format': 'ovf', |
2939 | + 'name': 'xyz', |
2940 | + 'size': 20, |
2941 | + 'checksum': None} |
2942 | + |
2943 | + glance.registry.db.api.image_create(None, extra_fixture) |
2944 | + |
2945 | + images = self.client.get_images(sort_key='status', sort_dir='desc') |
2946 | + |
2947 | + self.assertEquals(len(images), 3) |
2948 | + self.assertEquals(int(images[0]['id']), 3) |
2949 | + self.assertEquals(int(images[1]['id']), 4) |
2950 | + self.assertEquals(int(images[2]['id']), 2) |
2951 | + |
2952 | + def test_get_index_sort_disk_format_asc(self): |
2953 | + """ |
2954 | + Tests that the /images registry API returns list of |
2955 | + public images sorted alphabetically by disk_format in |
2956 | + ascending order. |
2957 | + """ |
2958 | + extra_fixture = {'id': 3, |
2959 | + 'status': 'active', |
2960 | + 'is_public': True, |
2961 | + 'disk_format': 'ami', |
2962 | + 'container_format': 'ami', |
2963 | + 'name': 'asdf', |
2964 | + 'size': 19, |
2965 | + 'checksum': None} |
2966 | + |
2967 | + glance.registry.db.api.image_create(None, extra_fixture) |
2968 | + |
2969 | + extra_fixture = {'id': 4, |
2970 | + 'status': 'active', |
2971 | + 'is_public': True, |
2972 | + 'disk_format': 'vdi', |
2973 | + 'container_format': 'ovf', |
2974 | + 'name': 'xyz', |
2975 | + 'size': 20, |
2976 | + 'checksum': None} |
2977 | + |
2978 | + glance.registry.db.api.image_create(None, extra_fixture) |
2979 | + |
2980 | + images = self.client.get_images(sort_key='disk_format', |
2981 | + sort_dir='asc') |
2982 | + |
2983 | + self.assertEquals(len(images), 3) |
2984 | + self.assertEquals(int(images[0]['id']), 3) |
2985 | + self.assertEquals(int(images[1]['id']), 4) |
2986 | + self.assertEquals(int(images[2]['id']), 2) |
2987 | + |
2988 | + def test_get_index_sort_container_format_desc(self): |
2989 | + """ |
2990 | + Tests that the /images registry API returns list of |
2991 | + public images sorted alphabetically by container_format in |
2992 | + descending order. |
2993 | + """ |
2994 | + extra_fixture = {'id': 3, |
2995 | + 'status': 'active', |
2996 | + 'is_public': True, |
2997 | + 'disk_format': 'ami', |
2998 | + 'container_format': 'ami', |
2999 | + 'name': 'asdf', |
3000 | + 'size': 19, |
3001 | + 'checksum': None} |
3002 | + |
3003 | + glance.registry.db.api.image_create(None, extra_fixture) |
3004 | + |
3005 | + extra_fixture = {'id': 4, |
3006 | + 'status': 'active', |
3007 | + 'is_public': True, |
3008 | + 'disk_format': 'iso', |
3009 | + 'container_format': 'bare', |
3010 | + 'name': 'xyz', |
3011 | + 'size': 20, |
3012 | + 'checksum': None} |
3013 | + |
3014 | + glance.registry.db.api.image_create(None, extra_fixture) |
3015 | + |
3016 | + images = self.client.get_images(sort_key='container_format', |
3017 | + sort_dir='desc') |
3018 | + |
3019 | + self.assertEquals(len(images), 3) |
3020 | + self.assertEquals(int(images[0]['id']), 2) |
3021 | + self.assertEquals(int(images[1]['id']), 4) |
3022 | + self.assertEquals(int(images[2]['id']), 3) |
3023 | + |
3024 | + def test_get_index_sort_size_asc(self): |
3025 | + """ |
3026 | + Tests that the /images registry API returns list of |
3027 | + public images sorted by size in ascending order. |
3028 | + """ |
3029 | + extra_fixture = {'id': 3, |
3030 | + 'status': 'active', |
3031 | + 'is_public': True, |
3032 | + 'disk_format': 'ami', |
3033 | + 'container_format': 'ami', |
3034 | + 'name': 'asdf', |
3035 | + 'size': 100, |
3036 | + 'checksum': None} |
3037 | + |
3038 | + glance.registry.db.api.image_create(None, extra_fixture) |
3039 | + |
3040 | + extra_fixture = {'id': 4, |
3041 | + 'status': 'active', |
3042 | + 'is_public': True, |
3043 | + 'disk_format': 'iso', |
3044 | + 'container_format': 'bare', |
3045 | + 'name': 'xyz', |
3046 | + 'size': 2, |
3047 | + 'checksum': None} |
3048 | + |
3049 | + glance.registry.db.api.image_create(None, extra_fixture) |
3050 | + |
3051 | + images = self.client.get_images(sort_key='size', sort_dir='asc') |
3052 | + |
3053 | + self.assertEquals(len(images), 3) |
3054 | + self.assertEquals(int(images[0]['id']), 4) |
3055 | + self.assertEquals(int(images[1]['id']), 2) |
3056 | + self.assertEquals(int(images[2]['id']), 3) |
3057 | + |
3058 | + def test_get_index_sort_created_at_asc(self): |
3059 | + """ |
3060 | + Tests that the /images registry API returns list of |
3061 | + public images sorted by created_at in ascending order. |
3062 | + """ |
3063 | + now = datetime.datetime.utcnow() |
3064 | + time1 = now + datetime.timedelta(seconds=5) |
3065 | + time2 = now |
3066 | + |
3067 | + extra_fixture = {'id': 3, |
3068 | + 'status': 'active', |
3069 | + 'is_public': True, |
3070 | + 'disk_format': 'vhd', |
3071 | + 'container_format': 'ovf', |
3072 | + 'name': 'new name! #123', |
3073 | + 'size': 19, |
3074 | + 'checksum': None, |
3075 | + 'created_at': time1} |
3076 | + |
3077 | + glance.registry.db.api.image_create(None, extra_fixture) |
3078 | + |
3079 | + extra_fixture = {'id': 4, |
3080 | + 'status': 'active', |
3081 | + 'is_public': True, |
3082 | + 'disk_format': 'vhd', |
3083 | + 'container_format': 'ovf', |
3084 | + 'name': 'new name! #123', |
3085 | + 'size': 20, |
3086 | + 'checksum': None, |
3087 | + 'created_at': time2} |
3088 | + |
3089 | + glance.registry.db.api.image_create(None, extra_fixture) |
3090 | + |
3091 | + images = self.client.get_images(sort_key='created_at', sort_dir='asc') |
3092 | + |
3093 | + self.assertEquals(len(images), 3) |
3094 | + self.assertEquals(int(images[0]['id']), 2) |
3095 | + self.assertEquals(int(images[1]['id']), 4) |
3096 | + self.assertEquals(int(images[2]['id']), 3) |
3097 | + |
3098 | + def test_get_index_sort_updated_at_desc(self): |
3099 | + """ |
3100 | + Tests that the /images registry API returns list of |
3101 | + public images sorted by updated_at in descending order. |
3102 | + """ |
3103 | + now = datetime.datetime.utcnow() |
3104 | + time1 = now + datetime.timedelta(seconds=5) |
3105 | + time2 = now |
3106 | + |
3107 | + extra_fixture = {'id': 3, |
3108 | + 'status': 'active', |
3109 | + 'is_public': True, |
3110 | + 'disk_format': 'vhd', |
3111 | + 'container_format': 'ovf', |
3112 | + 'name': 'new name! #123', |
3113 | + 'size': 19, |
3114 | + 'checksum': None, |
3115 | + 'created_at': None, |
3116 | + 'created_at': time1} |
3117 | + |
3118 | + glance.registry.db.api.image_create(None, extra_fixture) |
3119 | + |
3120 | + extra_fixture = {'id': 4, |
3121 | + 'status': 'active', |
3122 | + 'is_public': True, |
3123 | + 'disk_format': 'vhd', |
3124 | + 'container_format': 'ovf', |
3125 | + 'name': 'new name! #123', |
3126 | + 'size': 20, |
3127 | + 'checksum': None, |
3128 | + 'created_at': None, |
3129 | + 'updated_at': time2} |
3130 | + |
3131 | + glance.registry.db.api.image_create(None, extra_fixture) |
3132 | + |
3133 | + images = self.client.get_images(sort_key='updated_at', sort_dir='desc') |
3134 | + |
3135 | + self.assertEquals(len(images), 3) |
3136 | + self.assertEquals(int(images[0]['id']), 3) |
3137 | + self.assertEquals(int(images[1]['id']), 4) |
3138 | + self.assertEquals(int(images[2]['id']), 2) |
3139 | + |
3140 | def test_get_image_index_marker(self): |
3141 | """Test correct set of images returned with marker param.""" |
3142 | extra_fixture = {'id': 3, |
3143 | @@ -170,7 +463,7 @@ |
3144 | |
3145 | glance.registry.db.api.image_create(None, extra_fixture) |
3146 | |
3147 | - images = self.client.get_images({'name': 'new name! #123'}) |
3148 | + images = self.client.get_images(filters={'name': 'new name! #123'}) |
3149 | self.assertEquals(len(images), 1) |
3150 | |
3151 | for image in images: |
3152 | @@ -236,7 +529,8 @@ |
3153 | |
3154 | glance.registry.db.api.image_create(None, extra_fixture) |
3155 | |
3156 | - images = self.client.get_images_detailed({'name': 'new name! #123'}) |
3157 | + filters = {'name': 'new name! #123'} |
3158 | + images = self.client.get_images_detailed(filters=filters) |
3159 | self.assertEquals(len(images), 1) |
3160 | |
3161 | for image in images: |
3162 | @@ -255,7 +549,7 @@ |
3163 | |
3164 | glance.registry.db.api.image_create(None, extra_fixture) |
3165 | |
3166 | - images = self.client.get_images_detailed({'status': 'saving'}) |
3167 | + images = self.client.get_images_detailed(filters={'status': 'saving'}) |
3168 | self.assertEquals(len(images), 1) |
3169 | |
3170 | for image in images: |
3171 | @@ -274,7 +568,8 @@ |
3172 | |
3173 | glance.registry.db.api.image_create(None, extra_fixture) |
3174 | |
3175 | - images = self.client.get_images_detailed({'container_format': 'ovf'}) |
3176 | + filters = {'container_format': 'ovf'} |
3177 | + images = self.client.get_images_detailed(filters=filters) |
3178 | self.assertEquals(len(images), 2) |
3179 | |
3180 | for image in images: |
3181 | @@ -293,7 +588,8 @@ |
3182 | |
3183 | glance.registry.db.api.image_create(None, extra_fixture) |
3184 | |
3185 | - images = self.client.get_images_detailed({'disk_format': 'vhd'}) |
3186 | + filters = {'disk_format': 'vhd'} |
3187 | + images = self.client.get_images_detailed(filters=filters) |
3188 | self.assertEquals(len(images), 2) |
3189 | |
3190 | for image in images: |
3191 | @@ -312,7 +608,7 @@ |
3192 | |
3193 | glance.registry.db.api.image_create(None, extra_fixture) |
3194 | |
3195 | - images = self.client.get_images_detailed({'size_max': 20}) |
3196 | + images = self.client.get_images_detailed(filters={'size_max': 20}) |
3197 | self.assertEquals(len(images), 1) |
3198 | |
3199 | for image in images: |
3200 | @@ -331,7 +627,7 @@ |
3201 | |
3202 | glance.registry.db.api.image_create(None, extra_fixture) |
3203 | |
3204 | - images = self.client.get_images_detailed({'size_min': 20}) |
3205 | + images = self.client.get_images_detailed(filters={'size_min': 20}) |
3206 | self.assertEquals(len(images), 1) |
3207 | |
3208 | for image in images: |
3209 | @@ -351,12 +647,49 @@ |
3210 | |
3211 | glance.registry.db.api.image_create(None, extra_fixture) |
3212 | |
3213 | - images = self.client.get_images_detailed({'property-p a': 'v a'}) |
3214 | + filters = {'property-p a': 'v a'} |
3215 | + images = self.client.get_images_detailed(filters=filters) |
3216 | self.assertEquals(len(images), 1) |
3217 | |
3218 | for image in images: |
3219 | self.assertEquals('v a', image['properties']['p a']) |
3220 | |
3221 | + def test_get_image_details_sort_disk_format_asc(self): |
3222 | + """ |
3223 | + Tests that a detailed call returns list of |
3224 | + public images sorted alphabetically by disk_format in |
3225 | + ascending order. |
3226 | + """ |
3227 | + extra_fixture = {'id': 3, |
3228 | + 'status': 'active', |
3229 | + 'is_public': True, |
3230 | + 'disk_format': 'ami', |
3231 | + 'container_format': 'ami', |
3232 | + 'name': 'asdf', |
3233 | + 'size': 19, |
3234 | + 'checksum': None} |
3235 | + |
3236 | + glance.registry.db.api.image_create(None, extra_fixture) |
3237 | + |
3238 | + extra_fixture = {'id': 4, |
3239 | + 'status': 'active', |
3240 | + 'is_public': True, |
3241 | + 'disk_format': 'vdi', |
3242 | + 'container_format': 'ovf', |
3243 | + 'name': 'xyz', |
3244 | + 'size': 20, |
3245 | + 'checksum': None} |
3246 | + |
3247 | + glance.registry.db.api.image_create(None, extra_fixture) |
3248 | + |
3249 | + images = self.client.get_images_detailed(sort_key='disk_format', |
3250 | + sort_dir='asc') |
3251 | + |
3252 | + self.assertEquals(len(images), 3) |
3253 | + self.assertEquals(int(images[0]['id']), 3) |
3254 | + self.assertEquals(int(images[1]['id']), 4) |
3255 | + self.assertEquals(int(images[2]['id']), 2) |
3256 | + |
3257 | def test_get_image(self): |
3258 | """Tests that the detailed info about an image returned""" |
3259 | fixture = {'id': 1, |
3260 | @@ -562,6 +895,42 @@ |
3261 | self.client.get_image, |
3262 | 3) |
3263 | |
3264 | + def test_get_image_index_sort_container_format_desc(self): |
3265 | + """ |
3266 | + Tests that the client returns list of public images |
3267 | + sorted alphabetically by container_format in |
3268 | + descending order. |
3269 | + """ |
3270 | + extra_fixture = {'id': 3, |
3271 | + 'status': 'active', |
3272 | + 'is_public': True, |
3273 | + 'disk_format': 'ami', |
3274 | + 'container_format': 'ami', |
3275 | + 'name': 'asdf', |
3276 | + 'size': 19, |
3277 | + 'checksum': None} |
3278 | + |
3279 | + glance.registry.db.api.image_create(None, extra_fixture) |
3280 | + |
3281 | + extra_fixture = {'id': 4, |
3282 | + 'status': 'active', |
3283 | + 'is_public': True, |
3284 | + 'disk_format': 'iso', |
3285 | + 'container_format': 'bare', |
3286 | + 'name': 'xyz', |
3287 | + 'size': 20, |
3288 | + 'checksum': None} |
3289 | + |
3290 | + glance.registry.db.api.image_create(None, extra_fixture) |
3291 | + |
3292 | + images = self.client.get_images(sort_key='container_format', |
3293 | + sort_dir='desc') |
3294 | + |
3295 | + self.assertEquals(len(images), 3) |
3296 | + self.assertEquals(int(images[0]['id']), 2) |
3297 | + self.assertEquals(int(images[1]['id']), 4) |
3298 | + self.assertEquals(int(images[2]['id']), 3) |
3299 | + |
3300 | def test_get_image_index(self): |
3301 | """Test correct set of public image returned""" |
3302 | fixture = {'id': 2, |
3303 | @@ -671,7 +1040,8 @@ |
3304 | |
3305 | glance.registry.db.api.image_create(None, extra_fixture) |
3306 | |
3307 | - images = self.client.get_images({'name': 'new name! #123'}) |
3308 | + filters = {'name': 'new name! #123'} |
3309 | + images = self.client.get_images(filters=filters) |
3310 | |
3311 | self.assertEquals(len(images), 1) |
3312 | self.assertEquals('new name! #123', images[0]['name']) |
3313 | @@ -690,7 +1060,8 @@ |
3314 | |
3315 | glance.registry.db.api.image_create(None, extra_fixture) |
3316 | |
3317 | - images = self.client.get_images({'property-p a': 'v a'}) |
3318 | + filters = {'property-p a': 'v a'} |
3319 | + images = self.client.get_images(filters=filters) |
3320 | |
3321 | self.assertEquals(len(images), 1) |
3322 | self.assertEquals(3, images[0]['id']) |
3323 | @@ -765,7 +1136,8 @@ |
3324 | |
3325 | glance.registry.db.api.image_create(None, extra_fixture) |
3326 | |
3327 | - images = self.client.get_images_detailed({'name': 'new name! #123'}) |
3328 | + filters = {'name': 'new name! #123'} |
3329 | + images = self.client.get_images_detailed(filters=filters) |
3330 | self.assertEquals(len(images), 1) |
3331 | |
3332 | for image in images: |
3333 | @@ -785,7 +1157,8 @@ |
3334 | |
3335 | glance.registry.db.api.image_create(None, extra_fixture) |
3336 | |
3337 | - images = self.client.get_images_detailed({'property-p a': 'v a'}) |
3338 | + filters = {'property-p a': 'v a'} |
3339 | + images = self.client.get_images_detailed(filters=filters) |
3340 | self.assertEquals(len(images), 1) |
3341 | |
3342 | for image in images: |
3343 | |
3344 | === added file 'tests/unit/test_wsgi.py' |
3345 | --- tests/unit/test_wsgi.py 1970-01-01 00:00:00 +0000 |
3346 | +++ tests/unit/test_wsgi.py 2011-06-28 16:16:37 +0000 |
3347 | @@ -0,0 +1,184 @@ |
3348 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
3349 | + |
3350 | +# Copyright 2010-2011 OpenStack, LLC |
3351 | +# All Rights Reserved. |
3352 | +# |
3353 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3354 | +# not use this file except in compliance with the License. You may obtain |
3355 | +# a copy of the License at |
3356 | +# |
3357 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3358 | +# |
3359 | +# Unless required by applicable law or agreed to in writing, software |
3360 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3361 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3362 | +# License for the specific language governing permissions and limitations |
3363 | +# under the License. |
3364 | + |
3365 | +import unittest |
3366 | +import webob |
3367 | + |
3368 | +from glance.common import wsgi |
3369 | +from glance.common import exception |
3370 | + |
3371 | + |
3372 | +class RequestTest(unittest.TestCase): |
3373 | + def test_content_type_missing(self): |
3374 | + request = wsgi.Request.blank('/tests/123') |
3375 | + self.assertRaises(exception.InvalidContentType, |
3376 | + request.get_content_type, ('application/xml')) |
3377 | + |
3378 | + def test_content_type_unsupported(self): |
3379 | + request = wsgi.Request.blank('/tests/123') |
3380 | + request.headers["Content-Type"] = "text/html" |
3381 | + self.assertRaises(exception.InvalidContentType, |
3382 | + request.get_content_type, ('application/xml')) |
3383 | + |
3384 | + def test_content_type_with_charset(self): |
3385 | + request = wsgi.Request.blank('/tests/123') |
3386 | + request.headers["Content-Type"] = "application/json; charset=UTF-8" |
3387 | + result = request.get_content_type(('application/json')) |
3388 | + self.assertEqual(result, "application/json") |
3389 | + |
3390 | + def test_content_type_from_accept_xml(self): |
3391 | + request = wsgi.Request.blank('/tests/123') |
3392 | + request.headers["Accept"] = "application/xml" |
3393 | + result = request.best_match_content_type() |
3394 | + self.assertEqual(result, "application/json") |
3395 | + |
3396 | + def test_content_type_from_accept_json(self): |
3397 | + request = wsgi.Request.blank('/tests/123') |
3398 | + request.headers["Accept"] = "application/json" |
3399 | + result = request.best_match_content_type() |
3400 | + self.assertEqual(result, "application/json") |
3401 | + |
3402 | + def test_content_type_from_accept_xml_json(self): |
3403 | + request = wsgi.Request.blank('/tests/123') |
3404 | + request.headers["Accept"] = "application/xml, application/json" |
3405 | + result = request.best_match_content_type() |
3406 | + self.assertEqual(result, "application/json") |
3407 | + |
3408 | + def test_content_type_from_accept_json_xml_quality(self): |
3409 | + request = wsgi.Request.blank('/tests/123') |
3410 | + request.headers["Accept"] = \ |
3411 | + "application/json; q=0.3, application/xml; q=0.9" |
3412 | + result = request.best_match_content_type() |
3413 | + self.assertEqual(result, "application/json") |
3414 | + |
3415 | + def test_content_type_accept_default(self): |
3416 | + request = wsgi.Request.blank('/tests/123.unsupported') |
3417 | + request.headers["Accept"] = "application/unsupported1" |
3418 | + result = request.best_match_content_type() |
3419 | + self.assertEqual(result, "application/json") |
3420 | + |
3421 | + |
3422 | +class ResourceTest(unittest.TestCase): |
3423 | + def test_get_action_args(self): |
3424 | + env = { |
3425 | + 'wsgiorg.routing_args': [ |
3426 | + None, |
3427 | + { |
3428 | + 'controller': None, |
3429 | + 'format': None, |
3430 | + 'action': 'update', |
3431 | + 'id': 12, |
3432 | + }, |
3433 | + ], |
3434 | + } |
3435 | + |
3436 | + expected = {'action': 'update', 'id': 12} |
3437 | + actual = wsgi.Resource(None, None, None).get_action_args(env) |
3438 | + |
3439 | + self.assertEqual(actual, expected) |
3440 | + |
3441 | + def test_dispatch(self): |
3442 | + class Controller(object): |
3443 | + def index(self, shirt, pants=None): |
3444 | + return (shirt, pants) |
3445 | + |
3446 | + resource = wsgi.Resource(None, None, None) |
3447 | + actual = resource.dispatch(Controller(), 'index', 'on', pants='off') |
3448 | + expected = ('on', 'off') |
3449 | + self.assertEqual(actual, expected) |
3450 | + |
3451 | + def test_dispatch_default(self): |
3452 | + class Controller(object): |
3453 | + def default(self, shirt, pants=None): |
3454 | + return (shirt, pants) |
3455 | + |
3456 | + resource = wsgi.Resource(None, None, None) |
3457 | + actual = resource.dispatch(Controller(), 'index', 'on', pants='off') |
3458 | + expected = ('on', 'off') |
3459 | + self.assertEqual(actual, expected) |
3460 | + |
3461 | + def test_dispatch_no_default(self): |
3462 | + class Controller(object): |
3463 | + def show(self, shirt, pants=None): |
3464 | + return (shirt, pants) |
3465 | + |
3466 | + resource = wsgi.Resource(None, None, None) |
3467 | + self.assertRaises(AttributeError, resource.dispatch, Controller(), |
3468 | + 'index', 'on', pants='off') |
3469 | + |
3470 | + |
3471 | +class JSONResponseSerializerTest(unittest.TestCase): |
3472 | + def test_to_json(self): |
3473 | + fixture = {"key": "value"} |
3474 | + expected = '{"key": "value"}' |
3475 | + actual = wsgi.JSONResponseSerializer().to_json(fixture) |
3476 | + self.assertEqual(actual, expected) |
3477 | + |
3478 | + def test_default(self): |
3479 | + fixture = {"key": "value"} |
3480 | + response = webob.Response() |
3481 | + wsgi.JSONResponseSerializer().default(response, fixture) |
3482 | + self.assertEqual(response.status_int, 200) |
3483 | + self.assertEqual(response.content_type, 'application/json') |
3484 | + self.assertEqual(response.body, '{"key": "value"}') |
3485 | + |
3486 | + |
3487 | +class JSONRequestDeserializerTest(unittest.TestCase): |
3488 | + def test_has_body_no_content_length(self): |
3489 | + request = wsgi.Request.blank('/') |
3490 | + request.method = 'POST' |
3491 | + request.body = 'asdf' |
3492 | + request.headers.pop('Content-Length') |
3493 | + self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request)) |
3494 | + |
3495 | + def test_has_body_zero_content_length(self): |
3496 | + request = wsgi.Request.blank('/') |
3497 | + request.method = 'POST' |
3498 | + request.body = 'asdf' |
3499 | + request.headers['Content-Length'] = 0 |
3500 | + self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request)) |
3501 | + |
3502 | + def test_has_body_has_content_length(self): |
3503 | + request = wsgi.Request.blank('/') |
3504 | + request.method = 'POST' |
3505 | + request.body = 'asdf' |
3506 | + self.assertTrue('Content-Length' in request.headers) |
3507 | + self.assertTrue(wsgi.JSONRequestDeserializer().has_body(request)) |
3508 | + |
3509 | + def test_no_body_no_content_length(self): |
3510 | + request = wsgi.Request.blank('/') |
3511 | + self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request)) |
3512 | + |
3513 | + def test_from_json(self): |
3514 | + fixture = '{"key": "value"}' |
3515 | + expected = {"key": "value"} |
3516 | + actual = wsgi.JSONRequestDeserializer().from_json(fixture) |
3517 | + self.assertEqual(actual, expected) |
3518 | + |
3519 | + def test_default_no_body(self): |
3520 | + request = wsgi.Request.blank('/') |
3521 | + actual = wsgi.JSONRequestDeserializer().default(request) |
3522 | + expected = {} |
3523 | + self.assertEqual(actual, expected) |
3524 | + |
3525 | + def test_default_with_body(self): |
3526 | + request = wsgi.Request.blank('/') |
3527 | + request.method = 'POST' |
3528 | + request.body = '{"key": "value"}' |
3529 | + actual = wsgi.JSONRequestDeserializer().default(request) |
3530 | + expected = {"body": {"key": "value"}} |
3531 | + self.assertEqual(actual, expected) |
3532 | |
3533 | === modified file 'tools/pip-requires' |
3534 | --- tools/pip-requires 2011-05-24 02:01:54 +0000 |
3535 | +++ tools/pip-requires 2011-06-28 16:16:37 +0000 |
3536 | @@ -6,7 +6,7 @@ |
3537 | eventlet>=0.9.12 |
3538 | PasteDeploy |
3539 | routes |
3540 | -webob |
3541 | +webob==1.0.8 |
3542 | wsgiref |
3543 | nose |
3544 | sphinx |
3545 | @@ -16,3 +16,5 @@ |
3546 | -f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz |
3547 | sqlalchemy-migrate>=0.6,<0.7 |
3548 | bzr |
3549 | +httplib2 |
3550 | +hashlib |
Good.