Merge lp:~ttx/glance/d2-merge into lp:~hudson-openstack/glance/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
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.
Revision history for this message
Jay Pipes (jaypipes) wrote :

Good.

review: Approve

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

Subscribers

People subscribed via source and target branches