Merge lp:~rackspace-titan/glance/api-results-ordering into lp:~hudson-openstack/glance/trunk

Proposed by Brian Waldon
Status: Merged
Approved by: Jay Pipes
Approved revision: 155
Merged at revision: 146
Proposed branch: lp:~rackspace-titan/glance/api-results-ordering
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 2032 lines (+1333/-256)
15 files modified
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/images.py (+17/-16)
glance/client.py (+13/-173)
glance/common/client.py (+169/-0)
glance/common/exception.py (+5/-0)
glance/registry/client.py (+10/-19)
glance/registry/db/api.py (+16/-6)
glance/registry/server.py (+54/-17)
tests/functional/test_curl_api.py (+115/-0)
tests/stubs.py (+19/-10)
tests/unit/test_api.py (+470/-0)
tests/unit/test_clients.py (+386/-13)
To merge this branch: bzr merge lp:~rackspace-titan/glance/api-results-ordering
Reviewer Review Type Date Requested Status
Jay Pipes (community) Approve
Review via email: mp+65290@code.launchpad.net

Description of the change

Added sort_key and sort_dir query params to apis and clients.

To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) wrote :

Hi Brian!

Nice work, yet again!

Small suggestions:

For the SUPPORTED_PARAMS, and SUPPORTED_GET_PARAMS (in glance.client), could you make a glance.api.v1.images module-level function that returns these supported parameters? Either that, or make the constant at that module-level and import them into glance.client. That will ensure that future modifications to that constant do not have to make the modifications in two places. You did this for the SUPPORTED_SORT_KEYS constant in the registry, so might as well do it for SUPPORTED_PARAMS, too. :)

278 + _sort_dir = sort_dir or 'desc'
279 + sort_dir_func = {
280 + 'asc': asc,
281 + 'desc': desc,
282 + }[_sort_dir]

Instead of having the sort_dir param default to None, how about having it default to 'desc', in which case, the above can be simplified:

sort_dir_func = {
    'asc': asc,
    'desc': desc
}[sort_dir]

Same can be said for this:

284 + _sort_key = sort_key or 'created_at'
285 + sort_key_attr = getattr(models.Image, _sort_key)

Just make the sort_key parameter default to 'created_at', and shorten to:

sort_key_attr = getattr(models.Image, sort_key)

For this repeated block of code:

174 + params = kwargs.get('filters', {})
175 + params.update(self._extract_get_params(kwargs))

Go ahead and put the first line in the _extract_get_params() method and have _extract_get_params() return the entire parameter dictionary. DRY...

Cheers!
jay

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :

I'll take care of your comments later today, Jay. I also just pushed some updates to the glance client docs. Missed that one spot last night.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> For the SUPPORTED_PARAMS, and SUPPORTED_GET_PARAMS (in glance.client), could
> you make a glance.api.v1.images module-level function that returns these
> supported parameters? Either that, or make the constant at that module-level
> and import them into glance.client. That will ensure that future modifications
> to that constant do not have to make the modifications in two places. You did
> this for the SUPPORTED_SORT_KEYS constant in the registry, so might as well do
> it for SUPPORTED_PARAMS, too. :)

I think I took care of this now. Can you double-check?

> 278 + _sort_dir = sort_dir or 'desc'
> 279 + sort_dir_func = {
> 280 + 'asc': asc,
> 281 + 'desc': desc,
> 282 + }[_sort_dir]
>
> Instead of having the sort_dir param default to None, how about having it
> default to 'desc', in which case, the above can be simplified:
>
>
> sort_dir_func = {
> 'asc': asc,
> 'desc': desc
> }[sort_dir]
>
> Same can be said for this:
>
> 284 + _sort_key = sort_key or 'created_at'
> 285 + sort_key_attr = getattr(models.Image, _sort_key)
>
> Just make the sort_key parameter default to 'created_at', and shorten to:
>
> sort_key_attr = getattr(models.Image, sort_key)

There are cases where None is being passed in explicitly. Is it okay if I leave it?

> For this repeated block of code:
>
> 174 + params = kwargs.get('filters', {})
> 175 + params.update(self._extract_get_params(kwargs))
>
> Go ahead and put the first line in the _extract_get_params() method and have
> _extract_get_params() return the entire parameter dictionary. DRY...

Got it.

Revision history for this message
Jay Pipes (jaypipes) wrote :

> I think I took care of this now. Can you double-check?

For the /glance/api/v1/images.py, looks good.

For the client, though:

161 +# parameters accepted in get_images* methods
162 +SUPPORTED_GET_PARAMS = ('marker', 'limit', 'sort_key', 'sort_dir')

I'd prefer you re-use the above constant in V1Client like so:

from glance.api.v1 import images as v1_images

and then move _extract_get_params() from the base Client class into the V1Client class, and reference v1_images.SUPPORTED_GET_PARAMS instead of the copied SUPPORTED_GET_PARAMS you have in the client.py file now... that make sense?

> > 278 + _sort_dir = sort_dir or 'desc'
> > 279 + sort_dir_func = {
> > 280 + 'asc': asc,
> > 281 + 'desc': desc,
> > 282 + }[_sort_dir]
> >
> > Instead of having the sort_dir param default to None, how about having it
> > default to 'desc', in which case, the above can be simplified:
> >
> >
> > sort_dir_func = {
> > 'asc': asc,
> > 'desc': desc
> > }[sort_dir]
> >
> > Same can be said for this:
> >
> > 284 + _sort_key = sort_key or 'created_at'
> > 285 + sort_key_attr = getattr(models.Image, _sort_key)
> >
> > Just make the sort_key parameter default to 'created_at', and shorten to:
> >
> > sort_key_attr = getattr(models.Image, sort_key)
>
> There are cases where None is being passed in explicitly. Is it okay if I
> leave it?

In what case is None being passed explicitly? Can we remove those cases and rely on default parameter values?

-jay

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > I think I took care of this now. Can you double-check?
>
> For the /glance/api/v1/images.py, looks good.
>
> For the client, though:
>
> 161 +# parameters accepted in get_images* methods
> 162 +SUPPORTED_GET_PARAMS = ('marker', 'limit', 'sort_key', 'sort_dir')
>
> I'd prefer you re-use the above constant in V1Client like so:
>
> from glance.api.v1 import images as v1_images
>
> and then move _extract_get_params() from the base Client class into the
> V1Client class, and reference v1_images.SUPPORTED_GET_PARAMS instead of the
> copied SUPPORTED_GET_PARAMS you have in the client.py file now... that make
> sense?

I had to move BaseClient out of glance/client.py and into glance/common/client.py. I was seeing some weird circular imports and that was the most direct solution I could think of.

I got all of the SUPPORTED_PARAMS stuff organized like you wanted. I ended up leaving the _extract_params function in BaseClient, as it is used by the registry client and the api client. I just have it passing in the allowed params.

> > > 278 + _sort_dir = sort_dir or 'desc'
> > > 279 + sort_dir_func = {
> > > 280 + 'asc': asc,
> > > 281 + 'desc': desc,
> > > 282 + }[_sort_dir]
> > >
> > > Instead of having the sort_dir param default to None, how about having it
> > > default to 'desc', in which case, the above can be simplified:
> > >
> > >
> > > sort_dir_func = {
> > > 'asc': asc,
> > > 'desc': desc
> > > }[sort_dir]
> > >
> > > Same can be said for this:
> > >
> > > 284 + _sort_key = sort_key or 'created_at'
> > > 285 + sort_key_attr = getattr(models.Image, _sort_key)
> > >
> > > Just make the sort_key parameter default to 'created_at', and shorten to:
> > >
> > > sort_key_attr = getattr(models.Image, sort_key)
> >
> > There are cases where None is being passed in explicitly. Is it okay if I
> > leave it?
>
> In what case is None being passed explicitly? Can we remove those cases and
> rely on default parameter values?

The function in registry/server.py that extracts the query params was returning None for keys that weren't provided. Now it filters out None. It's definitely cleaner to define the defaults in kwargs.

148. By Brian Waldon

merging trunk

149. By Brian Waldon

restructuring client code

150. By Brian Waldon

adding base client module

151. By Brian Waldon

cleaning up None values being passed into images_get_all_public db call

Revision history for this message
Jay Pipes (jaypipes) wrote :

Wicked. Great work, Brian!

Teeny weeny style thing:

789 + """Extract necessary query parameters from http request.
790 +
791 + :param req: the Request object coming from the wsgi layer
792 + :retval dictionary of filters to apply to list of images
793 +
794 + """

Should be:

"""
Extract necessary query parameters from http request.

:param req: the Request object coming from the wsgi layer
:retval dictionary of filters to apply to list of images
"""

There are a number of docstrings in the test cases that also need fixing for that. Sorry for being picky :(

-jay

review: Needs Fixing
152. By Brian Waldon

docstring

153. By Brian Waldon

reverting one import change; another docstring fix

Revision history for this message
Brian Waldon (bcwaldon) wrote :

I'm a little confused on what you want in the tests. Do you want me to change all of the comments like this:

"""Some really long dosctring that
wraps at 80 chars.

"""

to this:

"""
Some really long docstring that
wraps at 80 chars.
"""

I ask because the precedent is firmly set as the first style. I'm fine making changes, I just want to make sure I do it correctly.

Revision history for this message
Brian Lamar (blamar) wrote :

I think Nova and Glance have slightly differences in this aspect. Nova 'requires' a single line summary followed by a multi-line description:

"""This is the first line summary.

This is the multiple line description which in itself
can have multiple line breaks and stuff. Plus it has a newline
at the end of it all.

"""

If it's just one line then it has to be all on one line:

"""This is a one line summary that needs to be 80 char or less."""

But meh, I've had nightmares about this ;)

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Brian: So how do we do it in glance?

Revision history for this message
Jay Pipes (jaypipes) wrote :

On Mon, Jun 27, 2011 at 1:19 PM, Brian Waldon
<email address hidden> wrote:
> Brian: So how do we do it in glance?

The way I wrote above...

"""
This is a description. It can go more than one
line long. It's silly to force a one-line description
and then a newline and a multi-line description. Sometimes that
just doesn't make much sense.

And having an extra newline at the end of the docstring doesn't make
much sense either. Oh, and I don't care if you end your sentences with
periods
"""

Cheers,
jay

154. By Brian Waldon

docstrings\!

155. By Brian Waldon

fixing one last docstring

Revision history for this message
Brian Waldon (bcwaldon) wrote :

I updated all the doctrings I touched. Would you be cool with a follow-up branch to clean up docstrings across the project?

Revision history for this message
Jay Pipes (jaypipes) wrote :

But of course monsieur.
On Jun 28, 2011 8:52 AM, "Brian Waldon" <email address hidden> wrote:
> I updated all the doctrings I touched. Would you be cool with a follow-up
branch to clean up docstrings across the project?
> --
>
https://code.launchpad.net/~rackspace-titan/glance/api-results-ordering/+merge/65290
> You are reviewing the proposed merge of
lp:~rackspace-titan/glance/api-results-ordering into lp:glance.

Revision history for this message
Jay Pipes (jaypipes) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/glance'
2--- bin/glance 2011-04-27 18:00:49 +0000
3+++ bin/glance 2011-06-28 13:52:02 +0000
4@@ -194,7 +194,7 @@
5 print "Returned the following metadata for the new image:"
6 for k, v in sorted(image_meta.items()):
7 print " %(k)30s => %(v)s" % locals()
8- except client.ClientConnectionError, e:
9+ except exception.ClientConnectionError, e:
10 host = options.host
11 port = options.port
12 print ("Failed to connect to the Glance API server "
13
14=== modified file 'doc/source/client.rst'
15--- doc/source/client.rst 2011-05-25 20:03:00 +0000
16+++ doc/source/client.rst 2011-06-28 13:52:02 +0000
17@@ -113,7 +113,36 @@
18 c = Client("glance.example.com", 9292)
19
20 filters = {'status': 'saving', 'size_max': (5 * 1024 * 1024 * 1024)}
21- print c.get_images_detailed(filters)
22+ print c.get_images_detailed(filters=filters)
23+
24+Sorting Images Returned via ``get_images()`` and ``get_images_detailed()``
25+--------------------------------------------------------------------------
26+
27+Two parameters are available to sort the list of images returned by
28+these methods.
29+
30+* ``sort_key: KEY``
31+
32+ Images can be ordered by the image attribute ``KEY``. Acceptable values:
33+ ``id``, ``name``, ``status``, ``container_format``, ``disk_format``,
34+ ``created_at`` (default) and ``updated_at``.
35+
36+* ``sort_dir: DIR``
37+
38+ The direction of the sort may be defined by ``DIR``. Accepted values:
39+ ``asc`` for ascending or ``desc`` (default) for descending.
40+
41+The following example will return a list of images sorted alphabetically
42+by name in ascending order.
43+
44+.. code-block:: python
45+
46+ from glance.client import Client
47+
48+ c = Client("glance.example.com", 9292)
49+
50+ print c.get_images(sort_key='name', sort_dir='asc')
51+
52
53 Requesting Detailed Metadata on a Specific Image
54 ------------------------------------------------
55
56=== modified file 'doc/source/glanceapi.rst'
57--- doc/source/glanceapi.rst 2011-05-26 12:53:48 +0000
58+++ doc/source/glanceapi.rst 2011-06-28 13:52:02 +0000
59@@ -129,6 +129,20 @@
60
61 Filters images having a ``size`` attribute less than or equal to ``BYTES``
62
63+These two resources also accept sort parameters:
64+
65+* ``sort_key=KEY``
66+
67+ Results will be ordered by the specified image attribute ``KEY``. Accepted
68+ values include ``id``, ``name``, ``status``, ``disk_format``,
69+ ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``.
70+
71+* ``sort_dir=DIR``
72+
73+ Results will be sorted in the direction ``DIR``. Accepted values are ``asc``
74+ for ascending or ``desc`` (default) for descending.
75+
76+
77 Requesting Detailed Metadata on a Specific Image
78 ------------------------------------------------
79
80
81=== modified file 'doc/source/registries.rst'
82--- doc/source/registries.rst 2011-05-26 12:53:48 +0000
83+++ doc/source/registries.rst 2011-06-28 13:52:02 +0000
84@@ -83,6 +83,20 @@
85
86 Filters images having a ``size`` attribute less than or equal to ``BYTES``
87
88+These two resources also accept sort parameters:
89+
90+* ``sort_key=KEY``
91+
92+ Results will be ordered by the specified image attribute ``KEY``. Accepted
93+ values include ``id``, ``name``, ``status``, ``disk_format``,
94+ ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``.
95+
96+* ``sort_dir=DIR``
97+
98+ Results will be sorted in the direction ``DIR``. Accepted values are ``asc``
99+ for ascending or ``desc`` (default) for descending.
100+
101+
102 ``POST /images``
103 ----------------
104
105
106=== modified file 'glance/api/v1/images.py'
107--- glance/api/v1/images.py 2011-06-27 14:37:40 +0000
108+++ glance/api/v1/images.py 2011-06-28 13:52:02 +0000
109@@ -45,6 +45,8 @@
110 SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
111 'size_min', 'size_max']
112
113+SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
114+
115
116 class Controller(object):
117
118@@ -92,14 +94,7 @@
119 'size': <SIZE>}, ...
120 ]}
121 """
122- params = {'filters': self._get_filters(req)}
123-
124- if 'limit' in req.str_params:
125- params['limit'] = req.str_params.get('limit')
126-
127- if 'marker' in req.str_params:
128- params['marker'] = req.str_params.get('marker')
129-
130+ params = self._get_query_params(req)
131 images = registry.get_images_list(self.options, **params)
132 return dict(images=images)
133
134@@ -125,17 +120,23 @@
135 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
136 ]}
137 """
138- params = {'filters': self._get_filters(req)}
139-
140- if 'limit' in req.str_params:
141- params['limit'] = req.str_params.get('limit')
142-
143- if 'marker' in req.str_params:
144- params['marker'] = req.str_params.get('marker')
145-
146+ params = self._get_query_params(req)
147 images = registry.get_images_detail(self.options, **params)
148 return dict(images=images)
149
150+ def _get_query_params(self, req):
151+ """
152+ Extracts necessary query params from request.
153+
154+ :param req: the WSGI Request object
155+ :retval dict of parameters that can be used by registry client
156+ """
157+ params = {'filters': self._get_filters(req)}
158+ for PARAM in SUPPORTED_PARAMS:
159+ if PARAM in req.str_params:
160+ params[PARAM] = req.str_params.get(PARAM)
161+ return params
162+
163 def _get_filters(self, req):
164 """
165 Return a dictionary of query param filters from the request
166
167=== modified file 'glance/client.py'
168--- glance/client.py 2011-05-31 13:21:16 +0000
169+++ glance/client.py 2011-06-28 13:52:02 +0000
170@@ -19,172 +19,17 @@
171 Client classes for callers of a Glance system
172 """
173
174-import httplib
175 import json
176-import logging
177-import urlparse
178-import socket
179-import sys
180-import urllib
181
182+from glance.api.v1 import images as v1_images
183+from glance.common import client as base_client
184+from glance.common import exception
185 from glance import utils
186-from glance.common import exception
187
188 #TODO(jaypipes) Allow a logger param for client classes
189
190
191-class ClientConnectionError(Exception):
192- """Error resulting from a client connecting to a server"""
193- pass
194-
195-
196-class ImageBodyIterator(object):
197-
198- """
199- A class that acts as an iterator over an image file's
200- chunks of data. This is returned as part of the result
201- tuple from `glance.client.Client.get_image`
202- """
203-
204- CHUNKSIZE = 65536
205-
206- def __init__(self, response):
207- """
208- Constructs the object from an HTTPResponse object
209- """
210- self.response = response
211-
212- def __iter__(self):
213- """
214- Exposes an iterator over the chunks of data in the
215- image file.
216- """
217- while True:
218- chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
219- if chunk:
220- yield chunk
221- else:
222- break
223-
224-
225-class BaseClient(object):
226-
227- """A base client class"""
228-
229- CHUNKSIZE = 65536
230-
231- def __init__(self, host, port, use_ssl):
232- """
233- Creates a new client to some service.
234-
235- :param host: The host where service resides
236- :param port: The port where service resides
237- :param use_ssl: Should we use HTTPS?
238- """
239- self.host = host
240- self.port = port
241- self.use_ssl = use_ssl
242- self.connection = None
243-
244- def get_connection_type(self):
245- """
246- Returns the proper connection type
247- """
248- if self.use_ssl:
249- return httplib.HTTPSConnection
250- else:
251- return httplib.HTTPConnection
252-
253- def do_request(self, method, action, body=None, headers=None,
254- params=None):
255- """
256- Connects to the server and issues a request. Handles converting
257- any returned HTTP error status codes to OpenStack/Glance exceptions
258- and closing the server connection. Returns the result data, or
259- raises an appropriate exception.
260-
261- :param method: HTTP method ("GET", "POST", "PUT", etc...)
262- :param action: part of URL after root netloc
263- :param body: string of data to send, or None (default)
264- :param headers: mapping of key/value pairs to add as headers
265- :param params: dictionary of key/value pairs to add to append
266- to action
267-
268- :note
269-
270- If the body param has a read attribute, and method is either
271- POST or PUT, this method will automatically conduct a chunked-transfer
272- encoding and use the body as a file object, transferring chunks
273- of data using the connection's send() method. This allows large
274- objects to be transferred efficiently without buffering the entire
275- body in memory.
276- """
277- if type(params) is dict:
278- action += '?' + urllib.urlencode(params)
279-
280- try:
281- connection_type = self.get_connection_type()
282- headers = headers or {}
283- c = connection_type(self.host, self.port)
284-
285- # Do a simple request or a chunked request, depending
286- # on whether the body param is a file-like object and
287- # the method is PUT or POST
288- if hasattr(body, 'read') and method.lower() in ('post', 'put'):
289- # Chunk it, baby...
290- c.putrequest(method, action)
291-
292- for header, value in headers.items():
293- c.putheader(header, value)
294- c.putheader('Transfer-Encoding', 'chunked')
295- c.endheaders()
296-
297- chunk = body.read(self.CHUNKSIZE)
298- while chunk:
299- c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
300- chunk = body.read(self.CHUNKSIZE)
301- c.send('0\r\n\r\n')
302- else:
303- # Simple request...
304- c.request(method, action, body, headers)
305- res = c.getresponse()
306- status_code = self.get_status_code(res)
307- if status_code in (httplib.OK,
308- httplib.CREATED,
309- httplib.ACCEPTED,
310- httplib.NO_CONTENT):
311- return res
312- elif status_code == httplib.UNAUTHORIZED:
313- raise exception.NotAuthorized
314- elif status_code == httplib.FORBIDDEN:
315- raise exception.NotAuthorized
316- elif status_code == httplib.NOT_FOUND:
317- raise exception.NotFound
318- elif status_code == httplib.CONFLICT:
319- raise exception.Duplicate(res.read())
320- elif status_code == httplib.BAD_REQUEST:
321- raise exception.Invalid(res.read())
322- elif status_code == httplib.INTERNAL_SERVER_ERROR:
323- raise Exception("Internal Server error: %s" % res.read())
324- else:
325- raise Exception("Unknown error occurred! %s" % res.read())
326-
327- except (socket.error, IOError), e:
328- raise ClientConnectionError("Unable to connect to "
329- "server. Got error: %s" % e)
330-
331- def get_status_code(self, response):
332- """
333- Returns the integer status code from the response, which
334- can be either a Webob.Response (used in testing) or httplib.Response
335- """
336- if hasattr(response, 'status_int'):
337- return response.status_int
338- else:
339- return response.status
340-
341-
342-class V1Client(BaseClient):
343+class V1Client(base_client.BaseClient):
344
345 """Main client class for accessing Glance resources"""
346
347@@ -209,7 +54,7 @@
348 return super(V1Client, self).do_request(method, action, body,
349 headers, params)
350
351- def get_images(self, filters=None, marker=None, limit=None):
352+ def get_images(self, **kwargs):
353 """
354 Returns a list of image id/name mappings from Registry
355
356@@ -217,18 +62,15 @@
357 collection of images should be filtered
358 :param marker: id after which to start the page of images
359 :param limit: maximum number of items to return
360+ :param sort_key: results will be ordered by this image attribute
361+ :param sort_dir: direction in which to to order results (asc, desc)
362 """
363-
364- params = filters or {}
365- if marker:
366- params['marker'] = marker
367- if limit:
368- params['limit'] = limit
369+ params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
370 res = self.do_request("GET", "/images", params=params)
371 data = json.loads(res.read())['images']
372 return data
373
374- def get_images_detailed(self, filters=None, marker=None, limit=None):
375+ def get_images_detailed(self, **kwargs):
376 """
377 Returns a list of detailed image data mappings from Registry
378
379@@ -236,13 +78,11 @@
380 collection of images should be filtered
381 :param marker: id after which to start the page of images
382 :param limit: maximum number of items to return
383+ :param sort_key: results will be ordered by this image attribute
384+ :param sort_dir: direction in which to to order results (asc, desc)
385 """
386
387- params = filters or {}
388- if marker:
389- params['marker'] = marker
390- if limit:
391- params['limit'] = limit
392+ params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
393 res = self.do_request("GET", "/images/detail", params=params)
394 data = json.loads(res.read())['images']
395 return data
396@@ -260,7 +100,7 @@
397 res = self.do_request("GET", "/images/%s" % image_id)
398
399 image = utils.get_image_meta_from_headers(res)
400- return image, ImageBodyIterator(res)
401+ return image, base_client.ImageBodyIterator(res)
402
403 def get_image_meta(self, image_id):
404 """
405
406=== added file 'glance/common/client.py'
407--- glance/common/client.py 1970-01-01 00:00:00 +0000
408+++ glance/common/client.py 2011-06-28 13:52:02 +0000
409@@ -0,0 +1,169 @@
410+import httplib
411+import logging
412+import socket
413+import urllib
414+
415+from glance.common import exception
416+
417+
418+class ImageBodyIterator(object):
419+
420+ """
421+ A class that acts as an iterator over an image file's
422+ chunks of data. This is returned as part of the result
423+ tuple from `glance.client.Client.get_image`
424+ """
425+
426+ CHUNKSIZE = 65536
427+
428+ def __init__(self, response):
429+ """
430+ Constructs the object from an HTTPResponse object
431+ """
432+ self.response = response
433+
434+ def __iter__(self):
435+ """
436+ Exposes an iterator over the chunks of data in the
437+ image file.
438+ """
439+ while True:
440+ chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
441+ if chunk:
442+ yield chunk
443+ else:
444+ break
445+
446+
447+class BaseClient(object):
448+
449+ """A base client class"""
450+
451+ CHUNKSIZE = 65536
452+
453+ def __init__(self, host, port, use_ssl):
454+ """
455+ Creates a new client to some service.
456+
457+ :param host: The host where service resides
458+ :param port: The port where service resides
459+ :param use_ssl: Should we use HTTPS?
460+ """
461+ self.host = host
462+ self.port = port
463+ self.use_ssl = use_ssl
464+ self.connection = None
465+
466+ def get_connection_type(self):
467+ """
468+ Returns the proper connection type
469+ """
470+ if self.use_ssl:
471+ return httplib.HTTPSConnection
472+ else:
473+ return httplib.HTTPConnection
474+
475+ def do_request(self, method, action, body=None, headers=None,
476+ params=None):
477+ """
478+ Connects to the server and issues a request. Handles converting
479+ any returned HTTP error status codes to OpenStack/Glance exceptions
480+ and closing the server connection. Returns the result data, or
481+ raises an appropriate exception.
482+
483+ :param method: HTTP method ("GET", "POST", "PUT", etc...)
484+ :param action: part of URL after root netloc
485+ :param body: string of data to send, or None (default)
486+ :param headers: mapping of key/value pairs to add as headers
487+ :param params: dictionary of key/value pairs to add to append
488+ to action
489+
490+ :note
491+
492+ If the body param has a read attribute, and method is either
493+ POST or PUT, this method will automatically conduct a chunked-transfer
494+ encoding and use the body as a file object, transferring chunks
495+ of data using the connection's send() method. This allows large
496+ objects to be transferred efficiently without buffering the entire
497+ body in memory.
498+ """
499+ if type(params) is dict:
500+ action += '?' + urllib.urlencode(params)
501+
502+ try:
503+ connection_type = self.get_connection_type()
504+ headers = headers or {}
505+ c = connection_type(self.host, self.port)
506+
507+ # Do a simple request or a chunked request, depending
508+ # on whether the body param is a file-like object and
509+ # the method is PUT or POST
510+ if hasattr(body, 'read') and method.lower() in ('post', 'put'):
511+ # Chunk it, baby...
512+ c.putrequest(method, action)
513+
514+ for header, value in headers.items():
515+ c.putheader(header, value)
516+ c.putheader('Transfer-Encoding', 'chunked')
517+ c.endheaders()
518+
519+ chunk = body.read(self.CHUNKSIZE)
520+ while chunk:
521+ c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
522+ chunk = body.read(self.CHUNKSIZE)
523+ c.send('0\r\n\r\n')
524+ else:
525+ # Simple request...
526+ c.request(method, action, body, headers)
527+ res = c.getresponse()
528+ status_code = self.get_status_code(res)
529+ if status_code in (httplib.OK,
530+ httplib.CREATED,
531+ httplib.ACCEPTED,
532+ httplib.NO_CONTENT):
533+ return res
534+ elif status_code == httplib.UNAUTHORIZED:
535+ raise exception.NotAuthorized
536+ elif status_code == httplib.FORBIDDEN:
537+ raise exception.NotAuthorized
538+ elif status_code == httplib.NOT_FOUND:
539+ raise exception.NotFound
540+ elif status_code == httplib.CONFLICT:
541+ raise exception.Duplicate(res.read())
542+ elif status_code == httplib.BAD_REQUEST:
543+ raise exception.Invalid(res.read())
544+ elif status_code == httplib.INTERNAL_SERVER_ERROR:
545+ raise Exception("Internal Server error: %s" % res.read())
546+ else:
547+ raise Exception("Unknown error occurred! %s" % res.read())
548+
549+ except (socket.error, IOError), e:
550+ raise exception.ClientConnectionError("Unable to connect to "
551+ "server. Got error: %s" % e)
552+
553+ def get_status_code(self, response):
554+ """
555+ Returns the integer status code from the response, which
556+ can be either a Webob.Response (used in testing) or httplib.Response
557+ """
558+ if hasattr(response, 'status_int'):
559+ return response.status_int
560+ else:
561+ return response.status
562+
563+ def _extract_params(self, actual_params, allowed_params):
564+ """
565+ Extract a subset of keys from a dictionary. The filters key
566+ will also be extracted, and each of its values will be returned
567+ as an individual param.
568+
569+ :param actual_params: dict of keys to filter
570+ :param allowed_params: list of keys that 'actual_params' will be
571+ reduced to
572+ :retval subset of 'params' dict
573+ """
574+ result = actual_params.get('filters', {})
575+ for allowed_param in allowed_params:
576+ if allowed_param in actual_params:
577+ result[allowed_param] = actual_params[allowed_param]
578+ return result
579
580=== modified file 'glance/common/exception.py'
581--- glance/common/exception.py 2011-06-15 14:47:00 +0000
582+++ glance/common/exception.py 2011-06-28 13:52:02 +0000
583@@ -83,6 +83,11 @@
584 pass
585
586
587+class ClientConnectionError(Exception):
588+ """Error resulting from a client connecting to a server"""
589+ pass
590+
591+
592 def wrap_exception(f):
593 def _wrap(*args, **kw):
594 try:
595
596=== modified file 'glance/registry/client.py'
597--- glance/registry/client.py 2011-06-11 13:47:47 +0000
598+++ glance/registry/client.py 2011-06-28 13:52:02 +0000
599@@ -23,7 +23,8 @@
600 import json
601 import urllib
602
603-from glance.client import BaseClient
604+from glance.common.client import BaseClient
605+from glance.registry import server
606
607
608 class RegistryClient(BaseClient):
609@@ -44,42 +45,32 @@
610 port = port or self.DEFAULT_PORT
611 super(RegistryClient, self).__init__(host, port, use_ssl)
612
613- def get_images(self, filters=None, marker=None, limit=None):
614+ def get_images(self, **kwargs):
615 """
616 Returns a list of image id/name mappings from Registry
617
618 :param filters: dict of keys & expected values to filter results
619 :param marker: image id after which to start page
620 :param limit: max number of images to return
621+ :param sort_key: results will be ordered by this image attribute
622+ :param sort_dir: direction in which to to order results (asc, desc)
623 """
624- params = filters or {}
625-
626- if marker != None:
627- params['marker'] = marker
628-
629- if limit != None:
630- params['limit'] = limit
631-
632+ params = self._extract_params(kwargs, server.SUPPORTED_PARAMS)
633 res = self.do_request("GET", "/images", params=params)
634 data = json.loads(res.read())['images']
635 return data
636
637- def get_images_detailed(self, filters=None, marker=None, limit=None):
638+ def get_images_detailed(self, **kwargs):
639 """
640 Returns a list of detailed image data mappings from Registry
641
642 :param filters: dict of keys & expected values to filter results
643 :param marker: image id after which to start page
644 :param limit: max number of images to return
645+ :param sort_key: results will be ordered by this image attribute
646+ :param sort_dir: direction in which to to order results (asc, desc)
647 """
648- params = filters or {}
649-
650- if marker != None:
651- params['marker'] = marker
652-
653- if limit != None:
654- params['limit'] = limit
655-
656+ params = self._extract_params(kwargs, server.SUPPORTED_PARAMS)
657 res = self.do_request("GET", "/images/detail", params=params)
658 data = json.loads(res.read())['images']
659 return data
660
661=== modified file 'glance/registry/db/api.py'
662--- glance/registry/db/api.py 2011-05-31 19:32:17 +0000
663+++ glance/registry/db/api.py 2011-06-28 13:52:02 +0000
664@@ -23,7 +23,7 @@
665
666 import logging
667
668-from sqlalchemy import create_engine, desc
669+from sqlalchemy import asc, create_engine, desc
670 from sqlalchemy.ext.declarative import declarative_base
671 from sqlalchemy.orm import exc
672 from sqlalchemy.orm import joinedload
673@@ -129,7 +129,8 @@
674 raise exception.NotFound("No image found with ID %s" % image_id)
675
676
677-def image_get_all_public(context, filters=None, marker=None, limit=None):
678+def image_get_all_public(context, filters=None, marker=None, limit=None,
679+ sort_key='created_at', sort_dir='desc'):
680 """Get all public images that match zero or more filters.
681
682 :param filters: dict of filter keys and values. If a 'properties'
683@@ -137,7 +138,8 @@
684 filters on the image properties attribute
685 :param marker: image id after which to start page
686 :param limit: maximum number of images to return
687-
688+ :param sort_key: image attribute by which results should be sorted
689+ :param sort_dir: direction in which results should be sorted (asc, desc)
690 """
691 filters = filters or {}
692
693@@ -146,9 +148,17 @@
694 options(joinedload(models.Image.properties)).\
695 filter_by(deleted=_deleted(context)).\
696 filter_by(is_public=True).\
697- filter(models.Image.status != 'killed').\
698- order_by(desc(models.Image.created_at)).\
699- order_by(desc(models.Image.id))
700+ filter(models.Image.status != 'killed')
701+
702+ sort_dir_func = {
703+ 'asc': asc,
704+ 'desc': desc,
705+ }[sort_dir]
706+
707+ sort_key_attr = getattr(models.Image, sort_key)
708+
709+ query = query.order_by(sort_dir_func(sort_key_attr)).\
710+ order_by(sort_dir_func(models.Image.id))
711
712 if 'size_min' in filters:
713 query = query.filter(models.Image.size >= filters['size_min'])
714
715=== modified file 'glance/registry/server.py'
716--- glance/registry/server.py 2011-06-15 14:47:00 +0000
717+++ glance/registry/server.py 2011-06-28 13:52:02 +0000
718@@ -39,8 +39,15 @@
719 SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
720 'size_min', 'size_max']
721
722+SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
723+ 'size', 'id', 'created_at', 'updated_at')
724+
725+SUPPORTED_SORT_DIRS = ('asc', 'desc')
726+
727 MAX_ITEM_LIMIT = 25
728
729+SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
730+
731
732 class Controller(object):
733 """Controller for the reference implementation registry server"""
734@@ -69,14 +76,7 @@
735 }
736
737 """
738- params = {
739- 'filters': self._get_filters(req),
740- 'limit': self._get_limit(req),
741- }
742-
743- if 'marker' in req.str_params:
744- params['marker'] = self._get_marker(req)
745-
746+ params = self._get_query_params(req)
747 images = db_api.image_get_all_public(None, **params)
748
749 results = []
750@@ -99,19 +99,33 @@
751 all image model fields.
752
753 """
754- params = {
755- 'filters': self._get_filters(req),
756- 'limit': self._get_limit(req),
757- }
758-
759- if 'marker' in req.str_params:
760- params['marker'] = self._get_marker(req)
761-
762+ params = self._get_query_params(req)
763 images = db_api.image_get_all_public(None, **params)
764
765 image_dicts = [make_image_dict(i) for i in images]
766 return dict(images=image_dicts)
767
768+ def _get_query_params(self, req):
769+ """
770+ Extract necessary query parameters from http request.
771+
772+ :param req: the Request object coming from the wsgi layer
773+ :retval dictionary of filters to apply to list of images
774+ """
775+ params = {
776+ 'filters': self._get_filters(req),
777+ 'limit': self._get_limit(req),
778+ 'sort_key': self._get_sort_key(req),
779+ 'sort_dir': self._get_sort_dir(req),
780+ 'marker': self._get_marker(req),
781+ }
782+
783+ for key, value in params.items():
784+ if value is None:
785+ del params[key]
786+
787+ return params
788+
789 def _get_filters(self, req):
790 """Return a dictionary of query param filters from the request
791
792@@ -148,12 +162,35 @@
793
794 def _get_marker(self, req):
795 """Parse a marker query param into something usable."""
796+ marker = req.str_params.get('marker', None)
797+
798+ if marker is None:
799+ return None
800+
801 try:
802- marker = int(req.str_params.get('marker', None))
803+ marker = int(marker)
804 except ValueError:
805 raise exc.HTTPBadRequest("marker param must be an integer")
806 return marker
807
808+ def _get_sort_key(self, req):
809+ """Parse a sort key query param from the request object."""
810+ sort_key = req.str_params.get('sort_key', None)
811+ if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS:
812+ _keys = ', '.join(SUPPORTED_SORT_KEYS)
813+ msg = "Unsupported sort_key. Acceptable values: %s" % (_keys,)
814+ raise exc.HTTPBadRequest(explanation=msg)
815+ return sort_key
816+
817+ def _get_sort_dir(self, req):
818+ """Parse a sort direction query param from the request object."""
819+ sort_dir = req.str_params.get('sort_dir', None)
820+ if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS:
821+ _keys = ', '.join(SUPPORTED_SORT_DIRS)
822+ msg = "Unsupported sort_dir. Acceptable values: %s" % (_keys,)
823+ raise exc.HTTPBadRequest(explanation=msg)
824+ return sort_dir
825+
826 def show(self, req, id):
827 """Return data about the given image id."""
828 try:
829
830=== modified file 'tests/functional/test_curl_api.py'
831--- tests/functional/test_curl_api.py 2011-06-22 14:19:33 +0000
832+++ tests/functional/test_curl_api.py 2011-06-28 13:52:02 +0000
833@@ -1117,3 +1117,118 @@
834 self.assertEqual(int(images[0]['id']), 2)
835
836 self.stop_servers()
837+
838+ def test_ordered_images(self):
839+ """
840+ Set up three test images and ensure each query param filter works
841+ """
842+ self.cleanup()
843+ self.start_servers()
844+
845+ api_port = self.api_port
846+ registry_port = self.registry_port
847+
848+ # 0. GET /images
849+ # Verify no public images
850+ cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
851+
852+ exitcode, out, err = execute(cmd)
853+
854+ self.assertEqual(0, exitcode)
855+ self.assertEqual('{"images": []}', out.strip())
856+
857+ # 1. POST /images with three public images with various attributes
858+ cmd = ("curl -i -X POST "
859+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
860+ "-H 'X-Image-Meta-Name: Image1' "
861+ "-H 'X-Image-Meta-Status: active' "
862+ "-H 'X-Image-Meta-Container-Format: ovf' "
863+ "-H 'X-Image-Meta-Disk-Format: vdi' "
864+ "-H 'X-Image-Meta-Size: 19' "
865+ "-H 'X-Image-Meta-Is-Public: True' "
866+ "http://0.0.0.0:%d/v1/images") % api_port
867+
868+ exitcode, out, err = execute(cmd)
869+ self.assertEqual(0, exitcode)
870+
871+ lines = out.split("\r\n")
872+ status_line = lines[0]
873+
874+ self.assertEqual("HTTP/1.1 201 Created", status_line)
875+
876+ cmd = ("curl -i -X POST "
877+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
878+ "-H 'X-Image-Meta-Name: ASDF' "
879+ "-H 'X-Image-Meta-Status: active' "
880+ "-H 'X-Image-Meta-Container-Format: bare' "
881+ "-H 'X-Image-Meta-Disk-Format: iso' "
882+ "-H 'X-Image-Meta-Size: 2' "
883+ "-H 'X-Image-Meta-Is-Public: True' "
884+ "http://0.0.0.0:%d/v1/images") % api_port
885+
886+ exitcode, out, err = execute(cmd)
887+ self.assertEqual(0, exitcode)
888+
889+ lines = out.split("\r\n")
890+ status_line = lines[0]
891+
892+ self.assertEqual("HTTP/1.1 201 Created", status_line)
893+ cmd = ("curl -i -X POST "
894+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
895+ "-H 'X-Image-Meta-Name: XYZ' "
896+ "-H 'X-Image-Meta-Status: saving' "
897+ "-H 'X-Image-Meta-Container-Format: ami' "
898+ "-H 'X-Image-Meta-Disk-Format: ami' "
899+ "-H 'X-Image-Meta-Size: 5' "
900+ "-H 'X-Image-Meta-Is-Public: True' "
901+ "http://0.0.0.0:%d/v1/images") % api_port
902+
903+ exitcode, out, err = execute(cmd)
904+ self.assertEqual(0, exitcode)
905+
906+ lines = out.split("\r\n")
907+ status_line = lines[0]
908+
909+ self.assertEqual("HTTP/1.1 201 Created", status_line)
910+
911+ # 2. GET /images with no query params
912+ # Verify three public images sorted by created_at desc
913+ cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
914+
915+ exitcode, out, err = execute(cmd)
916+
917+ self.assertEqual(0, exitcode)
918+ images = json.loads(out.strip())['images']
919+
920+ self.assertEqual(len(images), 3)
921+ self.assertEqual(images[0]['id'], 3)
922+ self.assertEqual(images[1]['id'], 2)
923+ self.assertEqual(images[2]['id'], 1)
924+
925+ # 3. GET /images sorted by name asc
926+ params = 'sort_key=name&sort_dir=asc'
927+ cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params)
928+
929+ exitcode, out, err = execute(cmd)
930+
931+ self.assertEqual(0, exitcode)
932+ images = json.loads(out.strip())['images']
933+
934+ self.assertEqual(len(images), 3)
935+ self.assertEqual(images[0]['id'], 2)
936+ self.assertEqual(images[1]['id'], 1)
937+ self.assertEqual(images[2]['id'], 3)
938+
939+ # 4. GET /images sorted by size desc
940+ params = 'sort_key=size&sort_dir=desc'
941+ cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params)
942+
943+ exitcode, out, err = execute(cmd)
944+
945+ self.assertEqual(0, exitcode)
946+ images = json.loads(out.strip())['images']
947+
948+ self.assertEqual(len(images), 3)
949+ self.assertEqual(images[0]['id'], 1)
950+ self.assertEqual(images[1]['id'], 3)
951+ self.assertEqual(images[2]['id'], 2)
952
953=== modified file 'tests/stubs.py'
954--- tests/stubs.py 2011-05-31 19:32:17 +0000
955+++ tests/stubs.py 2011-06-28 13:52:02 +0000
956@@ -28,6 +28,7 @@
957 import stubout
958 import webob
959
960+import glance.common.client
961 from glance.common import exception
962 from glance.registry import server as rserver
963 from glance.api import v1 as server
964@@ -254,9 +255,9 @@
965 for i in self.response.app_iter:
966 yield i
967
968- stubs.Set(glance.client.BaseClient, 'get_connection_type',
969+ stubs.Set(glance.common.client.BaseClient, 'get_connection_type',
970 fake_get_connection_type)
971- stubs.Set(glance.client.ImageBodyIterator, '__iter__',
972+ stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
973 fake_image_iter)
974
975
976@@ -388,8 +389,8 @@
977 else:
978 return images[0]
979
980- def image_get_all_public(self, _context, filters=None,
981- marker=None, limit=1000):
982+ def image_get_all_public(self, _context, filters=None, marker=None,
983+ limit=1000, sort_key=None, sort_dir=None):
984 images = [f for f in self.images if f['is_public'] == True]
985
986 if 'size_min' in filters:
987@@ -414,16 +415,24 @@
988 for k, v in filters.items():
989 images = [f for f in images if f[k] == v]
990
991+ # sorted func expects func that compares in descending order
992 def image_cmp(x, y):
993- if x['created_at'] > y['created_at']:
994- return 1
995- elif x['created_at'] == y['created_at']:
996+ _sort_dir = sort_dir or 'desc'
997+ multiplier = {
998+ 'asc': -1,
999+ 'desc': 1,
1000+ }[_sort_dir]
1001+
1002+ _sort_key = sort_key or 'created_at'
1003+ if x[_sort_key] > y[_sort_key]:
1004+ return 1 * multiplier
1005+ elif x[_sort_key] == y[_sort_key]:
1006 if x['id'] > y['id']:
1007- return 1
1008+ return 1 * multiplier
1009 else:
1010- return -1
1011+ return -1 * multiplier
1012 else:
1013- return -1
1014+ return -1 * multiplier
1015
1016 images = sorted(images, cmp=image_cmp)
1017 images.reverse()
1018
1019=== modified file 'tests/unit/test_api.py'
1020--- tests/unit/test_api.py 2011-06-24 20:57:02 +0000
1021+++ tests/unit/test_api.py 2011-06-28 13:52:02 +0000
1022@@ -285,6 +285,398 @@
1023 for image in images:
1024 self.assertEqual('new name! #123', image['name'])
1025
1026+ def test_get_index_sort_default_created_at_desc(self):
1027+ """
1028+ Tests that the /images registry API returns list of
1029+ public images that conforms to a default sort key/dir
1030+ """
1031+ time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
1032+ time2 = datetime.datetime.utcnow()
1033+
1034+ extra_fixture = {'id': 3,
1035+ 'status': 'active',
1036+ 'is_public': True,
1037+ 'disk_format': 'vhd',
1038+ 'container_format': 'ovf',
1039+ 'name': 'new name! #123',
1040+ 'size': 19,
1041+ 'checksum': None,
1042+ 'created_at': time1}
1043+
1044+ glance.registry.db.api.image_create(None, extra_fixture)
1045+
1046+ extra_fixture = {'id': 4,
1047+ 'status': 'active',
1048+ 'is_public': True,
1049+ 'disk_format': 'vhd',
1050+ 'container_format': 'ovf',
1051+ 'name': 'new name! #123',
1052+ 'size': 20,
1053+ 'checksum': None,
1054+ 'created_at': time1}
1055+
1056+ glance.registry.db.api.image_create(None, extra_fixture)
1057+
1058+ extra_fixture = {'id': 5,
1059+ 'status': 'active',
1060+ 'is_public': True,
1061+ 'disk_format': 'vhd',
1062+ 'container_format': 'ovf',
1063+ 'name': 'new name! #123',
1064+ 'size': 20,
1065+ 'checksum': None,
1066+ 'created_at': time2}
1067+
1068+ glance.registry.db.api.image_create(None, extra_fixture)
1069+
1070+ req = webob.Request.blank('/images')
1071+ res = req.get_response(self.api)
1072+ res_dict = json.loads(res.body)
1073+ self.assertEquals(res.status_int, 200)
1074+
1075+ images = res_dict['images']
1076+ self.assertEquals(len(images), 4)
1077+ self.assertEquals(int(images[0]['id']), 4)
1078+ self.assertEquals(int(images[1]['id']), 3)
1079+ self.assertEquals(int(images[2]['id']), 5)
1080+ self.assertEquals(int(images[3]['id']), 2)
1081+
1082+ def test_get_index_bad_sort_key(self):
1083+ """Ensure a 400 is returned when a bad sort_key is provided."""
1084+ req = webob.Request.blank('/images?sort_key=asdf')
1085+ res = req.get_response(self.api)
1086+ self.assertEqual(400, res.status_int)
1087+
1088+ def test_get_index_bad_sort_dir(self):
1089+ """Ensure a 400 is returned when a bad sort_dir is provided."""
1090+ req = webob.Request.blank('/images?sort_dir=asdf')
1091+ res = req.get_response(self.api)
1092+ self.assertEqual(400, res.status_int)
1093+
1094+ def test_get_index_sort_id_desc(self):
1095+ """
1096+ Tests that the /images registry API returns list of
1097+ public images sorted by id in descending order.
1098+ """
1099+ extra_fixture = {'id': 3,
1100+ 'status': 'active',
1101+ 'is_public': True,
1102+ 'disk_format': 'vhd',
1103+ 'container_format': 'ovf',
1104+ 'name': 'asdf',
1105+ 'size': 19,
1106+ 'checksum': None}
1107+
1108+ glance.registry.db.api.image_create(None, extra_fixture)
1109+
1110+ extra_fixture = {'id': 4,
1111+ 'status': 'active',
1112+ 'is_public': True,
1113+ 'disk_format': 'vhd',
1114+ 'container_format': 'ovf',
1115+ 'name': 'xyz',
1116+ 'size': 20,
1117+ 'checksum': None}
1118+
1119+ glance.registry.db.api.image_create(None, extra_fixture)
1120+
1121+ req = webob.Request.blank('/images?sort_key=id&sort_dir=desc')
1122+ res = req.get_response(self.api)
1123+ self.assertEquals(res.status_int, 200)
1124+ res_dict = json.loads(res.body)
1125+
1126+ images = res_dict['images']
1127+ self.assertEquals(len(images), 3)
1128+ self.assertEquals(int(images[0]['id']), 4)
1129+ self.assertEquals(int(images[1]['id']), 3)
1130+ self.assertEquals(int(images[2]['id']), 2)
1131+
1132+ def test_get_index_sort_name_asc(self):
1133+ """
1134+ Tests that the /images registry API returns list of
1135+ public images sorted alphabetically by name in
1136+ ascending order.
1137+ """
1138+ extra_fixture = {'id': 3,
1139+ 'status': 'active',
1140+ 'is_public': True,
1141+ 'disk_format': 'vhd',
1142+ 'container_format': 'ovf',
1143+ 'name': 'asdf',
1144+ 'size': 19,
1145+ 'checksum': None}
1146+
1147+ glance.registry.db.api.image_create(None, extra_fixture)
1148+
1149+ extra_fixture = {'id': 4,
1150+ 'status': 'active',
1151+ 'is_public': True,
1152+ 'disk_format': 'vhd',
1153+ 'container_format': 'ovf',
1154+ 'name': 'xyz',
1155+ 'size': 20,
1156+ 'checksum': None}
1157+
1158+ glance.registry.db.api.image_create(None, extra_fixture)
1159+
1160+ req = webob.Request.blank('/images?sort_key=name&sort_dir=asc')
1161+ res = req.get_response(self.api)
1162+ self.assertEquals(res.status_int, 200)
1163+ res_dict = json.loads(res.body)
1164+
1165+ images = res_dict['images']
1166+ self.assertEquals(len(images), 3)
1167+ self.assertEquals(int(images[0]['id']), 3)
1168+ self.assertEquals(int(images[1]['id']), 2)
1169+ self.assertEquals(int(images[2]['id']), 4)
1170+
1171+ def test_get_index_sort_status_desc(self):
1172+ """
1173+ Tests that the /images registry API returns list of
1174+ public images sorted alphabetically by status in
1175+ descending order.
1176+ """
1177+ extra_fixture = {'id': 3,
1178+ 'status': 'killed',
1179+ 'is_public': True,
1180+ 'disk_format': 'vhd',
1181+ 'container_format': 'ovf',
1182+ 'name': 'asdf',
1183+ 'size': 19,
1184+ 'checksum': None}
1185+
1186+ glance.registry.db.api.image_create(None, extra_fixture)
1187+
1188+ extra_fixture = {'id': 4,
1189+ 'status': 'active',
1190+ 'is_public': True,
1191+ 'disk_format': 'vhd',
1192+ 'container_format': 'ovf',
1193+ 'name': 'xyz',
1194+ 'size': 20,
1195+ 'checksum': None}
1196+
1197+ glance.registry.db.api.image_create(None, extra_fixture)
1198+
1199+ req = webob.Request.blank('/images?sort_key=status&sort_dir=desc')
1200+ res = req.get_response(self.api)
1201+ self.assertEquals(res.status_int, 200)
1202+ res_dict = json.loads(res.body)
1203+
1204+ images = res_dict['images']
1205+ self.assertEquals(len(images), 3)
1206+ self.assertEquals(int(images[0]['id']), 3)
1207+ self.assertEquals(int(images[1]['id']), 4)
1208+ self.assertEquals(int(images[2]['id']), 2)
1209+
1210+ def test_get_index_sort_disk_format_asc(self):
1211+ """
1212+ Tests that the /images registry API returns list of
1213+ public images sorted alphabetically by disk_format in
1214+ ascending order.
1215+ """
1216+ extra_fixture = {'id': 3,
1217+ 'status': 'active',
1218+ 'is_public': True,
1219+ 'disk_format': 'ami',
1220+ 'container_format': 'ami',
1221+ 'name': 'asdf',
1222+ 'size': 19,
1223+ 'checksum': None}
1224+
1225+ glance.registry.db.api.image_create(None, extra_fixture)
1226+
1227+ extra_fixture = {'id': 4,
1228+ 'status': 'active',
1229+ 'is_public': True,
1230+ 'disk_format': 'vdi',
1231+ 'container_format': 'ovf',
1232+ 'name': 'xyz',
1233+ 'size': 20,
1234+ 'checksum': None}
1235+
1236+ glance.registry.db.api.image_create(None, extra_fixture)
1237+
1238+ req = webob.Request.blank('/images?sort_key=disk_format&sort_dir=asc')
1239+ res = req.get_response(self.api)
1240+ self.assertEquals(res.status_int, 200)
1241+ res_dict = json.loads(res.body)
1242+
1243+ images = res_dict['images']
1244+ self.assertEquals(len(images), 3)
1245+ self.assertEquals(int(images[0]['id']), 3)
1246+ self.assertEquals(int(images[1]['id']), 4)
1247+ self.assertEquals(int(images[2]['id']), 2)
1248+
1249+ def test_get_index_sort_container_format_desc(self):
1250+ """
1251+ Tests that the /images registry API returns list of
1252+ public images sorted alphabetically by container_format in
1253+ descending order.
1254+ """
1255+ extra_fixture = {'id': 3,
1256+ 'status': 'active',
1257+ 'is_public': True,
1258+ 'disk_format': 'ami',
1259+ 'container_format': 'ami',
1260+ 'name': 'asdf',
1261+ 'size': 19,
1262+ 'checksum': None}
1263+
1264+ glance.registry.db.api.image_create(None, extra_fixture)
1265+
1266+ extra_fixture = {'id': 4,
1267+ 'status': 'active',
1268+ 'is_public': True,
1269+ 'disk_format': 'iso',
1270+ 'container_format': 'bare',
1271+ 'name': 'xyz',
1272+ 'size': 20,
1273+ 'checksum': None}
1274+
1275+ glance.registry.db.api.image_create(None, extra_fixture)
1276+
1277+ url = '/images?sort_key=container_format&sort_dir=desc'
1278+ req = webob.Request.blank(url)
1279+ res = req.get_response(self.api)
1280+ self.assertEquals(res.status_int, 200)
1281+ res_dict = json.loads(res.body)
1282+
1283+ images = res_dict['images']
1284+ self.assertEquals(len(images), 3)
1285+ self.assertEquals(int(images[0]['id']), 2)
1286+ self.assertEquals(int(images[1]['id']), 4)
1287+ self.assertEquals(int(images[2]['id']), 3)
1288+
1289+ def test_get_index_sort_size_asc(self):
1290+ """
1291+ Tests that the /images registry API returns list of
1292+ public images sorted by size in ascending order.
1293+ """
1294+ extra_fixture = {'id': 3,
1295+ 'status': 'active',
1296+ 'is_public': True,
1297+ 'disk_format': 'ami',
1298+ 'container_format': 'ami',
1299+ 'name': 'asdf',
1300+ 'size': 100,
1301+ 'checksum': None}
1302+
1303+ glance.registry.db.api.image_create(None, extra_fixture)
1304+
1305+ extra_fixture = {'id': 4,
1306+ 'status': 'active',
1307+ 'is_public': True,
1308+ 'disk_format': 'iso',
1309+ 'container_format': 'bare',
1310+ 'name': 'xyz',
1311+ 'size': 2,
1312+ 'checksum': None}
1313+
1314+ glance.registry.db.api.image_create(None, extra_fixture)
1315+
1316+ url = '/images?sort_key=size&sort_dir=asc'
1317+ req = webob.Request.blank(url)
1318+ res = req.get_response(self.api)
1319+ self.assertEquals(res.status_int, 200)
1320+ res_dict = json.loads(res.body)
1321+
1322+ images = res_dict['images']
1323+ self.assertEquals(len(images), 3)
1324+ self.assertEquals(int(images[0]['id']), 4)
1325+ self.assertEquals(int(images[1]['id']), 2)
1326+ self.assertEquals(int(images[2]['id']), 3)
1327+
1328+ def test_get_index_sort_created_at_asc(self):
1329+ """
1330+ Tests that the /images registry API returns list of
1331+ public images sorted by created_at in ascending order.
1332+ """
1333+ now = datetime.datetime.utcnow()
1334+ time1 = now + datetime.timedelta(seconds=5)
1335+ time2 = now
1336+
1337+ extra_fixture = {'id': 3,
1338+ 'status': 'active',
1339+ 'is_public': True,
1340+ 'disk_format': 'vhd',
1341+ 'container_format': 'ovf',
1342+ 'name': 'new name! #123',
1343+ 'size': 19,
1344+ 'checksum': None,
1345+ 'created_at': time1}
1346+
1347+ glance.registry.db.api.image_create(None, extra_fixture)
1348+
1349+ extra_fixture = {'id': 4,
1350+ 'status': 'active',
1351+ 'is_public': True,
1352+ 'disk_format': 'vhd',
1353+ 'container_format': 'ovf',
1354+ 'name': 'new name! #123',
1355+ 'size': 20,
1356+ 'checksum': None,
1357+ 'created_at': time2}
1358+
1359+ glance.registry.db.api.image_create(None, extra_fixture)
1360+
1361+ req = webob.Request.blank('/images?sort_key=created_at&sort_dir=asc')
1362+ res = req.get_response(self.api)
1363+ self.assertEquals(res.status_int, 200)
1364+ res_dict = json.loads(res.body)
1365+
1366+ images = res_dict['images']
1367+ self.assertEquals(len(images), 3)
1368+ self.assertEquals(int(images[0]['id']), 2)
1369+ self.assertEquals(int(images[1]['id']), 4)
1370+ self.assertEquals(int(images[2]['id']), 3)
1371+
1372+ def test_get_index_sort_updated_at_desc(self):
1373+ """
1374+ Tests that the /images registry API returns list of
1375+ public images sorted by updated_at in descending order.
1376+ """
1377+ now = datetime.datetime.utcnow()
1378+ time1 = now + datetime.timedelta(seconds=5)
1379+ time2 = now
1380+
1381+ extra_fixture = {'id': 3,
1382+ 'status': 'active',
1383+ 'is_public': True,
1384+ 'disk_format': 'vhd',
1385+ 'container_format': 'ovf',
1386+ 'name': 'new name! #123',
1387+ 'size': 19,
1388+ 'checksum': None,
1389+ 'created_at': None,
1390+ 'created_at': time1}
1391+
1392+ glance.registry.db.api.image_create(None, extra_fixture)
1393+
1394+ extra_fixture = {'id': 4,
1395+ 'status': 'active',
1396+ 'is_public': True,
1397+ 'disk_format': 'vhd',
1398+ 'container_format': 'ovf',
1399+ 'name': 'new name! #123',
1400+ 'size': 20,
1401+ 'checksum': None,
1402+ 'created_at': None,
1403+ 'updated_at': time2}
1404+
1405+ glance.registry.db.api.image_create(None, extra_fixture)
1406+
1407+ req = webob.Request.blank('/images?sort_key=updated_at&sort_dir=desc')
1408+ res = req.get_response(self.api)
1409+ self.assertEquals(res.status_int, 200)
1410+ res_dict = json.loads(res.body)
1411+
1412+ images = res_dict['images']
1413+ self.assertEquals(len(images), 3)
1414+ self.assertEquals(int(images[0]['id']), 3)
1415+ self.assertEquals(int(images[1]['id']), 4)
1416+ self.assertEquals(int(images[2]['id']), 2)
1417+
1418 def test_get_details(self):
1419 """Tests that the /images/detail registry API returns
1420 a mapping containing a list of detailed image information
1421@@ -668,6 +1060,45 @@
1422 for image in images:
1423 self.assertEqual('v a', image['properties']['prop_123'])
1424
1425+ def test_get_details_sort_name_asc(self):
1426+ """
1427+ Tests that the /images/details registry API returns list of
1428+ public images sorted alphabetically by name in
1429+ ascending order.
1430+ """
1431+ extra_fixture = {'id': 3,
1432+ 'status': 'active',
1433+ 'is_public': True,
1434+ 'disk_format': 'vhd',
1435+ 'container_format': 'ovf',
1436+ 'name': 'asdf',
1437+ 'size': 19,
1438+ 'checksum': None}
1439+
1440+ glance.registry.db.api.image_create(None, extra_fixture)
1441+
1442+ extra_fixture = {'id': 4,
1443+ 'status': 'active',
1444+ 'is_public': True,
1445+ 'disk_format': 'vhd',
1446+ 'container_format': 'ovf',
1447+ 'name': 'xyz',
1448+ 'size': 20,
1449+ 'checksum': None}
1450+
1451+ glance.registry.db.api.image_create(None, extra_fixture)
1452+
1453+ req = webob.Request.blank('/images/detail?sort_key=name&sort_dir=asc')
1454+ res = req.get_response(self.api)
1455+ self.assertEquals(res.status_int, 200)
1456+ res_dict = json.loads(res.body)
1457+
1458+ images = res_dict['images']
1459+ self.assertEquals(len(images), 3)
1460+ self.assertEquals(int(images[0]['id']), 3)
1461+ self.assertEquals(int(images[1]['id']), 2)
1462+ self.assertEquals(int(images[2]['id']), 4)
1463+
1464 def test_create_image(self):
1465 """Tests that the /images POST registry API creates the image"""
1466 fixture = {'name': 'fake public image',
1467@@ -1042,6 +1473,45 @@
1468 "res.headerlist = %r" % res.headerlist)
1469 self.assertTrue('/images/3' in res.headers['location'])
1470
1471+ def test_get_index_sort_name_asc(self):
1472+ """
1473+ Tests that the /images registry API returns list of
1474+ public images sorted alphabetically by name in
1475+ ascending order.
1476+ """
1477+ extra_fixture = {'id': 3,
1478+ 'status': 'active',
1479+ 'is_public': True,
1480+ 'disk_format': 'vhd',
1481+ 'container_format': 'ovf',
1482+ 'name': 'asdf',
1483+ 'size': 19,
1484+ 'checksum': None}
1485+
1486+ glance.registry.db.api.image_create(None, extra_fixture)
1487+
1488+ extra_fixture = {'id': 4,
1489+ 'status': 'active',
1490+ 'is_public': True,
1491+ 'disk_format': 'vhd',
1492+ 'container_format': 'ovf',
1493+ 'name': 'xyz',
1494+ 'size': 20,
1495+ 'checksum': None}
1496+
1497+ glance.registry.db.api.image_create(None, extra_fixture)
1498+
1499+ req = webob.Request.blank('/images?sort_key=name&sort_dir=asc')
1500+ res = req.get_response(self.api)
1501+ self.assertEquals(res.status_int, 200)
1502+ res_dict = json.loads(res.body)
1503+
1504+ images = res_dict['images']
1505+ self.assertEquals(len(images), 3)
1506+ self.assertEquals(int(images[0]['id']), 3)
1507+ self.assertEquals(int(images[1]['id']), 2)
1508+ self.assertEquals(int(images[2]['id']), 4)
1509+
1510 def test_image_is_checksummed(self):
1511 """Test that the image contents are checksummed properly"""
1512 fixture_headers = {'x-image-meta-store': 'file',
1513
1514=== modified file 'tests/unit/test_clients.py'
1515--- tests/unit/test_clients.py 2011-05-31 19:32:17 +0000
1516+++ tests/unit/test_clients.py 2011-06-28 13:52:02 +0000
1517@@ -15,6 +15,7 @@
1518 # License for the specific language governing permissions and limitations
1519 # under the License.
1520
1521+import datetime
1522 import json
1523 import os
1524 import stubout
1525@@ -37,7 +38,7 @@
1526 def test_bad_address(self):
1527 """Test ClientConnectionError raised"""
1528 c = client.Client("127.999.1.1")
1529- self.assertRaises(client.ClientConnectionError,
1530+ self.assertRaises(exception.ClientConnectionError,
1531 c.get_image,
1532 1)
1533
1534@@ -70,6 +71,298 @@
1535 for k, v in fixture.items():
1536 self.assertEquals(v, images[0][k])
1537
1538+ def test_get_index_sort_id_desc(self):
1539+ """
1540+ Tests that the /images registry API returns list of
1541+ public images sorted by id in descending order.
1542+ """
1543+ extra_fixture = {'id': 3,
1544+ 'status': 'active',
1545+ 'is_public': True,
1546+ 'disk_format': 'vhd',
1547+ 'container_format': 'ovf',
1548+ 'name': 'asdf',
1549+ 'size': 19,
1550+ 'checksum': None}
1551+
1552+ glance.registry.db.api.image_create(None, extra_fixture)
1553+
1554+ extra_fixture = {'id': 4,
1555+ 'status': 'active',
1556+ 'is_public': True,
1557+ 'disk_format': 'vhd',
1558+ 'container_format': 'ovf',
1559+ 'name': 'xyz',
1560+ 'size': 20,
1561+ 'checksum': None}
1562+
1563+ glance.registry.db.api.image_create(None, extra_fixture)
1564+
1565+ images = self.client.get_images(sort_key='id', sort_dir='desc')
1566+
1567+ self.assertEquals(len(images), 3)
1568+ self.assertEquals(int(images[0]['id']), 4)
1569+ self.assertEquals(int(images[1]['id']), 3)
1570+ self.assertEquals(int(images[2]['id']), 2)
1571+
1572+ def test_get_index_sort_name_asc(self):
1573+ """
1574+ Tests that the /images registry API returns list of
1575+ public images sorted alphabetically by name in
1576+ ascending order.
1577+ """
1578+ extra_fixture = {'id': 3,
1579+ 'status': 'active',
1580+ 'is_public': True,
1581+ 'disk_format': 'vhd',
1582+ 'container_format': 'ovf',
1583+ 'name': 'asdf',
1584+ 'size': 19,
1585+ 'checksum': None}
1586+
1587+ glance.registry.db.api.image_create(None, extra_fixture)
1588+
1589+ extra_fixture = {'id': 4,
1590+ 'status': 'active',
1591+ 'is_public': True,
1592+ 'disk_format': 'vhd',
1593+ 'container_format': 'ovf',
1594+ 'name': 'xyz',
1595+ 'size': 20,
1596+ 'checksum': None}
1597+
1598+ glance.registry.db.api.image_create(None, extra_fixture)
1599+
1600+ images = self.client.get_images(sort_key='name', sort_dir='asc')
1601+
1602+ self.assertEquals(len(images), 3)
1603+ self.assertEquals(int(images[0]['id']), 3)
1604+ self.assertEquals(int(images[1]['id']), 2)
1605+ self.assertEquals(int(images[2]['id']), 4)
1606+
1607+ def test_get_index_sort_status_desc(self):
1608+ """
1609+ Tests that the /images registry API returns list of
1610+ public images sorted alphabetically by status in
1611+ descending order.
1612+ """
1613+ extra_fixture = {'id': 3,
1614+ 'status': 'killed',
1615+ 'is_public': True,
1616+ 'disk_format': 'vhd',
1617+ 'container_format': 'ovf',
1618+ 'name': 'asdf',
1619+ 'size': 19,
1620+ 'checksum': None}
1621+
1622+ glance.registry.db.api.image_create(None, extra_fixture)
1623+
1624+ extra_fixture = {'id': 4,
1625+ 'status': 'active',
1626+ 'is_public': True,
1627+ 'disk_format': 'vhd',
1628+ 'container_format': 'ovf',
1629+ 'name': 'xyz',
1630+ 'size': 20,
1631+ 'checksum': None}
1632+
1633+ glance.registry.db.api.image_create(None, extra_fixture)
1634+
1635+ images = self.client.get_images(sort_key='status', sort_dir='desc')
1636+
1637+ self.assertEquals(len(images), 3)
1638+ self.assertEquals(int(images[0]['id']), 3)
1639+ self.assertEquals(int(images[1]['id']), 4)
1640+ self.assertEquals(int(images[2]['id']), 2)
1641+
1642+ def test_get_index_sort_disk_format_asc(self):
1643+ """
1644+ Tests that the /images registry API returns list of
1645+ public images sorted alphabetically by disk_format in
1646+ ascending order.
1647+ """
1648+ extra_fixture = {'id': 3,
1649+ 'status': 'active',
1650+ 'is_public': True,
1651+ 'disk_format': 'ami',
1652+ 'container_format': 'ami',
1653+ 'name': 'asdf',
1654+ 'size': 19,
1655+ 'checksum': None}
1656+
1657+ glance.registry.db.api.image_create(None, extra_fixture)
1658+
1659+ extra_fixture = {'id': 4,
1660+ 'status': 'active',
1661+ 'is_public': True,
1662+ 'disk_format': 'vdi',
1663+ 'container_format': 'ovf',
1664+ 'name': 'xyz',
1665+ 'size': 20,
1666+ 'checksum': None}
1667+
1668+ glance.registry.db.api.image_create(None, extra_fixture)
1669+
1670+ images = self.client.get_images(sort_key='disk_format',
1671+ sort_dir='asc')
1672+
1673+ self.assertEquals(len(images), 3)
1674+ self.assertEquals(int(images[0]['id']), 3)
1675+ self.assertEquals(int(images[1]['id']), 4)
1676+ self.assertEquals(int(images[2]['id']), 2)
1677+
1678+ def test_get_index_sort_container_format_desc(self):
1679+ """
1680+ Tests that the /images registry API returns list of
1681+ public images sorted alphabetically by container_format in
1682+ descending order.
1683+ """
1684+ extra_fixture = {'id': 3,
1685+ 'status': 'active',
1686+ 'is_public': True,
1687+ 'disk_format': 'ami',
1688+ 'container_format': 'ami',
1689+ 'name': 'asdf',
1690+ 'size': 19,
1691+ 'checksum': None}
1692+
1693+ glance.registry.db.api.image_create(None, extra_fixture)
1694+
1695+ extra_fixture = {'id': 4,
1696+ 'status': 'active',
1697+ 'is_public': True,
1698+ 'disk_format': 'iso',
1699+ 'container_format': 'bare',
1700+ 'name': 'xyz',
1701+ 'size': 20,
1702+ 'checksum': None}
1703+
1704+ glance.registry.db.api.image_create(None, extra_fixture)
1705+
1706+ images = self.client.get_images(sort_key='container_format',
1707+ sort_dir='desc')
1708+
1709+ self.assertEquals(len(images), 3)
1710+ self.assertEquals(int(images[0]['id']), 2)
1711+ self.assertEquals(int(images[1]['id']), 4)
1712+ self.assertEquals(int(images[2]['id']), 3)
1713+
1714+ def test_get_index_sort_size_asc(self):
1715+ """
1716+ Tests that the /images registry API returns list of
1717+ public images sorted by size in ascending order.
1718+ """
1719+ extra_fixture = {'id': 3,
1720+ 'status': 'active',
1721+ 'is_public': True,
1722+ 'disk_format': 'ami',
1723+ 'container_format': 'ami',
1724+ 'name': 'asdf',
1725+ 'size': 100,
1726+ 'checksum': None}
1727+
1728+ glance.registry.db.api.image_create(None, extra_fixture)
1729+
1730+ extra_fixture = {'id': 4,
1731+ 'status': 'active',
1732+ 'is_public': True,
1733+ 'disk_format': 'iso',
1734+ 'container_format': 'bare',
1735+ 'name': 'xyz',
1736+ 'size': 2,
1737+ 'checksum': None}
1738+
1739+ glance.registry.db.api.image_create(None, extra_fixture)
1740+
1741+ images = self.client.get_images(sort_key='size', sort_dir='asc')
1742+
1743+ self.assertEquals(len(images), 3)
1744+ self.assertEquals(int(images[0]['id']), 4)
1745+ self.assertEquals(int(images[1]['id']), 2)
1746+ self.assertEquals(int(images[2]['id']), 3)
1747+
1748+ def test_get_index_sort_created_at_asc(self):
1749+ """
1750+ Tests that the /images registry API returns list of
1751+ public images sorted by created_at in ascending order.
1752+ """
1753+ now = datetime.datetime.utcnow()
1754+ time1 = now + datetime.timedelta(seconds=5)
1755+ time2 = now
1756+
1757+ extra_fixture = {'id': 3,
1758+ 'status': 'active',
1759+ 'is_public': True,
1760+ 'disk_format': 'vhd',
1761+ 'container_format': 'ovf',
1762+ 'name': 'new name! #123',
1763+ 'size': 19,
1764+ 'checksum': None,
1765+ 'created_at': time1}
1766+
1767+ glance.registry.db.api.image_create(None, extra_fixture)
1768+
1769+ extra_fixture = {'id': 4,
1770+ 'status': 'active',
1771+ 'is_public': True,
1772+ 'disk_format': 'vhd',
1773+ 'container_format': 'ovf',
1774+ 'name': 'new name! #123',
1775+ 'size': 20,
1776+ 'checksum': None,
1777+ 'created_at': time2}
1778+
1779+ glance.registry.db.api.image_create(None, extra_fixture)
1780+
1781+ images = self.client.get_images(sort_key='created_at', sort_dir='asc')
1782+
1783+ self.assertEquals(len(images), 3)
1784+ self.assertEquals(int(images[0]['id']), 2)
1785+ self.assertEquals(int(images[1]['id']), 4)
1786+ self.assertEquals(int(images[2]['id']), 3)
1787+
1788+ def test_get_index_sort_updated_at_desc(self):
1789+ """
1790+ Tests that the /images registry API returns list of
1791+ public images sorted by updated_at in descending order.
1792+ """
1793+ now = datetime.datetime.utcnow()
1794+ time1 = now + datetime.timedelta(seconds=5)
1795+ time2 = now
1796+
1797+ extra_fixture = {'id': 3,
1798+ 'status': 'active',
1799+ 'is_public': True,
1800+ 'disk_format': 'vhd',
1801+ 'container_format': 'ovf',
1802+ 'name': 'new name! #123',
1803+ 'size': 19,
1804+ 'checksum': None,
1805+ 'created_at': None,
1806+ 'created_at': time1}
1807+
1808+ glance.registry.db.api.image_create(None, extra_fixture)
1809+
1810+ extra_fixture = {'id': 4,
1811+ 'status': 'active',
1812+ 'is_public': True,
1813+ 'disk_format': 'vhd',
1814+ 'container_format': 'ovf',
1815+ 'name': 'new name! #123',
1816+ 'size': 20,
1817+ 'checksum': None,
1818+ 'created_at': None,
1819+ 'updated_at': time2}
1820+
1821+ glance.registry.db.api.image_create(None, extra_fixture)
1822+
1823+ images = self.client.get_images(sort_key='updated_at', sort_dir='desc')
1824+
1825+ self.assertEquals(len(images), 3)
1826+ self.assertEquals(int(images[0]['id']), 3)
1827+ self.assertEquals(int(images[1]['id']), 4)
1828+ self.assertEquals(int(images[2]['id']), 2)
1829+
1830 def test_get_image_index_marker(self):
1831 """Test correct set of images returned with marker param."""
1832 extra_fixture = {'id': 3,
1833@@ -170,7 +463,7 @@
1834
1835 glance.registry.db.api.image_create(None, extra_fixture)
1836
1837- images = self.client.get_images({'name': 'new name! #123'})
1838+ images = self.client.get_images(filters={'name': 'new name! #123'})
1839 self.assertEquals(len(images), 1)
1840
1841 for image in images:
1842@@ -236,7 +529,8 @@
1843
1844 glance.registry.db.api.image_create(None, extra_fixture)
1845
1846- images = self.client.get_images_detailed({'name': 'new name! #123'})
1847+ filters = {'name': 'new name! #123'}
1848+ images = self.client.get_images_detailed(filters=filters)
1849 self.assertEquals(len(images), 1)
1850
1851 for image in images:
1852@@ -255,7 +549,7 @@
1853
1854 glance.registry.db.api.image_create(None, extra_fixture)
1855
1856- images = self.client.get_images_detailed({'status': 'saving'})
1857+ images = self.client.get_images_detailed(filters={'status': 'saving'})
1858 self.assertEquals(len(images), 1)
1859
1860 for image in images:
1861@@ -274,7 +568,8 @@
1862
1863 glance.registry.db.api.image_create(None, extra_fixture)
1864
1865- images = self.client.get_images_detailed({'container_format': 'ovf'})
1866+ filters = {'container_format': 'ovf'}
1867+ images = self.client.get_images_detailed(filters=filters)
1868 self.assertEquals(len(images), 2)
1869
1870 for image in images:
1871@@ -293,7 +588,8 @@
1872
1873 glance.registry.db.api.image_create(None, extra_fixture)
1874
1875- images = self.client.get_images_detailed({'disk_format': 'vhd'})
1876+ filters = {'disk_format': 'vhd'}
1877+ images = self.client.get_images_detailed(filters=filters)
1878 self.assertEquals(len(images), 2)
1879
1880 for image in images:
1881@@ -312,7 +608,7 @@
1882
1883 glance.registry.db.api.image_create(None, extra_fixture)
1884
1885- images = self.client.get_images_detailed({'size_max': 20})
1886+ images = self.client.get_images_detailed(filters={'size_max': 20})
1887 self.assertEquals(len(images), 1)
1888
1889 for image in images:
1890@@ -331,7 +627,7 @@
1891
1892 glance.registry.db.api.image_create(None, extra_fixture)
1893
1894- images = self.client.get_images_detailed({'size_min': 20})
1895+ images = self.client.get_images_detailed(filters={'size_min': 20})
1896 self.assertEquals(len(images), 1)
1897
1898 for image in images:
1899@@ -351,12 +647,49 @@
1900
1901 glance.registry.db.api.image_create(None, extra_fixture)
1902
1903- images = self.client.get_images_detailed({'property-p a': 'v a'})
1904+ filters = {'property-p a': 'v a'}
1905+ images = self.client.get_images_detailed(filters=filters)
1906 self.assertEquals(len(images), 1)
1907
1908 for image in images:
1909 self.assertEquals('v a', image['properties']['p a'])
1910
1911+ def test_get_image_details_sort_disk_format_asc(self):
1912+ """
1913+ Tests that a detailed call returns list of
1914+ public images sorted alphabetically by disk_format in
1915+ ascending order.
1916+ """
1917+ extra_fixture = {'id': 3,
1918+ 'status': 'active',
1919+ 'is_public': True,
1920+ 'disk_format': 'ami',
1921+ 'container_format': 'ami',
1922+ 'name': 'asdf',
1923+ 'size': 19,
1924+ 'checksum': None}
1925+
1926+ glance.registry.db.api.image_create(None, extra_fixture)
1927+
1928+ extra_fixture = {'id': 4,
1929+ 'status': 'active',
1930+ 'is_public': True,
1931+ 'disk_format': 'vdi',
1932+ 'container_format': 'ovf',
1933+ 'name': 'xyz',
1934+ 'size': 20,
1935+ 'checksum': None}
1936+
1937+ glance.registry.db.api.image_create(None, extra_fixture)
1938+
1939+ images = self.client.get_images_detailed(sort_key='disk_format',
1940+ sort_dir='asc')
1941+
1942+ self.assertEquals(len(images), 3)
1943+ self.assertEquals(int(images[0]['id']), 3)
1944+ self.assertEquals(int(images[1]['id']), 4)
1945+ self.assertEquals(int(images[2]['id']), 2)
1946+
1947 def test_get_image(self):
1948 """Tests that the detailed info about an image returned"""
1949 fixture = {'id': 1,
1950@@ -562,6 +895,42 @@
1951 self.client.get_image,
1952 3)
1953
1954+ def test_get_image_index_sort_container_format_desc(self):
1955+ """
1956+ Tests that the client returns list of public images
1957+ sorted alphabetically by container_format in
1958+ descending order.
1959+ """
1960+ extra_fixture = {'id': 3,
1961+ 'status': 'active',
1962+ 'is_public': True,
1963+ 'disk_format': 'ami',
1964+ 'container_format': 'ami',
1965+ 'name': 'asdf',
1966+ 'size': 19,
1967+ 'checksum': None}
1968+
1969+ glance.registry.db.api.image_create(None, extra_fixture)
1970+
1971+ extra_fixture = {'id': 4,
1972+ 'status': 'active',
1973+ 'is_public': True,
1974+ 'disk_format': 'iso',
1975+ 'container_format': 'bare',
1976+ 'name': 'xyz',
1977+ 'size': 20,
1978+ 'checksum': None}
1979+
1980+ glance.registry.db.api.image_create(None, extra_fixture)
1981+
1982+ images = self.client.get_images(sort_key='container_format',
1983+ sort_dir='desc')
1984+
1985+ self.assertEquals(len(images), 3)
1986+ self.assertEquals(int(images[0]['id']), 2)
1987+ self.assertEquals(int(images[1]['id']), 4)
1988+ self.assertEquals(int(images[2]['id']), 3)
1989+
1990 def test_get_image_index(self):
1991 """Test correct set of public image returned"""
1992 fixture = {'id': 2,
1993@@ -671,7 +1040,8 @@
1994
1995 glance.registry.db.api.image_create(None, extra_fixture)
1996
1997- images = self.client.get_images({'name': 'new name! #123'})
1998+ filters = {'name': 'new name! #123'}
1999+ images = self.client.get_images(filters=filters)
2000
2001 self.assertEquals(len(images), 1)
2002 self.assertEquals('new name! #123', images[0]['name'])
2003@@ -690,7 +1060,8 @@
2004
2005 glance.registry.db.api.image_create(None, extra_fixture)
2006
2007- images = self.client.get_images({'property-p a': 'v a'})
2008+ filters = {'property-p a': 'v a'}
2009+ images = self.client.get_images(filters=filters)
2010
2011 self.assertEquals(len(images), 1)
2012 self.assertEquals(3, images[0]['id'])
2013@@ -765,7 +1136,8 @@
2014
2015 glance.registry.db.api.image_create(None, extra_fixture)
2016
2017- images = self.client.get_images_detailed({'name': 'new name! #123'})
2018+ filters = {'name': 'new name! #123'}
2019+ images = self.client.get_images_detailed(filters=filters)
2020 self.assertEquals(len(images), 1)
2021
2022 for image in images:
2023@@ -785,7 +1157,8 @@
2024
2025 glance.registry.db.api.image_create(None, extra_fixture)
2026
2027- images = self.client.get_images_detailed({'property-p a': 'v a'})
2028+ filters = {'property-p a': 'v a'}
2029+ images = self.client.get_images_detailed(filters=filters)
2030 self.assertEquals(len(images), 1)
2031
2032 for image in images:

Subscribers

People subscribed via source and target branches