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
=== modified file 'bin/glance'
--- bin/glance 2011-04-27 18:00:49 +0000
+++ bin/glance 2011-06-28 13:52:02 +0000
@@ -194,7 +194,7 @@
194 print "Returned the following metadata for the new image:"194 print "Returned the following metadata for the new image:"
195 for k, v in sorted(image_meta.items()):195 for k, v in sorted(image_meta.items()):
196 print " %(k)30s => %(v)s" % locals()196 print " %(k)30s => %(v)s" % locals()
197 except client.ClientConnectionError, e:197 except exception.ClientConnectionError, e:
198 host = options.host198 host = options.host
199 port = options.port199 port = options.port
200 print ("Failed to connect to the Glance API server "200 print ("Failed to connect to the Glance API server "
201201
=== modified file 'doc/source/client.rst'
--- doc/source/client.rst 2011-05-25 20:03:00 +0000
+++ doc/source/client.rst 2011-06-28 13:52:02 +0000
@@ -113,7 +113,36 @@
113 c = Client("glance.example.com", 9292)113 c = Client("glance.example.com", 9292)
114114
115 filters = {'status': 'saving', 'size_max': (5 * 1024 * 1024 * 1024)}115 filters = {'status': 'saving', 'size_max': (5 * 1024 * 1024 * 1024)}
116 print c.get_images_detailed(filters)116 print c.get_images_detailed(filters=filters)
117
118Sorting Images Returned via ``get_images()`` and ``get_images_detailed()``
119--------------------------------------------------------------------------
120
121Two parameters are available to sort the list of images returned by
122these methods.
123
124* ``sort_key: KEY``
125
126 Images can be ordered by the image attribute ``KEY``. Acceptable values:
127 ``id``, ``name``, ``status``, ``container_format``, ``disk_format``,
128 ``created_at`` (default) and ``updated_at``.
129
130* ``sort_dir: DIR``
131
132 The direction of the sort may be defined by ``DIR``. Accepted values:
133 ``asc`` for ascending or ``desc`` (default) for descending.
134
135The following example will return a list of images sorted alphabetically
136by name in ascending order.
137
138.. code-block:: python
139
140 from glance.client import Client
141
142 c = Client("glance.example.com", 9292)
143
144 print c.get_images(sort_key='name', sort_dir='asc')
145
117146
118Requesting Detailed Metadata on a Specific Image147Requesting Detailed Metadata on a Specific Image
119------------------------------------------------148------------------------------------------------
120149
=== modified file 'doc/source/glanceapi.rst'
--- doc/source/glanceapi.rst 2011-05-26 12:53:48 +0000
+++ doc/source/glanceapi.rst 2011-06-28 13:52:02 +0000
@@ -129,6 +129,20 @@
129129
130 Filters images having a ``size`` attribute less than or equal to ``BYTES``130 Filters images having a ``size`` attribute less than or equal to ``BYTES``
131131
132These two resources also accept sort parameters:
133
134* ``sort_key=KEY``
135
136 Results will be ordered by the specified image attribute ``KEY``. Accepted
137 values include ``id``, ``name``, ``status``, ``disk_format``,
138 ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``.
139
140* ``sort_dir=DIR``
141
142 Results will be sorted in the direction ``DIR``. Accepted values are ``asc``
143 for ascending or ``desc`` (default) for descending.
144
145
132Requesting Detailed Metadata on a Specific Image146Requesting Detailed Metadata on a Specific Image
133------------------------------------------------147------------------------------------------------
134148
135149
=== modified file 'doc/source/registries.rst'
--- doc/source/registries.rst 2011-05-26 12:53:48 +0000
+++ doc/source/registries.rst 2011-06-28 13:52:02 +0000
@@ -83,6 +83,20 @@
8383
84 Filters images having a ``size`` attribute less than or equal to ``BYTES``84 Filters images having a ``size`` attribute less than or equal to ``BYTES``
8585
86These two resources also accept sort parameters:
87
88* ``sort_key=KEY``
89
90 Results will be ordered by the specified image attribute ``KEY``. Accepted
91 values include ``id``, ``name``, ``status``, ``disk_format``,
92 ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``.
93
94* ``sort_dir=DIR``
95
96 Results will be sorted in the direction ``DIR``. Accepted values are ``asc``
97 for ascending or ``desc`` (default) for descending.
98
99
86``POST /images``100``POST /images``
87----------------101----------------
88102
89103
=== modified file 'glance/api/v1/images.py'
--- glance/api/v1/images.py 2011-06-27 14:37:40 +0000
+++ glance/api/v1/images.py 2011-06-28 13:52:02 +0000
@@ -45,6 +45,8 @@
45SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',45SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
46 'size_min', 'size_max']46 'size_min', 'size_max']
4747
48SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
49
4850
49class Controller(object):51class Controller(object):
5052
@@ -92,14 +94,7 @@
92 'size': <SIZE>}, ...94 'size': <SIZE>}, ...
93 ]}95 ]}
94 """96 """
95 params = {'filters': self._get_filters(req)}97 params = self._get_query_params(req)
96
97 if 'limit' in req.str_params:
98 params['limit'] = req.str_params.get('limit')
99
100 if 'marker' in req.str_params:
101 params['marker'] = req.str_params.get('marker')
102
103 images = registry.get_images_list(self.options, **params)98 images = registry.get_images_list(self.options, **params)
104 return dict(images=images)99 return dict(images=images)
105100
@@ -125,17 +120,23 @@
125 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...120 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
126 ]}121 ]}
127 """122 """
128 params = {'filters': self._get_filters(req)}123 params = self._get_query_params(req)
129
130 if 'limit' in req.str_params:
131 params['limit'] = req.str_params.get('limit')
132
133 if 'marker' in req.str_params:
134 params['marker'] = req.str_params.get('marker')
135
136 images = registry.get_images_detail(self.options, **params)124 images = registry.get_images_detail(self.options, **params)
137 return dict(images=images)125 return dict(images=images)
138126
127 def _get_query_params(self, req):
128 """
129 Extracts necessary query params from request.
130
131 :param req: the WSGI Request object
132 :retval dict of parameters that can be used by registry client
133 """
134 params = {'filters': self._get_filters(req)}
135 for PARAM in SUPPORTED_PARAMS:
136 if PARAM in req.str_params:
137 params[PARAM] = req.str_params.get(PARAM)
138 return params
139
139 def _get_filters(self, req):140 def _get_filters(self, req):
140 """141 """
141 Return a dictionary of query param filters from the request142 Return a dictionary of query param filters from the request
142143
=== modified file 'glance/client.py'
--- glance/client.py 2011-05-31 13:21:16 +0000
+++ glance/client.py 2011-06-28 13:52:02 +0000
@@ -19,172 +19,17 @@
19Client classes for callers of a Glance system19Client classes for callers of a Glance system
20"""20"""
2121
22import httplib
23import json22import json
24import logging
25import urlparse
26import socket
27import sys
28import urllib
2923
24from glance.api.v1 import images as v1_images
25from glance.common import client as base_client
26from glance.common import exception
30from glance import utils27from glance import utils
31from glance.common import exception
3228
33#TODO(jaypipes) Allow a logger param for client classes29#TODO(jaypipes) Allow a logger param for client classes
3430
3531
36class ClientConnectionError(Exception):32class V1Client(base_client.BaseClient):
37 """Error resulting from a client connecting to a server"""
38 pass
39
40
41class ImageBodyIterator(object):
42
43 """
44 A class that acts as an iterator over an image file's
45 chunks of data. This is returned as part of the result
46 tuple from `glance.client.Client.get_image`
47 """
48
49 CHUNKSIZE = 65536
50
51 def __init__(self, response):
52 """
53 Constructs the object from an HTTPResponse object
54 """
55 self.response = response
56
57 def __iter__(self):
58 """
59 Exposes an iterator over the chunks of data in the
60 image file.
61 """
62 while True:
63 chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
64 if chunk:
65 yield chunk
66 else:
67 break
68
69
70class BaseClient(object):
71
72 """A base client class"""
73
74 CHUNKSIZE = 65536
75
76 def __init__(self, host, port, use_ssl):
77 """
78 Creates a new client to some service.
79
80 :param host: The host where service resides
81 :param port: The port where service resides
82 :param use_ssl: Should we use HTTPS?
83 """
84 self.host = host
85 self.port = port
86 self.use_ssl = use_ssl
87 self.connection = None
88
89 def get_connection_type(self):
90 """
91 Returns the proper connection type
92 """
93 if self.use_ssl:
94 return httplib.HTTPSConnection
95 else:
96 return httplib.HTTPConnection
97
98 def do_request(self, method, action, body=None, headers=None,
99 params=None):
100 """
101 Connects to the server and issues a request. Handles converting
102 any returned HTTP error status codes to OpenStack/Glance exceptions
103 and closing the server connection. Returns the result data, or
104 raises an appropriate exception.
105
106 :param method: HTTP method ("GET", "POST", "PUT", etc...)
107 :param action: part of URL after root netloc
108 :param body: string of data to send, or None (default)
109 :param headers: mapping of key/value pairs to add as headers
110 :param params: dictionary of key/value pairs to add to append
111 to action
112
113 :note
114
115 If the body param has a read attribute, and method is either
116 POST or PUT, this method will automatically conduct a chunked-transfer
117 encoding and use the body as a file object, transferring chunks
118 of data using the connection's send() method. This allows large
119 objects to be transferred efficiently without buffering the entire
120 body in memory.
121 """
122 if type(params) is dict:
123 action += '?' + urllib.urlencode(params)
124
125 try:
126 connection_type = self.get_connection_type()
127 headers = headers or {}
128 c = connection_type(self.host, self.port)
129
130 # Do a simple request or a chunked request, depending
131 # on whether the body param is a file-like object and
132 # the method is PUT or POST
133 if hasattr(body, 'read') and method.lower() in ('post', 'put'):
134 # Chunk it, baby...
135 c.putrequest(method, action)
136
137 for header, value in headers.items():
138 c.putheader(header, value)
139 c.putheader('Transfer-Encoding', 'chunked')
140 c.endheaders()
141
142 chunk = body.read(self.CHUNKSIZE)
143 while chunk:
144 c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
145 chunk = body.read(self.CHUNKSIZE)
146 c.send('0\r\n\r\n')
147 else:
148 # Simple request...
149 c.request(method, action, body, headers)
150 res = c.getresponse()
151 status_code = self.get_status_code(res)
152 if status_code in (httplib.OK,
153 httplib.CREATED,
154 httplib.ACCEPTED,
155 httplib.NO_CONTENT):
156 return res
157 elif status_code == httplib.UNAUTHORIZED:
158 raise exception.NotAuthorized
159 elif status_code == httplib.FORBIDDEN:
160 raise exception.NotAuthorized
161 elif status_code == httplib.NOT_FOUND:
162 raise exception.NotFound
163 elif status_code == httplib.CONFLICT:
164 raise exception.Duplicate(res.read())
165 elif status_code == httplib.BAD_REQUEST:
166 raise exception.Invalid(res.read())
167 elif status_code == httplib.INTERNAL_SERVER_ERROR:
168 raise Exception("Internal Server error: %s" % res.read())
169 else:
170 raise Exception("Unknown error occurred! %s" % res.read())
171
172 except (socket.error, IOError), e:
173 raise ClientConnectionError("Unable to connect to "
174 "server. Got error: %s" % e)
175
176 def get_status_code(self, response):
177 """
178 Returns the integer status code from the response, which
179 can be either a Webob.Response (used in testing) or httplib.Response
180 """
181 if hasattr(response, 'status_int'):
182 return response.status_int
183 else:
184 return response.status
185
186
187class V1Client(BaseClient):
18833
189 """Main client class for accessing Glance resources"""34 """Main client class for accessing Glance resources"""
19035
@@ -209,7 +54,7 @@
209 return super(V1Client, self).do_request(method, action, body,54 return super(V1Client, self).do_request(method, action, body,
210 headers, params)55 headers, params)
21156
212 def get_images(self, filters=None, marker=None, limit=None):57 def get_images(self, **kwargs):
213 """58 """
214 Returns a list of image id/name mappings from Registry59 Returns a list of image id/name mappings from Registry
21560
@@ -217,18 +62,15 @@
217 collection of images should be filtered62 collection of images should be filtered
218 :param marker: id after which to start the page of images63 :param marker: id after which to start the page of images
219 :param limit: maximum number of items to return64 :param limit: maximum number of items to return
65 :param sort_key: results will be ordered by this image attribute
66 :param sort_dir: direction in which to to order results (asc, desc)
220 """67 """
22168 params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
222 params = filters or {}
223 if marker:
224 params['marker'] = marker
225 if limit:
226 params['limit'] = limit
227 res = self.do_request("GET", "/images", params=params)69 res = self.do_request("GET", "/images", params=params)
228 data = json.loads(res.read())['images']70 data = json.loads(res.read())['images']
229 return data71 return data
23072
231 def get_images_detailed(self, filters=None, marker=None, limit=None):73 def get_images_detailed(self, **kwargs):
232 """74 """
233 Returns a list of detailed image data mappings from Registry75 Returns a list of detailed image data mappings from Registry
23476
@@ -236,13 +78,11 @@
236 collection of images should be filtered78 collection of images should be filtered
237 :param marker: id after which to start the page of images79 :param marker: id after which to start the page of images
238 :param limit: maximum number of items to return80 :param limit: maximum number of items to return
81 :param sort_key: results will be ordered by this image attribute
82 :param sort_dir: direction in which to to order results (asc, desc)
239 """83 """
24084
241 params = filters or {}85 params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
242 if marker:
243 params['marker'] = marker
244 if limit:
245 params['limit'] = limit
246 res = self.do_request("GET", "/images/detail", params=params)86 res = self.do_request("GET", "/images/detail", params=params)
247 data = json.loads(res.read())['images']87 data = json.loads(res.read())['images']
248 return data88 return data
@@ -260,7 +100,7 @@
260 res = self.do_request("GET", "/images/%s" % image_id)100 res = self.do_request("GET", "/images/%s" % image_id)
261101
262 image = utils.get_image_meta_from_headers(res)102 image = utils.get_image_meta_from_headers(res)
263 return image, ImageBodyIterator(res)103 return image, base_client.ImageBodyIterator(res)
264104
265 def get_image_meta(self, image_id):105 def get_image_meta(self, image_id):
266 """106 """
267107
=== added file 'glance/common/client.py'
--- glance/common/client.py 1970-01-01 00:00:00 +0000
+++ glance/common/client.py 2011-06-28 13:52:02 +0000
@@ -0,0 +1,169 @@
1import httplib
2import logging
3import socket
4import urllib
5
6from glance.common import exception
7
8
9class ImageBodyIterator(object):
10
11 """
12 A class that acts as an iterator over an image file's
13 chunks of data. This is returned as part of the result
14 tuple from `glance.client.Client.get_image`
15 """
16
17 CHUNKSIZE = 65536
18
19 def __init__(self, response):
20 """
21 Constructs the object from an HTTPResponse object
22 """
23 self.response = response
24
25 def __iter__(self):
26 """
27 Exposes an iterator over the chunks of data in the
28 image file.
29 """
30 while True:
31 chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
32 if chunk:
33 yield chunk
34 else:
35 break
36
37
38class BaseClient(object):
39
40 """A base client class"""
41
42 CHUNKSIZE = 65536
43
44 def __init__(self, host, port, use_ssl):
45 """
46 Creates a new client to some service.
47
48 :param host: The host where service resides
49 :param port: The port where service resides
50 :param use_ssl: Should we use HTTPS?
51 """
52 self.host = host
53 self.port = port
54 self.use_ssl = use_ssl
55 self.connection = None
56
57 def get_connection_type(self):
58 """
59 Returns the proper connection type
60 """
61 if self.use_ssl:
62 return httplib.HTTPSConnection
63 else:
64 return httplib.HTTPConnection
65
66 def do_request(self, method, action, body=None, headers=None,
67 params=None):
68 """
69 Connects to the server and issues a request. Handles converting
70 any returned HTTP error status codes to OpenStack/Glance exceptions
71 and closing the server connection. Returns the result data, or
72 raises an appropriate exception.
73
74 :param method: HTTP method ("GET", "POST", "PUT", etc...)
75 :param action: part of URL after root netloc
76 :param body: string of data to send, or None (default)
77 :param headers: mapping of key/value pairs to add as headers
78 :param params: dictionary of key/value pairs to add to append
79 to action
80
81 :note
82
83 If the body param has a read attribute, and method is either
84 POST or PUT, this method will automatically conduct a chunked-transfer
85 encoding and use the body as a file object, transferring chunks
86 of data using the connection's send() method. This allows large
87 objects to be transferred efficiently without buffering the entire
88 body in memory.
89 """
90 if type(params) is dict:
91 action += '?' + urllib.urlencode(params)
92
93 try:
94 connection_type = self.get_connection_type()
95 headers = headers or {}
96 c = connection_type(self.host, self.port)
97
98 # Do a simple request or a chunked request, depending
99 # on whether the body param is a file-like object and
100 # the method is PUT or POST
101 if hasattr(body, 'read') and method.lower() in ('post', 'put'):
102 # Chunk it, baby...
103 c.putrequest(method, action)
104
105 for header, value in headers.items():
106 c.putheader(header, value)
107 c.putheader('Transfer-Encoding', 'chunked')
108 c.endheaders()
109
110 chunk = body.read(self.CHUNKSIZE)
111 while chunk:
112 c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
113 chunk = body.read(self.CHUNKSIZE)
114 c.send('0\r\n\r\n')
115 else:
116 # Simple request...
117 c.request(method, action, body, headers)
118 res = c.getresponse()
119 status_code = self.get_status_code(res)
120 if status_code in (httplib.OK,
121 httplib.CREATED,
122 httplib.ACCEPTED,
123 httplib.NO_CONTENT):
124 return res
125 elif status_code == httplib.UNAUTHORIZED:
126 raise exception.NotAuthorized
127 elif status_code == httplib.FORBIDDEN:
128 raise exception.NotAuthorized
129 elif status_code == httplib.NOT_FOUND:
130 raise exception.NotFound
131 elif status_code == httplib.CONFLICT:
132 raise exception.Duplicate(res.read())
133 elif status_code == httplib.BAD_REQUEST:
134 raise exception.Invalid(res.read())
135 elif status_code == httplib.INTERNAL_SERVER_ERROR:
136 raise Exception("Internal Server error: %s" % res.read())
137 else:
138 raise Exception("Unknown error occurred! %s" % res.read())
139
140 except (socket.error, IOError), e:
141 raise exception.ClientConnectionError("Unable to connect to "
142 "server. Got error: %s" % e)
143
144 def get_status_code(self, response):
145 """
146 Returns the integer status code from the response, which
147 can be either a Webob.Response (used in testing) or httplib.Response
148 """
149 if hasattr(response, 'status_int'):
150 return response.status_int
151 else:
152 return response.status
153
154 def _extract_params(self, actual_params, allowed_params):
155 """
156 Extract a subset of keys from a dictionary. The filters key
157 will also be extracted, and each of its values will be returned
158 as an individual param.
159
160 :param actual_params: dict of keys to filter
161 :param allowed_params: list of keys that 'actual_params' will be
162 reduced to
163 :retval subset of 'params' dict
164 """
165 result = actual_params.get('filters', {})
166 for allowed_param in allowed_params:
167 if allowed_param in actual_params:
168 result[allowed_param] = actual_params[allowed_param]
169 return result
0170
=== modified file 'glance/common/exception.py'
--- glance/common/exception.py 2011-06-15 14:47:00 +0000
+++ glance/common/exception.py 2011-06-28 13:52:02 +0000
@@ -83,6 +83,11 @@
83 pass83 pass
8484
8585
86class ClientConnectionError(Exception):
87 """Error resulting from a client connecting to a server"""
88 pass
89
90
86def wrap_exception(f):91def wrap_exception(f):
87 def _wrap(*args, **kw):92 def _wrap(*args, **kw):
88 try:93 try:
8994
=== modified file 'glance/registry/client.py'
--- glance/registry/client.py 2011-06-11 13:47:47 +0000
+++ glance/registry/client.py 2011-06-28 13:52:02 +0000
@@ -23,7 +23,8 @@
23import json23import json
24import urllib24import urllib
2525
26from glance.client import BaseClient26from glance.common.client import BaseClient
27from glance.registry import server
2728
2829
29class RegistryClient(BaseClient):30class RegistryClient(BaseClient):
@@ -44,42 +45,32 @@
44 port = port or self.DEFAULT_PORT45 port = port or self.DEFAULT_PORT
45 super(RegistryClient, self).__init__(host, port, use_ssl)46 super(RegistryClient, self).__init__(host, port, use_ssl)
4647
47 def get_images(self, filters=None, marker=None, limit=None):48 def get_images(self, **kwargs):
48 """49 """
49 Returns a list of image id/name mappings from Registry50 Returns a list of image id/name mappings from Registry
5051
51 :param filters: dict of keys & expected values to filter results52 :param filters: dict of keys & expected values to filter results
52 :param marker: image id after which to start page53 :param marker: image id after which to start page
53 :param limit: max number of images to return54 :param limit: max number of images to return
55 :param sort_key: results will be ordered by this image attribute
56 :param sort_dir: direction in which to to order results (asc, desc)
54 """57 """
55 params = filters or {}58 params = self._extract_params(kwargs, server.SUPPORTED_PARAMS)
56
57 if marker != None:
58 params['marker'] = marker
59
60 if limit != None:
61 params['limit'] = limit
62
63 res = self.do_request("GET", "/images", params=params)59 res = self.do_request("GET", "/images", params=params)
64 data = json.loads(res.read())['images']60 data = json.loads(res.read())['images']
65 return data61 return data
6662
67 def get_images_detailed(self, filters=None, marker=None, limit=None):63 def get_images_detailed(self, **kwargs):
68 """64 """
69 Returns a list of detailed image data mappings from Registry65 Returns a list of detailed image data mappings from Registry
7066
71 :param filters: dict of keys & expected values to filter results67 :param filters: dict of keys & expected values to filter results
72 :param marker: image id after which to start page68 :param marker: image id after which to start page
73 :param limit: max number of images to return69 :param limit: max number of images to return
70 :param sort_key: results will be ordered by this image attribute
71 :param sort_dir: direction in which to to order results (asc, desc)
74 """72 """
75 params = filters or {}73 params = self._extract_params(kwargs, server.SUPPORTED_PARAMS)
76
77 if marker != None:
78 params['marker'] = marker
79
80 if limit != None:
81 params['limit'] = limit
82
83 res = self.do_request("GET", "/images/detail", params=params)74 res = self.do_request("GET", "/images/detail", params=params)
84 data = json.loads(res.read())['images']75 data = json.loads(res.read())['images']
85 return data76 return data
8677
=== modified file 'glance/registry/db/api.py'
--- glance/registry/db/api.py 2011-05-31 19:32:17 +0000
+++ glance/registry/db/api.py 2011-06-28 13:52:02 +0000
@@ -23,7 +23,7 @@
2323
24import logging24import logging
2525
26from sqlalchemy import create_engine, desc26from sqlalchemy import asc, create_engine, desc
27from sqlalchemy.ext.declarative import declarative_base27from sqlalchemy.ext.declarative import declarative_base
28from sqlalchemy.orm import exc28from sqlalchemy.orm import exc
29from sqlalchemy.orm import joinedload29from sqlalchemy.orm import joinedload
@@ -129,7 +129,8 @@
129 raise exception.NotFound("No image found with ID %s" % image_id)129 raise exception.NotFound("No image found with ID %s" % image_id)
130130
131131
132def image_get_all_public(context, filters=None, marker=None, limit=None):132def image_get_all_public(context, filters=None, marker=None, limit=None,
133 sort_key='created_at', sort_dir='desc'):
133 """Get all public images that match zero or more filters.134 """Get all public images that match zero or more filters.
134135
135 :param filters: dict of filter keys and values. If a 'properties'136 :param filters: dict of filter keys and values. If a 'properties'
@@ -137,7 +138,8 @@
137 filters on the image properties attribute138 filters on the image properties attribute
138 :param marker: image id after which to start page139 :param marker: image id after which to start page
139 :param limit: maximum number of images to return140 :param limit: maximum number of images to return
140141 :param sort_key: image attribute by which results should be sorted
142 :param sort_dir: direction in which results should be sorted (asc, desc)
141 """143 """
142 filters = filters or {}144 filters = filters or {}
143145
@@ -146,9 +148,17 @@
146 options(joinedload(models.Image.properties)).\148 options(joinedload(models.Image.properties)).\
147 filter_by(deleted=_deleted(context)).\149 filter_by(deleted=_deleted(context)).\
148 filter_by(is_public=True).\150 filter_by(is_public=True).\
149 filter(models.Image.status != 'killed').\151 filter(models.Image.status != 'killed')
150 order_by(desc(models.Image.created_at)).\152
151 order_by(desc(models.Image.id))153 sort_dir_func = {
154 'asc': asc,
155 'desc': desc,
156 }[sort_dir]
157
158 sort_key_attr = getattr(models.Image, sort_key)
159
160 query = query.order_by(sort_dir_func(sort_key_attr)).\
161 order_by(sort_dir_func(models.Image.id))
152162
153 if 'size_min' in filters:163 if 'size_min' in filters:
154 query = query.filter(models.Image.size >= filters['size_min'])164 query = query.filter(models.Image.size >= filters['size_min'])
155165
=== modified file 'glance/registry/server.py'
--- glance/registry/server.py 2011-06-15 14:47:00 +0000
+++ glance/registry/server.py 2011-06-28 13:52:02 +0000
@@ -39,8 +39,15 @@
39SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',39SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
40 'size_min', 'size_max']40 'size_min', 'size_max']
4141
42SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
43 'size', 'id', 'created_at', 'updated_at')
44
45SUPPORTED_SORT_DIRS = ('asc', 'desc')
46
42MAX_ITEM_LIMIT = 2547MAX_ITEM_LIMIT = 25
4348
49SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
50
4451
45class Controller(object):52class Controller(object):
46 """Controller for the reference implementation registry server"""53 """Controller for the reference implementation registry server"""
@@ -69,14 +76,7 @@
69 }76 }
7077
71 """78 """
72 params = {79 params = self._get_query_params(req)
73 'filters': self._get_filters(req),
74 'limit': self._get_limit(req),
75 }
76
77 if 'marker' in req.str_params:
78 params['marker'] = self._get_marker(req)
79
80 images = db_api.image_get_all_public(None, **params)80 images = db_api.image_get_all_public(None, **params)
8181
82 results = []82 results = []
@@ -99,19 +99,33 @@
99 all image model fields.99 all image model fields.
100100
101 """101 """
102 params = {102 params = self._get_query_params(req)
103 'filters': self._get_filters(req),
104 'limit': self._get_limit(req),
105 }
106
107 if 'marker' in req.str_params:
108 params['marker'] = self._get_marker(req)
109
110 images = db_api.image_get_all_public(None, **params)103 images = db_api.image_get_all_public(None, **params)
111104
112 image_dicts = [make_image_dict(i) for i in images]105 image_dicts = [make_image_dict(i) for i in images]
113 return dict(images=image_dicts)106 return dict(images=image_dicts)
114107
108 def _get_query_params(self, req):
109 """
110 Extract necessary query parameters from http request.
111
112 :param req: the Request object coming from the wsgi layer
113 :retval dictionary of filters to apply to list of images
114 """
115 params = {
116 'filters': self._get_filters(req),
117 'limit': self._get_limit(req),
118 'sort_key': self._get_sort_key(req),
119 'sort_dir': self._get_sort_dir(req),
120 'marker': self._get_marker(req),
121 }
122
123 for key, value in params.items():
124 if value is None:
125 del params[key]
126
127 return params
128
115 def _get_filters(self, req):129 def _get_filters(self, req):
116 """Return a dictionary of query param filters from the request130 """Return a dictionary of query param filters from the request
117131
@@ -148,12 +162,35 @@
148162
149 def _get_marker(self, req):163 def _get_marker(self, req):
150 """Parse a marker query param into something usable."""164 """Parse a marker query param into something usable."""
165 marker = req.str_params.get('marker', None)
166
167 if marker is None:
168 return None
169
151 try:170 try:
152 marker = int(req.str_params.get('marker', None))171 marker = int(marker)
153 except ValueError:172 except ValueError:
154 raise exc.HTTPBadRequest("marker param must be an integer")173 raise exc.HTTPBadRequest("marker param must be an integer")
155 return marker174 return marker
156175
176 def _get_sort_key(self, req):
177 """Parse a sort key query param from the request object."""
178 sort_key = req.str_params.get('sort_key', None)
179 if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS:
180 _keys = ', '.join(SUPPORTED_SORT_KEYS)
181 msg = "Unsupported sort_key. Acceptable values: %s" % (_keys,)
182 raise exc.HTTPBadRequest(explanation=msg)
183 return sort_key
184
185 def _get_sort_dir(self, req):
186 """Parse a sort direction query param from the request object."""
187 sort_dir = req.str_params.get('sort_dir', None)
188 if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS:
189 _keys = ', '.join(SUPPORTED_SORT_DIRS)
190 msg = "Unsupported sort_dir. Acceptable values: %s" % (_keys,)
191 raise exc.HTTPBadRequest(explanation=msg)
192 return sort_dir
193
157 def show(self, req, id):194 def show(self, req, id):
158 """Return data about the given image id."""195 """Return data about the given image id."""
159 try:196 try:
160197
=== modified file 'tests/functional/test_curl_api.py'
--- tests/functional/test_curl_api.py 2011-06-22 14:19:33 +0000
+++ tests/functional/test_curl_api.py 2011-06-28 13:52:02 +0000
@@ -1117,3 +1117,118 @@
1117 self.assertEqual(int(images[0]['id']), 2)1117 self.assertEqual(int(images[0]['id']), 2)
11181118
1119 self.stop_servers()1119 self.stop_servers()
1120
1121 def test_ordered_images(self):
1122 """
1123 Set up three test images and ensure each query param filter works
1124 """
1125 self.cleanup()
1126 self.start_servers()
1127
1128 api_port = self.api_port
1129 registry_port = self.registry_port
1130
1131 # 0. GET /images
1132 # Verify no public images
1133 cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
1134
1135 exitcode, out, err = execute(cmd)
1136
1137 self.assertEqual(0, exitcode)
1138 self.assertEqual('{"images": []}', out.strip())
1139
1140 # 1. POST /images with three public images with various attributes
1141 cmd = ("curl -i -X POST "
1142 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1143 "-H 'X-Image-Meta-Name: Image1' "
1144 "-H 'X-Image-Meta-Status: active' "
1145 "-H 'X-Image-Meta-Container-Format: ovf' "
1146 "-H 'X-Image-Meta-Disk-Format: vdi' "
1147 "-H 'X-Image-Meta-Size: 19' "
1148 "-H 'X-Image-Meta-Is-Public: True' "
1149 "http://0.0.0.0:%d/v1/images") % api_port
1150
1151 exitcode, out, err = execute(cmd)
1152 self.assertEqual(0, exitcode)
1153
1154 lines = out.split("\r\n")
1155 status_line = lines[0]
1156
1157 self.assertEqual("HTTP/1.1 201 Created", status_line)
1158
1159 cmd = ("curl -i -X POST "
1160 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1161 "-H 'X-Image-Meta-Name: ASDF' "
1162 "-H 'X-Image-Meta-Status: active' "
1163 "-H 'X-Image-Meta-Container-Format: bare' "
1164 "-H 'X-Image-Meta-Disk-Format: iso' "
1165 "-H 'X-Image-Meta-Size: 2' "
1166 "-H 'X-Image-Meta-Is-Public: True' "
1167 "http://0.0.0.0:%d/v1/images") % api_port
1168
1169 exitcode, out, err = execute(cmd)
1170 self.assertEqual(0, exitcode)
1171
1172 lines = out.split("\r\n")
1173 status_line = lines[0]
1174
1175 self.assertEqual("HTTP/1.1 201 Created", status_line)
1176 cmd = ("curl -i -X POST "
1177 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1178 "-H 'X-Image-Meta-Name: XYZ' "
1179 "-H 'X-Image-Meta-Status: saving' "
1180 "-H 'X-Image-Meta-Container-Format: ami' "
1181 "-H 'X-Image-Meta-Disk-Format: ami' "
1182 "-H 'X-Image-Meta-Size: 5' "
1183 "-H 'X-Image-Meta-Is-Public: True' "
1184 "http://0.0.0.0:%d/v1/images") % api_port
1185
1186 exitcode, out, err = execute(cmd)
1187 self.assertEqual(0, exitcode)
1188
1189 lines = out.split("\r\n")
1190 status_line = lines[0]
1191
1192 self.assertEqual("HTTP/1.1 201 Created", status_line)
1193
1194 # 2. GET /images with no query params
1195 # Verify three public images sorted by created_at desc
1196 cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
1197
1198 exitcode, out, err = execute(cmd)
1199
1200 self.assertEqual(0, exitcode)
1201 images = json.loads(out.strip())['images']
1202
1203 self.assertEqual(len(images), 3)
1204 self.assertEqual(images[0]['id'], 3)
1205 self.assertEqual(images[1]['id'], 2)
1206 self.assertEqual(images[2]['id'], 1)
1207
1208 # 3. GET /images sorted by name asc
1209 params = 'sort_key=name&sort_dir=asc'
1210 cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params)
1211
1212 exitcode, out, err = execute(cmd)
1213
1214 self.assertEqual(0, exitcode)
1215 images = json.loads(out.strip())['images']
1216
1217 self.assertEqual(len(images), 3)
1218 self.assertEqual(images[0]['id'], 2)
1219 self.assertEqual(images[1]['id'], 1)
1220 self.assertEqual(images[2]['id'], 3)
1221
1222 # 4. GET /images sorted by size desc
1223 params = 'sort_key=size&sort_dir=desc'
1224 cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params)
1225
1226 exitcode, out, err = execute(cmd)
1227
1228 self.assertEqual(0, exitcode)
1229 images = json.loads(out.strip())['images']
1230
1231 self.assertEqual(len(images), 3)
1232 self.assertEqual(images[0]['id'], 1)
1233 self.assertEqual(images[1]['id'], 3)
1234 self.assertEqual(images[2]['id'], 2)
11201235
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2011-05-31 19:32:17 +0000
+++ tests/stubs.py 2011-06-28 13:52:02 +0000
@@ -28,6 +28,7 @@
28import stubout28import stubout
29import webob29import webob
3030
31import glance.common.client
31from glance.common import exception32from glance.common import exception
32from glance.registry import server as rserver33from glance.registry import server as rserver
33from glance.api import v1 as server34from glance.api import v1 as server
@@ -254,9 +255,9 @@
254 for i in self.response.app_iter:255 for i in self.response.app_iter:
255 yield i256 yield i
256257
257 stubs.Set(glance.client.BaseClient, 'get_connection_type',258 stubs.Set(glance.common.client.BaseClient, 'get_connection_type',
258 fake_get_connection_type)259 fake_get_connection_type)
259 stubs.Set(glance.client.ImageBodyIterator, '__iter__',260 stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
260 fake_image_iter)261 fake_image_iter)
261262
262263
@@ -388,8 +389,8 @@
388 else:389 else:
389 return images[0]390 return images[0]
390391
391 def image_get_all_public(self, _context, filters=None,392 def image_get_all_public(self, _context, filters=None, marker=None,
392 marker=None, limit=1000):393 limit=1000, sort_key=None, sort_dir=None):
393 images = [f for f in self.images if f['is_public'] == True]394 images = [f for f in self.images if f['is_public'] == True]
394395
395 if 'size_min' in filters:396 if 'size_min' in filters:
@@ -414,16 +415,24 @@
414 for k, v in filters.items():415 for k, v in filters.items():
415 images = [f for f in images if f[k] == v]416 images = [f for f in images if f[k] == v]
416417
418 # sorted func expects func that compares in descending order
417 def image_cmp(x, y):419 def image_cmp(x, y):
418 if x['created_at'] > y['created_at']:420 _sort_dir = sort_dir or 'desc'
419 return 1421 multiplier = {
420 elif x['created_at'] == y['created_at']:422 'asc': -1,
423 'desc': 1,
424 }[_sort_dir]
425
426 _sort_key = sort_key or 'created_at'
427 if x[_sort_key] > y[_sort_key]:
428 return 1 * multiplier
429 elif x[_sort_key] == y[_sort_key]:
421 if x['id'] > y['id']:430 if x['id'] > y['id']:
422 return 1431 return 1 * multiplier
423 else:432 else:
424 return -1433 return -1 * multiplier
425 else:434 else:
426 return -1435 return -1 * multiplier
427436
428 images = sorted(images, cmp=image_cmp)437 images = sorted(images, cmp=image_cmp)
429 images.reverse()438 images.reverse()
430439
=== modified file 'tests/unit/test_api.py'
--- tests/unit/test_api.py 2011-06-24 20:57:02 +0000
+++ tests/unit/test_api.py 2011-06-28 13:52:02 +0000
@@ -285,6 +285,398 @@
285 for image in images:285 for image in images:
286 self.assertEqual('new name! #123', image['name'])286 self.assertEqual('new name! #123', image['name'])
287287
288 def test_get_index_sort_default_created_at_desc(self):
289 """
290 Tests that the /images registry API returns list of
291 public images that conforms to a default sort key/dir
292 """
293 time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
294 time2 = datetime.datetime.utcnow()
295
296 extra_fixture = {'id': 3,
297 'status': 'active',
298 'is_public': True,
299 'disk_format': 'vhd',
300 'container_format': 'ovf',
301 'name': 'new name! #123',
302 'size': 19,
303 'checksum': None,
304 'created_at': time1}
305
306 glance.registry.db.api.image_create(None, extra_fixture)
307
308 extra_fixture = {'id': 4,
309 'status': 'active',
310 'is_public': True,
311 'disk_format': 'vhd',
312 'container_format': 'ovf',
313 'name': 'new name! #123',
314 'size': 20,
315 'checksum': None,
316 'created_at': time1}
317
318 glance.registry.db.api.image_create(None, extra_fixture)
319
320 extra_fixture = {'id': 5,
321 'status': 'active',
322 'is_public': True,
323 'disk_format': 'vhd',
324 'container_format': 'ovf',
325 'name': 'new name! #123',
326 'size': 20,
327 'checksum': None,
328 'created_at': time2}
329
330 glance.registry.db.api.image_create(None, extra_fixture)
331
332 req = webob.Request.blank('/images')
333 res = req.get_response(self.api)
334 res_dict = json.loads(res.body)
335 self.assertEquals(res.status_int, 200)
336
337 images = res_dict['images']
338 self.assertEquals(len(images), 4)
339 self.assertEquals(int(images[0]['id']), 4)
340 self.assertEquals(int(images[1]['id']), 3)
341 self.assertEquals(int(images[2]['id']), 5)
342 self.assertEquals(int(images[3]['id']), 2)
343
344 def test_get_index_bad_sort_key(self):
345 """Ensure a 400 is returned when a bad sort_key is provided."""
346 req = webob.Request.blank('/images?sort_key=asdf')
347 res = req.get_response(self.api)
348 self.assertEqual(400, res.status_int)
349
350 def test_get_index_bad_sort_dir(self):
351 """Ensure a 400 is returned when a bad sort_dir is provided."""
352 req = webob.Request.blank('/images?sort_dir=asdf')
353 res = req.get_response(self.api)
354 self.assertEqual(400, res.status_int)
355
356 def test_get_index_sort_id_desc(self):
357 """
358 Tests that the /images registry API returns list of
359 public images sorted by id in descending order.
360 """
361 extra_fixture = {'id': 3,
362 'status': 'active',
363 'is_public': True,
364 'disk_format': 'vhd',
365 'container_format': 'ovf',
366 'name': 'asdf',
367 'size': 19,
368 'checksum': None}
369
370 glance.registry.db.api.image_create(None, extra_fixture)
371
372 extra_fixture = {'id': 4,
373 'status': 'active',
374 'is_public': True,
375 'disk_format': 'vhd',
376 'container_format': 'ovf',
377 'name': 'xyz',
378 'size': 20,
379 'checksum': None}
380
381 glance.registry.db.api.image_create(None, extra_fixture)
382
383 req = webob.Request.blank('/images?sort_key=id&sort_dir=desc')
384 res = req.get_response(self.api)
385 self.assertEquals(res.status_int, 200)
386 res_dict = json.loads(res.body)
387
388 images = res_dict['images']
389 self.assertEquals(len(images), 3)
390 self.assertEquals(int(images[0]['id']), 4)
391 self.assertEquals(int(images[1]['id']), 3)
392 self.assertEquals(int(images[2]['id']), 2)
393
394 def test_get_index_sort_name_asc(self):
395 """
396 Tests that the /images registry API returns list of
397 public images sorted alphabetically by name in
398 ascending order.
399 """
400 extra_fixture = {'id': 3,
401 'status': 'active',
402 'is_public': True,
403 'disk_format': 'vhd',
404 'container_format': 'ovf',
405 'name': 'asdf',
406 'size': 19,
407 'checksum': None}
408
409 glance.registry.db.api.image_create(None, extra_fixture)
410
411 extra_fixture = {'id': 4,
412 'status': 'active',
413 'is_public': True,
414 'disk_format': 'vhd',
415 'container_format': 'ovf',
416 'name': 'xyz',
417 'size': 20,
418 'checksum': None}
419
420 glance.registry.db.api.image_create(None, extra_fixture)
421
422 req = webob.Request.blank('/images?sort_key=name&sort_dir=asc')
423 res = req.get_response(self.api)
424 self.assertEquals(res.status_int, 200)
425 res_dict = json.loads(res.body)
426
427 images = res_dict['images']
428 self.assertEquals(len(images), 3)
429 self.assertEquals(int(images[0]['id']), 3)
430 self.assertEquals(int(images[1]['id']), 2)
431 self.assertEquals(int(images[2]['id']), 4)
432
433 def test_get_index_sort_status_desc(self):
434 """
435 Tests that the /images registry API returns list of
436 public images sorted alphabetically by status in
437 descending order.
438 """
439 extra_fixture = {'id': 3,
440 'status': 'killed',
441 'is_public': True,
442 'disk_format': 'vhd',
443 'container_format': 'ovf',
444 'name': 'asdf',
445 'size': 19,
446 'checksum': None}
447
448 glance.registry.db.api.image_create(None, extra_fixture)
449
450 extra_fixture = {'id': 4,
451 'status': 'active',
452 'is_public': True,
453 'disk_format': 'vhd',
454 'container_format': 'ovf',
455 'name': 'xyz',
456 'size': 20,
457 'checksum': None}
458
459 glance.registry.db.api.image_create(None, extra_fixture)
460
461 req = webob.Request.blank('/images?sort_key=status&sort_dir=desc')
462 res = req.get_response(self.api)
463 self.assertEquals(res.status_int, 200)
464 res_dict = json.loads(res.body)
465
466 images = res_dict['images']
467 self.assertEquals(len(images), 3)
468 self.assertEquals(int(images[0]['id']), 3)
469 self.assertEquals(int(images[1]['id']), 4)
470 self.assertEquals(int(images[2]['id']), 2)
471
472 def test_get_index_sort_disk_format_asc(self):
473 """
474 Tests that the /images registry API returns list of
475 public images sorted alphabetically by disk_format in
476 ascending order.
477 """
478 extra_fixture = {'id': 3,
479 'status': 'active',
480 'is_public': True,
481 'disk_format': 'ami',
482 'container_format': 'ami',
483 'name': 'asdf',
484 'size': 19,
485 'checksum': None}
486
487 glance.registry.db.api.image_create(None, extra_fixture)
488
489 extra_fixture = {'id': 4,
490 'status': 'active',
491 'is_public': True,
492 'disk_format': 'vdi',
493 'container_format': 'ovf',
494 'name': 'xyz',
495 'size': 20,
496 'checksum': None}
497
498 glance.registry.db.api.image_create(None, extra_fixture)
499
500 req = webob.Request.blank('/images?sort_key=disk_format&sort_dir=asc')
501 res = req.get_response(self.api)
502 self.assertEquals(res.status_int, 200)
503 res_dict = json.loads(res.body)
504
505 images = res_dict['images']
506 self.assertEquals(len(images), 3)
507 self.assertEquals(int(images[0]['id']), 3)
508 self.assertEquals(int(images[1]['id']), 4)
509 self.assertEquals(int(images[2]['id']), 2)
510
511 def test_get_index_sort_container_format_desc(self):
512 """
513 Tests that the /images registry API returns list of
514 public images sorted alphabetically by container_format in
515 descending order.
516 """
517 extra_fixture = {'id': 3,
518 'status': 'active',
519 'is_public': True,
520 'disk_format': 'ami',
521 'container_format': 'ami',
522 'name': 'asdf',
523 'size': 19,
524 'checksum': None}
525
526 glance.registry.db.api.image_create(None, extra_fixture)
527
528 extra_fixture = {'id': 4,
529 'status': 'active',
530 'is_public': True,
531 'disk_format': 'iso',
532 'container_format': 'bare',
533 'name': 'xyz',
534 'size': 20,
535 'checksum': None}
536
537 glance.registry.db.api.image_create(None, extra_fixture)
538
539 url = '/images?sort_key=container_format&sort_dir=desc'
540 req = webob.Request.blank(url)
541 res = req.get_response(self.api)
542 self.assertEquals(res.status_int, 200)
543 res_dict = json.loads(res.body)
544
545 images = res_dict['images']
546 self.assertEquals(len(images), 3)
547 self.assertEquals(int(images[0]['id']), 2)
548 self.assertEquals(int(images[1]['id']), 4)
549 self.assertEquals(int(images[2]['id']), 3)
550
551 def test_get_index_sort_size_asc(self):
552 """
553 Tests that the /images registry API returns list of
554 public images sorted by size in ascending order.
555 """
556 extra_fixture = {'id': 3,
557 'status': 'active',
558 'is_public': True,
559 'disk_format': 'ami',
560 'container_format': 'ami',
561 'name': 'asdf',
562 'size': 100,
563 'checksum': None}
564
565 glance.registry.db.api.image_create(None, extra_fixture)
566
567 extra_fixture = {'id': 4,
568 'status': 'active',
569 'is_public': True,
570 'disk_format': 'iso',
571 'container_format': 'bare',
572 'name': 'xyz',
573 'size': 2,
574 'checksum': None}
575
576 glance.registry.db.api.image_create(None, extra_fixture)
577
578 url = '/images?sort_key=size&sort_dir=asc'
579 req = webob.Request.blank(url)
580 res = req.get_response(self.api)
581 self.assertEquals(res.status_int, 200)
582 res_dict = json.loads(res.body)
583
584 images = res_dict['images']
585 self.assertEquals(len(images), 3)
586 self.assertEquals(int(images[0]['id']), 4)
587 self.assertEquals(int(images[1]['id']), 2)
588 self.assertEquals(int(images[2]['id']), 3)
589
590 def test_get_index_sort_created_at_asc(self):
591 """
592 Tests that the /images registry API returns list of
593 public images sorted by created_at in ascending order.
594 """
595 now = datetime.datetime.utcnow()
596 time1 = now + datetime.timedelta(seconds=5)
597 time2 = now
598
599 extra_fixture = {'id': 3,
600 'status': 'active',
601 'is_public': True,
602 'disk_format': 'vhd',
603 'container_format': 'ovf',
604 'name': 'new name! #123',
605 'size': 19,
606 'checksum': None,
607 'created_at': time1}
608
609 glance.registry.db.api.image_create(None, extra_fixture)
610
611 extra_fixture = {'id': 4,
612 'status': 'active',
613 'is_public': True,
614 'disk_format': 'vhd',
615 'container_format': 'ovf',
616 'name': 'new name! #123',
617 'size': 20,
618 'checksum': None,
619 'created_at': time2}
620
621 glance.registry.db.api.image_create(None, extra_fixture)
622
623 req = webob.Request.blank('/images?sort_key=created_at&sort_dir=asc')
624 res = req.get_response(self.api)
625 self.assertEquals(res.status_int, 200)
626 res_dict = json.loads(res.body)
627
628 images = res_dict['images']
629 self.assertEquals(len(images), 3)
630 self.assertEquals(int(images[0]['id']), 2)
631 self.assertEquals(int(images[1]['id']), 4)
632 self.assertEquals(int(images[2]['id']), 3)
633
634 def test_get_index_sort_updated_at_desc(self):
635 """
636 Tests that the /images registry API returns list of
637 public images sorted by updated_at in descending order.
638 """
639 now = datetime.datetime.utcnow()
640 time1 = now + datetime.timedelta(seconds=5)
641 time2 = now
642
643 extra_fixture = {'id': 3,
644 'status': 'active',
645 'is_public': True,
646 'disk_format': 'vhd',
647 'container_format': 'ovf',
648 'name': 'new name! #123',
649 'size': 19,
650 'checksum': None,
651 'created_at': None,
652 'created_at': time1}
653
654 glance.registry.db.api.image_create(None, extra_fixture)
655
656 extra_fixture = {'id': 4,
657 'status': 'active',
658 'is_public': True,
659 'disk_format': 'vhd',
660 'container_format': 'ovf',
661 'name': 'new name! #123',
662 'size': 20,
663 'checksum': None,
664 'created_at': None,
665 'updated_at': time2}
666
667 glance.registry.db.api.image_create(None, extra_fixture)
668
669 req = webob.Request.blank('/images?sort_key=updated_at&sort_dir=desc')
670 res = req.get_response(self.api)
671 self.assertEquals(res.status_int, 200)
672 res_dict = json.loads(res.body)
673
674 images = res_dict['images']
675 self.assertEquals(len(images), 3)
676 self.assertEquals(int(images[0]['id']), 3)
677 self.assertEquals(int(images[1]['id']), 4)
678 self.assertEquals(int(images[2]['id']), 2)
679
288 def test_get_details(self):680 def test_get_details(self):
289 """Tests that the /images/detail registry API returns681 """Tests that the /images/detail registry API returns
290 a mapping containing a list of detailed image information682 a mapping containing a list of detailed image information
@@ -668,6 +1060,45 @@
668 for image in images:1060 for image in images:
669 self.assertEqual('v a', image['properties']['prop_123'])1061 self.assertEqual('v a', image['properties']['prop_123'])
6701062
1063 def test_get_details_sort_name_asc(self):
1064 """
1065 Tests that the /images/details registry API returns list of
1066 public images sorted alphabetically by name in
1067 ascending order.
1068 """
1069 extra_fixture = {'id': 3,
1070 'status': 'active',
1071 'is_public': True,
1072 'disk_format': 'vhd',
1073 'container_format': 'ovf',
1074 'name': 'asdf',
1075 'size': 19,
1076 'checksum': None}
1077
1078 glance.registry.db.api.image_create(None, extra_fixture)
1079
1080 extra_fixture = {'id': 4,
1081 'status': 'active',
1082 'is_public': True,
1083 'disk_format': 'vhd',
1084 'container_format': 'ovf',
1085 'name': 'xyz',
1086 'size': 20,
1087 'checksum': None}
1088
1089 glance.registry.db.api.image_create(None, extra_fixture)
1090
1091 req = webob.Request.blank('/images/detail?sort_key=name&sort_dir=asc')
1092 res = req.get_response(self.api)
1093 self.assertEquals(res.status_int, 200)
1094 res_dict = json.loads(res.body)
1095
1096 images = res_dict['images']
1097 self.assertEquals(len(images), 3)
1098 self.assertEquals(int(images[0]['id']), 3)
1099 self.assertEquals(int(images[1]['id']), 2)
1100 self.assertEquals(int(images[2]['id']), 4)
1101
671 def test_create_image(self):1102 def test_create_image(self):
672 """Tests that the /images POST registry API creates the image"""1103 """Tests that the /images POST registry API creates the image"""
673 fixture = {'name': 'fake public image',1104 fixture = {'name': 'fake public image',
@@ -1042,6 +1473,45 @@
1042 "res.headerlist = %r" % res.headerlist)1473 "res.headerlist = %r" % res.headerlist)
1043 self.assertTrue('/images/3' in res.headers['location'])1474 self.assertTrue('/images/3' in res.headers['location'])
10441475
1476 def test_get_index_sort_name_asc(self):
1477 """
1478 Tests that the /images registry API returns list of
1479 public images sorted alphabetically by name in
1480 ascending order.
1481 """
1482 extra_fixture = {'id': 3,
1483 'status': 'active',
1484 'is_public': True,
1485 'disk_format': 'vhd',
1486 'container_format': 'ovf',
1487 'name': 'asdf',
1488 'size': 19,
1489 'checksum': None}
1490
1491 glance.registry.db.api.image_create(None, extra_fixture)
1492
1493 extra_fixture = {'id': 4,
1494 'status': 'active',
1495 'is_public': True,
1496 'disk_format': 'vhd',
1497 'container_format': 'ovf',
1498 'name': 'xyz',
1499 'size': 20,
1500 'checksum': None}
1501
1502 glance.registry.db.api.image_create(None, extra_fixture)
1503
1504 req = webob.Request.blank('/images?sort_key=name&sort_dir=asc')
1505 res = req.get_response(self.api)
1506 self.assertEquals(res.status_int, 200)
1507 res_dict = json.loads(res.body)
1508
1509 images = res_dict['images']
1510 self.assertEquals(len(images), 3)
1511 self.assertEquals(int(images[0]['id']), 3)
1512 self.assertEquals(int(images[1]['id']), 2)
1513 self.assertEquals(int(images[2]['id']), 4)
1514
1045 def test_image_is_checksummed(self):1515 def test_image_is_checksummed(self):
1046 """Test that the image contents are checksummed properly"""1516 """Test that the image contents are checksummed properly"""
1047 fixture_headers = {'x-image-meta-store': 'file',1517 fixture_headers = {'x-image-meta-store': 'file',
10481518
=== modified file 'tests/unit/test_clients.py'
--- tests/unit/test_clients.py 2011-05-31 19:32:17 +0000
+++ tests/unit/test_clients.py 2011-06-28 13:52:02 +0000
@@ -15,6 +15,7 @@
15# License for the specific language governing permissions and limitations15# License for the specific language governing permissions and limitations
16# under the License.16# under the License.
1717
18import datetime
18import json19import json
19import os20import os
20import stubout21import stubout
@@ -37,7 +38,7 @@
37 def test_bad_address(self):38 def test_bad_address(self):
38 """Test ClientConnectionError raised"""39 """Test ClientConnectionError raised"""
39 c = client.Client("127.999.1.1")40 c = client.Client("127.999.1.1")
40 self.assertRaises(client.ClientConnectionError,41 self.assertRaises(exception.ClientConnectionError,
41 c.get_image,42 c.get_image,
42 1)43 1)
4344
@@ -70,6 +71,298 @@
70 for k, v in fixture.items():71 for k, v in fixture.items():
71 self.assertEquals(v, images[0][k])72 self.assertEquals(v, images[0][k])
7273
74 def test_get_index_sort_id_desc(self):
75 """
76 Tests that the /images registry API returns list of
77 public images sorted by id in descending order.
78 """
79 extra_fixture = {'id': 3,
80 'status': 'active',
81 'is_public': True,
82 'disk_format': 'vhd',
83 'container_format': 'ovf',
84 'name': 'asdf',
85 'size': 19,
86 'checksum': None}
87
88 glance.registry.db.api.image_create(None, extra_fixture)
89
90 extra_fixture = {'id': 4,
91 'status': 'active',
92 'is_public': True,
93 'disk_format': 'vhd',
94 'container_format': 'ovf',
95 'name': 'xyz',
96 'size': 20,
97 'checksum': None}
98
99 glance.registry.db.api.image_create(None, extra_fixture)
100
101 images = self.client.get_images(sort_key='id', sort_dir='desc')
102
103 self.assertEquals(len(images), 3)
104 self.assertEquals(int(images[0]['id']), 4)
105 self.assertEquals(int(images[1]['id']), 3)
106 self.assertEquals(int(images[2]['id']), 2)
107
108 def test_get_index_sort_name_asc(self):
109 """
110 Tests that the /images registry API returns list of
111 public images sorted alphabetically by name in
112 ascending order.
113 """
114 extra_fixture = {'id': 3,
115 'status': 'active',
116 'is_public': True,
117 'disk_format': 'vhd',
118 'container_format': 'ovf',
119 'name': 'asdf',
120 'size': 19,
121 'checksum': None}
122
123 glance.registry.db.api.image_create(None, extra_fixture)
124
125 extra_fixture = {'id': 4,
126 'status': 'active',
127 'is_public': True,
128 'disk_format': 'vhd',
129 'container_format': 'ovf',
130 'name': 'xyz',
131 'size': 20,
132 'checksum': None}
133
134 glance.registry.db.api.image_create(None, extra_fixture)
135
136 images = self.client.get_images(sort_key='name', sort_dir='asc')
137
138 self.assertEquals(len(images), 3)
139 self.assertEquals(int(images[0]['id']), 3)
140 self.assertEquals(int(images[1]['id']), 2)
141 self.assertEquals(int(images[2]['id']), 4)
142
143 def test_get_index_sort_status_desc(self):
144 """
145 Tests that the /images registry API returns list of
146 public images sorted alphabetically by status in
147 descending order.
148 """
149 extra_fixture = {'id': 3,
150 'status': 'killed',
151 'is_public': True,
152 'disk_format': 'vhd',
153 'container_format': 'ovf',
154 'name': 'asdf',
155 'size': 19,
156 'checksum': None}
157
158 glance.registry.db.api.image_create(None, extra_fixture)
159
160 extra_fixture = {'id': 4,
161 'status': 'active',
162 'is_public': True,
163 'disk_format': 'vhd',
164 'container_format': 'ovf',
165 'name': 'xyz',
166 'size': 20,
167 'checksum': None}
168
169 glance.registry.db.api.image_create(None, extra_fixture)
170
171 images = self.client.get_images(sort_key='status', sort_dir='desc')
172
173 self.assertEquals(len(images), 3)
174 self.assertEquals(int(images[0]['id']), 3)
175 self.assertEquals(int(images[1]['id']), 4)
176 self.assertEquals(int(images[2]['id']), 2)
177
178 def test_get_index_sort_disk_format_asc(self):
179 """
180 Tests that the /images registry API returns list of
181 public images sorted alphabetically by disk_format in
182 ascending order.
183 """
184 extra_fixture = {'id': 3,
185 'status': 'active',
186 'is_public': True,
187 'disk_format': 'ami',
188 'container_format': 'ami',
189 'name': 'asdf',
190 'size': 19,
191 'checksum': None}
192
193 glance.registry.db.api.image_create(None, extra_fixture)
194
195 extra_fixture = {'id': 4,
196 'status': 'active',
197 'is_public': True,
198 'disk_format': 'vdi',
199 'container_format': 'ovf',
200 'name': 'xyz',
201 'size': 20,
202 'checksum': None}
203
204 glance.registry.db.api.image_create(None, extra_fixture)
205
206 images = self.client.get_images(sort_key='disk_format',
207 sort_dir='asc')
208
209 self.assertEquals(len(images), 3)
210 self.assertEquals(int(images[0]['id']), 3)
211 self.assertEquals(int(images[1]['id']), 4)
212 self.assertEquals(int(images[2]['id']), 2)
213
214 def test_get_index_sort_container_format_desc(self):
215 """
216 Tests that the /images registry API returns list of
217 public images sorted alphabetically by container_format in
218 descending order.
219 """
220 extra_fixture = {'id': 3,
221 'status': 'active',
222 'is_public': True,
223 'disk_format': 'ami',
224 'container_format': 'ami',
225 'name': 'asdf',
226 'size': 19,
227 'checksum': None}
228
229 glance.registry.db.api.image_create(None, extra_fixture)
230
231 extra_fixture = {'id': 4,
232 'status': 'active',
233 'is_public': True,
234 'disk_format': 'iso',
235 'container_format': 'bare',
236 'name': 'xyz',
237 'size': 20,
238 'checksum': None}
239
240 glance.registry.db.api.image_create(None, extra_fixture)
241
242 images = self.client.get_images(sort_key='container_format',
243 sort_dir='desc')
244
245 self.assertEquals(len(images), 3)
246 self.assertEquals(int(images[0]['id']), 2)
247 self.assertEquals(int(images[1]['id']), 4)
248 self.assertEquals(int(images[2]['id']), 3)
249
250 def test_get_index_sort_size_asc(self):
251 """
252 Tests that the /images registry API returns list of
253 public images sorted by size in ascending order.
254 """
255 extra_fixture = {'id': 3,
256 'status': 'active',
257 'is_public': True,
258 'disk_format': 'ami',
259 'container_format': 'ami',
260 'name': 'asdf',
261 'size': 100,
262 'checksum': None}
263
264 glance.registry.db.api.image_create(None, extra_fixture)
265
266 extra_fixture = {'id': 4,
267 'status': 'active',
268 'is_public': True,
269 'disk_format': 'iso',
270 'container_format': 'bare',
271 'name': 'xyz',
272 'size': 2,
273 'checksum': None}
274
275 glance.registry.db.api.image_create(None, extra_fixture)
276
277 images = self.client.get_images(sort_key='size', sort_dir='asc')
278
279 self.assertEquals(len(images), 3)
280 self.assertEquals(int(images[0]['id']), 4)
281 self.assertEquals(int(images[1]['id']), 2)
282 self.assertEquals(int(images[2]['id']), 3)
283
284 def test_get_index_sort_created_at_asc(self):
285 """
286 Tests that the /images registry API returns list of
287 public images sorted by created_at in ascending order.
288 """
289 now = datetime.datetime.utcnow()
290 time1 = now + datetime.timedelta(seconds=5)
291 time2 = now
292
293 extra_fixture = {'id': 3,
294 'status': 'active',
295 'is_public': True,
296 'disk_format': 'vhd',
297 'container_format': 'ovf',
298 'name': 'new name! #123',
299 'size': 19,
300 'checksum': None,
301 'created_at': time1}
302
303 glance.registry.db.api.image_create(None, extra_fixture)
304
305 extra_fixture = {'id': 4,
306 'status': 'active',
307 'is_public': True,
308 'disk_format': 'vhd',
309 'container_format': 'ovf',
310 'name': 'new name! #123',
311 'size': 20,
312 'checksum': None,
313 'created_at': time2}
314
315 glance.registry.db.api.image_create(None, extra_fixture)
316
317 images = self.client.get_images(sort_key='created_at', sort_dir='asc')
318
319 self.assertEquals(len(images), 3)
320 self.assertEquals(int(images[0]['id']), 2)
321 self.assertEquals(int(images[1]['id']), 4)
322 self.assertEquals(int(images[2]['id']), 3)
323
324 def test_get_index_sort_updated_at_desc(self):
325 """
326 Tests that the /images registry API returns list of
327 public images sorted by updated_at in descending order.
328 """
329 now = datetime.datetime.utcnow()
330 time1 = now + datetime.timedelta(seconds=5)
331 time2 = now
332
333 extra_fixture = {'id': 3,
334 'status': 'active',
335 'is_public': True,
336 'disk_format': 'vhd',
337 'container_format': 'ovf',
338 'name': 'new name! #123',
339 'size': 19,
340 'checksum': None,
341 'created_at': None,
342 'created_at': time1}
343
344 glance.registry.db.api.image_create(None, extra_fixture)
345
346 extra_fixture = {'id': 4,
347 'status': 'active',
348 'is_public': True,
349 'disk_format': 'vhd',
350 'container_format': 'ovf',
351 'name': 'new name! #123',
352 'size': 20,
353 'checksum': None,
354 'created_at': None,
355 'updated_at': time2}
356
357 glance.registry.db.api.image_create(None, extra_fixture)
358
359 images = self.client.get_images(sort_key='updated_at', sort_dir='desc')
360
361 self.assertEquals(len(images), 3)
362 self.assertEquals(int(images[0]['id']), 3)
363 self.assertEquals(int(images[1]['id']), 4)
364 self.assertEquals(int(images[2]['id']), 2)
365
73 def test_get_image_index_marker(self):366 def test_get_image_index_marker(self):
74 """Test correct set of images returned with marker param."""367 """Test correct set of images returned with marker param."""
75 extra_fixture = {'id': 3,368 extra_fixture = {'id': 3,
@@ -170,7 +463,7 @@
170463
171 glance.registry.db.api.image_create(None, extra_fixture)464 glance.registry.db.api.image_create(None, extra_fixture)
172465
173 images = self.client.get_images({'name': 'new name! #123'})466 images = self.client.get_images(filters={'name': 'new name! #123'})
174 self.assertEquals(len(images), 1)467 self.assertEquals(len(images), 1)
175468
176 for image in images:469 for image in images:
@@ -236,7 +529,8 @@
236529
237 glance.registry.db.api.image_create(None, extra_fixture)530 glance.registry.db.api.image_create(None, extra_fixture)
238531
239 images = self.client.get_images_detailed({'name': 'new name! #123'})532 filters = {'name': 'new name! #123'}
533 images = self.client.get_images_detailed(filters=filters)
240 self.assertEquals(len(images), 1)534 self.assertEquals(len(images), 1)
241535
242 for image in images:536 for image in images:
@@ -255,7 +549,7 @@
255549
256 glance.registry.db.api.image_create(None, extra_fixture)550 glance.registry.db.api.image_create(None, extra_fixture)
257551
258 images = self.client.get_images_detailed({'status': 'saving'})552 images = self.client.get_images_detailed(filters={'status': 'saving'})
259 self.assertEquals(len(images), 1)553 self.assertEquals(len(images), 1)
260554
261 for image in images:555 for image in images:
@@ -274,7 +568,8 @@
274568
275 glance.registry.db.api.image_create(None, extra_fixture)569 glance.registry.db.api.image_create(None, extra_fixture)
276570
277 images = self.client.get_images_detailed({'container_format': 'ovf'})571 filters = {'container_format': 'ovf'}
572 images = self.client.get_images_detailed(filters=filters)
278 self.assertEquals(len(images), 2)573 self.assertEquals(len(images), 2)
279574
280 for image in images:575 for image in images:
@@ -293,7 +588,8 @@
293588
294 glance.registry.db.api.image_create(None, extra_fixture)589 glance.registry.db.api.image_create(None, extra_fixture)
295590
296 images = self.client.get_images_detailed({'disk_format': 'vhd'})591 filters = {'disk_format': 'vhd'}
592 images = self.client.get_images_detailed(filters=filters)
297 self.assertEquals(len(images), 2)593 self.assertEquals(len(images), 2)
298594
299 for image in images:595 for image in images:
@@ -312,7 +608,7 @@
312608
313 glance.registry.db.api.image_create(None, extra_fixture)609 glance.registry.db.api.image_create(None, extra_fixture)
314610
315 images = self.client.get_images_detailed({'size_max': 20})611 images = self.client.get_images_detailed(filters={'size_max': 20})
316 self.assertEquals(len(images), 1)612 self.assertEquals(len(images), 1)
317613
318 for image in images:614 for image in images:
@@ -331,7 +627,7 @@
331627
332 glance.registry.db.api.image_create(None, extra_fixture)628 glance.registry.db.api.image_create(None, extra_fixture)
333629
334 images = self.client.get_images_detailed({'size_min': 20})630 images = self.client.get_images_detailed(filters={'size_min': 20})
335 self.assertEquals(len(images), 1)631 self.assertEquals(len(images), 1)
336632
337 for image in images:633 for image in images:
@@ -351,12 +647,49 @@
351647
352 glance.registry.db.api.image_create(None, extra_fixture)648 glance.registry.db.api.image_create(None, extra_fixture)
353649
354 images = self.client.get_images_detailed({'property-p a': 'v a'})650 filters = {'property-p a': 'v a'}
651 images = self.client.get_images_detailed(filters=filters)
355 self.assertEquals(len(images), 1)652 self.assertEquals(len(images), 1)
356653
357 for image in images:654 for image in images:
358 self.assertEquals('v a', image['properties']['p a'])655 self.assertEquals('v a', image['properties']['p a'])
359656
657 def test_get_image_details_sort_disk_format_asc(self):
658 """
659 Tests that a detailed call returns list of
660 public images sorted alphabetically by disk_format in
661 ascending order.
662 """
663 extra_fixture = {'id': 3,
664 'status': 'active',
665 'is_public': True,
666 'disk_format': 'ami',
667 'container_format': 'ami',
668 'name': 'asdf',
669 'size': 19,
670 'checksum': None}
671
672 glance.registry.db.api.image_create(None, extra_fixture)
673
674 extra_fixture = {'id': 4,
675 'status': 'active',
676 'is_public': True,
677 'disk_format': 'vdi',
678 'container_format': 'ovf',
679 'name': 'xyz',
680 'size': 20,
681 'checksum': None}
682
683 glance.registry.db.api.image_create(None, extra_fixture)
684
685 images = self.client.get_images_detailed(sort_key='disk_format',
686 sort_dir='asc')
687
688 self.assertEquals(len(images), 3)
689 self.assertEquals(int(images[0]['id']), 3)
690 self.assertEquals(int(images[1]['id']), 4)
691 self.assertEquals(int(images[2]['id']), 2)
692
360 def test_get_image(self):693 def test_get_image(self):
361 """Tests that the detailed info about an image returned"""694 """Tests that the detailed info about an image returned"""
362 fixture = {'id': 1,695 fixture = {'id': 1,
@@ -562,6 +895,42 @@
562 self.client.get_image,895 self.client.get_image,
563 3)896 3)
564897
898 def test_get_image_index_sort_container_format_desc(self):
899 """
900 Tests that the client returns list of public images
901 sorted alphabetically by container_format in
902 descending order.
903 """
904 extra_fixture = {'id': 3,
905 'status': 'active',
906 'is_public': True,
907 'disk_format': 'ami',
908 'container_format': 'ami',
909 'name': 'asdf',
910 'size': 19,
911 'checksum': None}
912
913 glance.registry.db.api.image_create(None, extra_fixture)
914
915 extra_fixture = {'id': 4,
916 'status': 'active',
917 'is_public': True,
918 'disk_format': 'iso',
919 'container_format': 'bare',
920 'name': 'xyz',
921 'size': 20,
922 'checksum': None}
923
924 glance.registry.db.api.image_create(None, extra_fixture)
925
926 images = self.client.get_images(sort_key='container_format',
927 sort_dir='desc')
928
929 self.assertEquals(len(images), 3)
930 self.assertEquals(int(images[0]['id']), 2)
931 self.assertEquals(int(images[1]['id']), 4)
932 self.assertEquals(int(images[2]['id']), 3)
933
565 def test_get_image_index(self):934 def test_get_image_index(self):
566 """Test correct set of public image returned"""935 """Test correct set of public image returned"""
567 fixture = {'id': 2,936 fixture = {'id': 2,
@@ -671,7 +1040,8 @@
6711040
672 glance.registry.db.api.image_create(None, extra_fixture)1041 glance.registry.db.api.image_create(None, extra_fixture)
6731042
674 images = self.client.get_images({'name': 'new name! #123'})1043 filters = {'name': 'new name! #123'}
1044 images = self.client.get_images(filters=filters)
6751045
676 self.assertEquals(len(images), 1)1046 self.assertEquals(len(images), 1)
677 self.assertEquals('new name! #123', images[0]['name'])1047 self.assertEquals('new name! #123', images[0]['name'])
@@ -690,7 +1060,8 @@
6901060
691 glance.registry.db.api.image_create(None, extra_fixture)1061 glance.registry.db.api.image_create(None, extra_fixture)
6921062
693 images = self.client.get_images({'property-p a': 'v a'})1063 filters = {'property-p a': 'v a'}
1064 images = self.client.get_images(filters=filters)
6941065
695 self.assertEquals(len(images), 1)1066 self.assertEquals(len(images), 1)
696 self.assertEquals(3, images[0]['id'])1067 self.assertEquals(3, images[0]['id'])
@@ -765,7 +1136,8 @@
7651136
766 glance.registry.db.api.image_create(None, extra_fixture)1137 glance.registry.db.api.image_create(None, extra_fixture)
7671138
768 images = self.client.get_images_detailed({'name': 'new name! #123'})1139 filters = {'name': 'new name! #123'}
1140 images = self.client.get_images_detailed(filters=filters)
769 self.assertEquals(len(images), 1)1141 self.assertEquals(len(images), 1)
7701142
771 for image in images:1143 for image in images:
@@ -785,7 +1157,8 @@
7851157
786 glance.registry.db.api.image_create(None, extra_fixture)1158 glance.registry.db.api.image_create(None, extra_fixture)
7871159
788 images = self.client.get_images_detailed({'property-p a': 'v a'})1160 filters = {'property-p a': 'v a'}
1161 images = self.client.get_images_detailed(filters=filters)
789 self.assertEquals(len(images), 1)1162 self.assertEquals(len(images), 1)
7901163
791 for image in images:1164 for image in images:

Subscribers

People subscribed via source and target branches