Merge lp:~rackspace-titan/glance/api-results-ordering into lp:~hudson-openstack/glance/trunk
- api-results-ordering
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
API Results Ordering
(Low)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
Added sort_key and sort_dir query params to apis and clients.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brian Waldon (bcwaldon) wrote : | # |
> For the SUPPORTED_PARAMS, and SUPPORTED_
> you make a glance.
> 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(
>
> Just make the sort_key parameter default to 'created_at', and shorten to:
>
> sort_key_attr = getattr(
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.
> 175 + params.
>
> Go ahead and put the first line in the _extract_
> _extract_
Got it.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jay Pipes (jaypipes) wrote : | # |
> I think I took care of this now. Can you double-check?
For the /glance/
For the client, though:
161 +# parameters accepted in get_images* methods
162 +SUPPORTED_
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_
> > 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(
> >
> > Just make the sort_key parameter default to 'created_at', and shorten to:
> >
> > sort_key_attr = getattr(
>
> 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brian Waldon (bcwaldon) wrote : | # |
> > I think I took care of this now. Can you double-check?
>
> For the /glance/
>
> For the client, though:
>
> 161 +# parameters accepted in get_images* methods
> 162 +SUPPORTED_
>
> 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_
> V1Client class, and reference v1_images.
> copied SUPPORTED_
> sense?
I had to move BaseClient out of glance/client.py and into glance/
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(
> > >
> > > Just make the sort_key parameter default to 'created_at', and shorten to:
> > >
> > > sort_key_attr = getattr(
> >
> > 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
- 152. By Brian Waldon
-
docstring
- 153. By Brian Waldon
-
reverting one import change; another docstring fix
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 ;)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brian Waldon (bcwaldon) wrote : | # |
Brian: So how do we do it in glance?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
> You are reviewing the proposed merge of
lp:~rackspace-titan/glance/api-results-ordering into lp:glance.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jay Pipes (jaypipes) : | # |
Preview Diff
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: |
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' models. Image, _sort_key)
285 + sort_key_attr = getattr(
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' , {}) update( self._extract_ get_params( kwargs) )
175 + params.
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