Merge lp:~rackspace-titan/glance/api-pagination-limiting into lp:~hudson-openstack/glance/trunk
- api-pagination-limiting
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Dan Prince |
Approved revision: | 149 |
Merged at revision: | 139 |
Proposed branch: | lp:~rackspace-titan/glance/api-pagination-limiting |
Merge into: | lp:~hudson-openstack/glance/trunk |
Prerequisite: | lp:~rackspace-titan/glance/api-results-filtering |
Diff against target: |
991 lines (+712/-33) 10 files modified
glance/api/v1/images.py (+18/-4) glance/client.py (+26/-4) glance/registry/__init__.py (+8/-8) glance/registry/client.py (+28/-4) glance/registry/db/api.py (+20/-5) glance/registry/server.py (+40/-2) tests/functional/test_curl_api.py (+116/-0) tests/stubs.py (+33/-6) tests/unit/test_api.py (+193/-0) tests/unit/test_clients.py (+230/-0) |
To merge this branch: | bzr merge lp:~rackspace-titan/glance/api-pagination-limiting |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mark Washenberger (community) | Approve | ||
Dan Prince (community) | Approve | ||
Jay Pipes (community) | Approve | ||
Review via email: mp+61155@code.launchpad.net |
Commit message
Description of the change
Adding support for marker/limit query params from api, through registry client/api, and implementing at registry db api layer
Brian Waldon (bcwaldon) wrote : | # |
> Hi Brian,
>
> Once again, great work. Couple little things:
>
> 1) Something I did not think about for the results filtering MP, as well as
> this one, is that we will want to bump the minor version since we are adding
> functionality to the API in these two MPs.
>
> You can do this in a separate merge proposal, I suppose. Basically, what needs
> to happen is that we need a way of describing the 1.1 API (1.0 + filters and
> limits) in the /versions endpoint.
>
> The reason this is important is that the API result for /images has changed:
> we go from returning ALL public images in no particular order to returning
> SOME public images in a particular order (by created_at DESC). So, we should
> bump the minor version of the API to indicate to clients that this has been
> done.
>
> Since we are in the middle of the D1 milestone, I'm not too concerned about
> having the results-filter branch, then this branch, then the API 1.1 branch
> going into trunk in that particular order. I'll handle the versioning stuff
> next week when I return from vacation.
According to the last versioning-related discussion we had, these changes were going to be part of v1.0. We haven't cut an official v1.0 release yet, and the limiting changes are definitely a major version change. I say we let the filtering and pagination branches in then call it v1.0. Any new feature (sets?) bump the version.
> 2) I'd prefer to set the default limit to < 1000. I'm thinking 25 or 50 would
> be a more appropriate default limit. Also, please make this a constant
> somewhere, not just a number in the method signature...
I meant to do that before I pushed the branch. Now it's a constant in registry/server.py, set to 25
> 3) Unless I'm mistaken these functions:
>
> 50 +def limit_collectio
> 243 + def _limit_
>
> in glance.common.wsgi and glance.
> removed. You've pushed (correctly) the limiting and offset down into the
> registry db query.
Good catch. Removed them.
> 4) This:
>
> 114 if filters != None:
> 115 - action = "/images?%s" % urllib.
> 116 + params = filters
> 117 else:
> 118 - action = "/images"
> 119 + params = {}
>
> Might be better as:
>
> params = filters or {}
Yep. Did this in a bunch of places.
> 5)
>
> 184 + order_by(
>
> I think I would prefer:
>
> order_by(
Done. I kept the secondary order_by id for predictability in the behavior of marker b/c created_at is not unique.
Shoot for custom ordering in a future milestone (for v1.1)?
> 6)
>
> 192 + if marker != None:
> 193 + query = query.filter(
>
> Should be:
>
> if marker != None:
> query = query.offset(
>
> That way, SQLAlchemy will pass the marker as-is to the query as a LIMIT X
> OFFSET Y. This allows more than just the Image.id column to be used for
> ordering, and will allow you to add custom ordering to the API ;)
Actually, the term "offset" means something different than "marker". Offset is an index in a list of images, while marker is the actual id of ...
Jay Pipes (jaypipes) wrote : | # |
On Tue, May 17, 2011 at 8:42 PM, Brian Waldon
<email address hidden> wrote:
> According to the last versioning-related discussion we had, these changes were going to be part of v1.0. We haven't cut an official v1.0 release yet, and the limiting changes are definitely a major version change. I say we let the filtering and pagination branches in then call it v1.0. Any new feature (sets?) bump the version.
Cool, yep, that sounds fine.
>> 2) I'd prefer to set the default limit to < 1000. I'm thinking 25 or 50 would
>> be a more appropriate default limit. Also, please make this a constant
>> somewhere, not just a number in the method signature...
>
> I meant to do that before I pushed the branch. Now it's a constant in registry/server.py, set to 25
Cheers.
>> 3) Unless I'm mistaken these functions:
>>
>> 50 +def limit_collectio
>> 243 + def _limit_
>>
>> in glance.common.wsgi and glance.
>> removed. You've pushed (correctly) the limiting and offset down into the
>> registry db query.
>
> Good catch. Removed them.
Cool.
>> 4) This:
>>
>> 114 if filters != None:
>> 115 - action = "/images?%s" % urllib.
>> 116 + params = filters
>> 117 else:
>> 118 - action = "/images"
>> 119 + params = {}
>>
>> Might be better as:
>>
>> params = filters or {}
>
> Yep. Did this in a bunch of places.
Thanks.
>> 5)
>>
>> 184 + order_by(
>>
>> I think I would prefer:
>>
>> order_by(
>
> Done. I kept the secondary order_by id for predictability in the behavior of marker b/c created_at is not unique.
Hmm, so you actually don't need created_at to be unique if you replace
the concept of marker with that of offset. See more below...
> Shoot for custom ordering in a future milestone (for v1.1)?
Cool with me :)
>> 6)
>>
>> 192 + if marker != None:
>> 193 + query = query.filter(
>>
>> Should be:
>>
>> if marker != None:
>> query = query.offset(
>>
>> That way, SQLAlchemy will pass the marker as-is to the query as a LIMIT X
>> OFFSET Y. This allows more than just the Image.id column to be used for
>> ordering, and will allow you to add custom ordering to the API ;)
>
> Actually, the term "offset" means something different than "marker". Offset is an index in a list of images, while marker is the actual id of an image in that list. I did have to change the code to support the new created_at sort, but I kept the original functionality.
I understand that marker is different from offset. I was recommending
that instead of using the concept of a marker, that you use the
concept of an offset. A marker isn't particularly useful unless you
have do ordering with a unique column. In addition, using a marker
requires you to do the pagination in code, whereas using an offset
allows you to do the pagination in the SQL query itself, which will be
much more efficient than in Python.
The code I demonstrated above would send the offset to the SQL query
and allow you to do pagination in the query instead of this:
398 + images = sorted(images, k...
Brian Waldon (bcwaldon) wrote : | # |
I updated the code to actually implement marker correctly. This branch should implement what was agreed upon (or proposed and waiting to be approved?) on the mailing list:
Quoted from that email:
1) Push the LIMIT variable into the database API layer
2) Ensure that all queries that return a set of results have an ORDER
BY expression to them
3) Push the marker into the database API layer. Continue to have the
marker variable be a value of a unique key (primary key for now at
least). Use a WHERE field > $marker LIMIT $pagesize construct.
Brian Waldon (bcwaldon) wrote : | # |
sqlalchemy query is still bad. I didn't test it thoroughly enough
Brian Waldon (bcwaldon) wrote : | # |
Washenberger helped me out. Should work now. Blame him if it doesn't.
Jay Pipes (jaypipes) wrote : | # |
Heya!
Lookin' good...
One thing though; we decided on the ML thread that we would *not* do the additional filter on the created_at date; that the possibility of a page of results changing over time was not a big deal and the marker would just be used as the start of the subset.
So, this code:
173 + # images returned should be created before the image defined by marker
174 + marker_created_at = image_get(context, marker).created_at
175 + query = query.filter(
176 + or_(models.
177 + and_(models.
178 + models.Image.id < marker)))
Should just be:
query = query.filter(
marker should be the primary key (or a unique index) of the last record on the previous page, or None for the first page.
Make sense?
-jay
Brian Waldon (bcwaldon) wrote : | # |
> One thing though; we decided on the ML thread that we would *not* do the
> additional filter on the created_at date; that the possibility of a page of
> results changing over time was not a big deal and the marker would just be
> used as the start of the subset.
[snip snip]
> query = query.filter(
>
> marker should be the primary key (or a unique index) of the last record on the
> previous page, or None for the first page.
So are you suggesting we sort by id asc (assuming your comparison isn't a typo)? My code does impose a default sort of created_at desc.
Jay Pipes (jaypipes) wrote : | # |
> So are you suggesting we sort by id asc (assuming your comparison isn't a
> typo)? My code does impose a default sort of created_at desc.
Hmm, good point. Should have thought of that... :)
I'd keep the default order as ORDER BY created_at DESC, id DESC, but change the WHERE id > $marker to the other direction: WHERE id < $marker. When we do custom ordering, the direction (> or <) will be determined by the direction of the sort order. Am I correct in that thinking?
-jay
Brian Waldon (bcwaldon) wrote : | # |
> I'd keep the default order as ORDER BY created_at DESC, id DESC, but change
> the WHERE id > $marker to the other direction: WHERE id < $marker. When we do
> custom ordering, the direction (> or <) will be determined by the direction of
> the sort order. Am I correct in that thinking?
Yes sir. So I think what I have should be good, assuming you understand why I am first doing a comparison on created_at.
Jay Pipes (jaypipes) wrote : | # |
> > I'd keep the default order as ORDER BY created_at DESC, id DESC, but change
> > the WHERE id > $marker to the other direction: WHERE id < $marker. When we
> do
> > custom ordering, the direction (> or <) will be determined by the direction
> of
> > the sort order. Am I correct in that thinking?
>
> Yes sir. So I think what I have should be good, assuming you understand why I
> am first doing a comparison on created_at.
Yep.
And you understand in the future for custom ordering, you only need to do the or_ expression when the order is on a field that does not have a unique index :)
-jay
Brian Waldon (bcwaldon) wrote : | # |
Added support to glance/client.py
Dan Prince (dan-prince) wrote : | # |
Looks good to me.
Mark Washenberger (markwash) wrote : | # |
This sqlalchemy looks exactly like what we talked about. Clear eyes full hearts can't lose.
Preview Diff
1 | === modified file 'glance/api/v1/images.py' |
2 | --- glance/api/v1/images.py 2011-05-17 13:27:09 +0000 |
3 | +++ glance/api/v1/images.py 2011-05-31 13:23:37 +0000 |
4 | @@ -92,8 +92,15 @@ |
5 | 'size': <SIZE>}, ... |
6 | ]} |
7 | """ |
8 | - filters = self._get_filters(req) |
9 | - images = registry.get_images_list(self.options, filters) |
10 | + params = {'filters': self._get_filters(req)} |
11 | + |
12 | + if 'limit' in req.str_params: |
13 | + params['limit'] = req.str_params.get('limit') |
14 | + |
15 | + if 'marker' in req.str_params: |
16 | + params['marker'] = req.str_params.get('marker') |
17 | + |
18 | + images = registry.get_images_list(self.options, **params) |
19 | return dict(images=images) |
20 | |
21 | def detail(self, req): |
22 | @@ -118,8 +125,15 @@ |
23 | 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ... |
24 | ]} |
25 | """ |
26 | - filters = self._get_filters(req) |
27 | - images = registry.get_images_detail(self.options, filters) |
28 | + params = {'filters': self._get_filters(req)} |
29 | + |
30 | + if 'limit' in req.str_params: |
31 | + params['limit'] = req.str_params.get('limit') |
32 | + |
33 | + if 'marker' in req.str_params: |
34 | + params['marker'] = req.str_params.get('marker') |
35 | + |
36 | + images = registry.get_images_detail(self.options, **params) |
37 | return dict(images=images) |
38 | |
39 | def _get_filters(self, req): |
40 | |
41 | === modified file 'glance/client.py' |
42 | --- glance/client.py 2011-05-24 19:47:59 +0000 |
43 | +++ glance/client.py 2011-05-31 13:23:37 +0000 |
44 | @@ -209,19 +209,41 @@ |
45 | return super(V1Client, self).do_request(method, action, body, |
46 | headers, params) |
47 | |
48 | - def get_images(self, filters=None): |
49 | + def get_images(self, filters=None, marker=None, limit=None): |
50 | """ |
51 | Returns a list of image id/name mappings from Registry |
52 | + |
53 | + :param filters: dictionary of attributes by which the resulting |
54 | + collection of images should be filtered |
55 | + :param marker: id after which to start the page of images |
56 | + :param limit: maximum number of items to return |
57 | """ |
58 | - res = self.do_request("GET", "/images", params=filters) |
59 | + |
60 | + params = filters or {} |
61 | + if marker: |
62 | + params['marker'] = marker |
63 | + if limit: |
64 | + params['limit'] = limit |
65 | + res = self.do_request("GET", "/images", params=params) |
66 | data = json.loads(res.read())['images'] |
67 | return data |
68 | |
69 | - def get_images_detailed(self, filters=None): |
70 | + def get_images_detailed(self, filters=None, marker=None, limit=None): |
71 | """ |
72 | Returns a list of detailed image data mappings from Registry |
73 | + |
74 | + :param filters: dictionary of attributes by which the resulting |
75 | + collection of images should be filtered |
76 | + :param marker: id after which to start the page of images |
77 | + :param limit: maximum number of items to return |
78 | """ |
79 | - res = self.do_request("GET", "/images/detail", params=filters) |
80 | + |
81 | + params = filters or {} |
82 | + if marker: |
83 | + params['marker'] = marker |
84 | + if limit: |
85 | + params['limit'] = limit |
86 | + res = self.do_request("GET", "/images/detail", params=params) |
87 | data = json.loads(res.read())['images'] |
88 | return data |
89 | |
90 | |
91 | === modified file 'glance/registry/__init__.py' |
92 | --- glance/registry/__init__.py 2011-05-15 01:05:04 +0000 |
93 | +++ glance/registry/__init__.py 2011-05-31 13:23:37 +0000 |
94 | @@ -32,14 +32,14 @@ |
95 | return client.RegistryClient(host, port) |
96 | |
97 | |
98 | -def get_images_list(options, filters): |
99 | - c = get_registry_client(options) |
100 | - return c.get_images(filters) |
101 | - |
102 | - |
103 | -def get_images_detail(options, filters): |
104 | - c = get_registry_client(options) |
105 | - return c.get_images_detailed(filters) |
106 | +def get_images_list(options, **kwargs): |
107 | + c = get_registry_client(options) |
108 | + return c.get_images(**kwargs) |
109 | + |
110 | + |
111 | +def get_images_detail(options, **kwargs): |
112 | + c = get_registry_client(options) |
113 | + return c.get_images_detailed(**kwargs) |
114 | |
115 | |
116 | def get_image_metadata(options, image_id): |
117 | |
118 | === modified file 'glance/registry/client.py' |
119 | --- glance/registry/client.py 2011-05-24 19:47:59 +0000 |
120 | +++ glance/registry/client.py 2011-05-31 13:23:37 +0000 |
121 | @@ -44,19 +44,43 @@ |
122 | port = port or self.DEFAULT_PORT |
123 | super(RegistryClient, self).__init__(host, port, use_ssl) |
124 | |
125 | - def get_images(self, filters=None): |
126 | + def get_images(self, filters=None, marker=None, limit=None): |
127 | """ |
128 | Returns a list of image id/name mappings from Registry |
129 | + |
130 | + :param filters: dict of keys & expected values to filter results |
131 | + :param marker: image id after which to start page |
132 | + :param limit: max number of images to return |
133 | """ |
134 | - res = self.do_request("GET", "/images", params=filters) |
135 | + params = filters or {} |
136 | + |
137 | + if marker != None: |
138 | + params['marker'] = marker |
139 | + |
140 | + if limit != None: |
141 | + params['limit'] = limit |
142 | + |
143 | + res = self.do_request("GET", "/images", params=params) |
144 | data = json.loads(res.read())['images'] |
145 | return data |
146 | |
147 | - def get_images_detailed(self, filters=None): |
148 | + def get_images_detailed(self, filters=None, marker=None, limit=None): |
149 | """ |
150 | Returns a list of detailed image data mappings from Registry |
151 | + |
152 | + :param filters: dict of keys & expected values to filter results |
153 | + :param marker: image id after which to start page |
154 | + :param limit: max number of images to return |
155 | """ |
156 | - res = self.do_request("GET", "/images/detail", params=filters) |
157 | + params = filters or {} |
158 | + |
159 | + if marker != None: |
160 | + params['marker'] = marker |
161 | + |
162 | + if limit != None: |
163 | + params['limit'] = limit |
164 | + |
165 | + res = self.do_request("GET", "/images/detail", params=params) |
166 | data = json.loads(res.read())['images'] |
167 | return data |
168 | |
169 | |
170 | === modified file 'glance/registry/db/api.py' |
171 | --- glance/registry/db/api.py 2011-05-27 02:24:55 +0000 |
172 | +++ glance/registry/db/api.py 2011-05-31 13:23:37 +0000 |
173 | @@ -23,11 +23,12 @@ |
174 | |
175 | import logging |
176 | |
177 | -from sqlalchemy import create_engine |
178 | +from sqlalchemy import create_engine, desc |
179 | from sqlalchemy.ext.declarative import declarative_base |
180 | from sqlalchemy.orm import exc |
181 | from sqlalchemy.orm import joinedload |
182 | from sqlalchemy.orm import sessionmaker |
183 | +from sqlalchemy.sql import or_, and_ |
184 | |
185 | from glance.common import config |
186 | from glance.common import exception |
187 | @@ -127,23 +128,26 @@ |
188 | raise exception.NotFound("No image found with ID %s" % image_id) |
189 | |
190 | |
191 | -def image_get_all_public(context, filters=None): |
192 | +def image_get_all_public(context, filters=None, marker=None, limit=None): |
193 | """Get all public images that match zero or more filters. |
194 | |
195 | :param filters: dict of filter keys and values. If a 'properties' |
196 | key is present, it is treated as a dict of key/value |
197 | filters on the image properties attribute |
198 | + :param marker: image id after which to start page |
199 | + :param limit: maximum number of images to return |
200 | |
201 | """ |
202 | - if filters == None: |
203 | - filters = {} |
204 | + filters = filters or {} |
205 | |
206 | session = get_session() |
207 | query = session.query(models.Image).\ |
208 | options(joinedload(models.Image.properties)).\ |
209 | filter_by(deleted=_deleted(context)).\ |
210 | filter_by(is_public=True).\ |
211 | - filter(models.Image.status != 'killed') |
212 | + filter(models.Image.status != 'killed').\ |
213 | + order_by(desc(models.Image.created_at)).\ |
214 | + order_by(desc(models.Image.id)) |
215 | |
216 | if 'size_min' in filters: |
217 | query = query.filter(models.Image.size >= filters['size_min']) |
218 | @@ -159,6 +163,17 @@ |
219 | for (k, v) in filters.items(): |
220 | query = query.filter(getattr(models.Image, k) == v) |
221 | |
222 | + if marker != None: |
223 | + # images returned should be created before the image defined by marker |
224 | + marker_created_at = image_get(context, marker).created_at |
225 | + query = query.filter( |
226 | + or_(models.Image.created_at < marker_created_at, |
227 | + and_(models.Image.created_at == marker_created_at, |
228 | + models.Image.id < marker))) |
229 | + |
230 | + if limit != None: |
231 | + query = query.limit(limit) |
232 | + |
233 | return query.all() |
234 | |
235 | |
236 | |
237 | === modified file 'glance/registry/server.py' |
238 | --- glance/registry/server.py 2011-05-16 13:20:53 +0000 |
239 | +++ glance/registry/server.py 2011-05-31 13:23:37 +0000 |
240 | @@ -39,6 +39,8 @@ |
241 | SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', |
242 | 'size_min', 'size_max'] |
243 | |
244 | +MAX_ITEM_LIMIT = 25 |
245 | + |
246 | |
247 | class Controller(wsgi.Controller): |
248 | """Controller for the reference implementation registry server""" |
249 | @@ -67,7 +69,15 @@ |
250 | } |
251 | |
252 | """ |
253 | - images = db_api.image_get_all_public(None, self._get_filters(req)) |
254 | + params = { |
255 | + 'filters': self._get_filters(req), |
256 | + 'limit': self._get_limit(req), |
257 | + } |
258 | + |
259 | + if 'marker' in req.str_params: |
260 | + params['marker'] = self._get_marker(req) |
261 | + |
262 | + images = db_api.image_get_all_public(None, **params) |
263 | |
264 | results = [] |
265 | for image in images: |
266 | @@ -89,7 +99,15 @@ |
267 | all image model fields. |
268 | |
269 | """ |
270 | - images = db_api.image_get_all_public(None, self._get_filters(req)) |
271 | + params = { |
272 | + 'filters': self._get_filters(req), |
273 | + 'limit': self._get_limit(req), |
274 | + } |
275 | + |
276 | + if 'marker' in req.str_params: |
277 | + params['marker'] = self._get_marker(req) |
278 | + |
279 | + images = db_api.image_get_all_public(None, **params) |
280 | |
281 | image_dicts = [make_image_dict(i) for i in images] |
282 | return dict(images=image_dicts) |
283 | @@ -116,6 +134,26 @@ |
284 | |
285 | return filters |
286 | |
287 | + def _get_limit(self, req): |
288 | + """Parse a limit query param into something usable.""" |
289 | + try: |
290 | + limit = int(req.str_params.get('limit', MAX_ITEM_LIMIT)) |
291 | + except ValueError: |
292 | + raise exc.HTTPBadRequest("limit param must be an integer") |
293 | + |
294 | + if limit < 0: |
295 | + raise exc.HTTPBadRequest("limit param must be positive") |
296 | + |
297 | + return min(MAX_ITEM_LIMIT, limit) |
298 | + |
299 | + def _get_marker(self, req): |
300 | + """Parse a marker query param into something usable.""" |
301 | + try: |
302 | + marker = int(req.str_params.get('marker', None)) |
303 | + except ValueError: |
304 | + raise exc.HTTPBadRequest("marker param must be an integer") |
305 | + return marker |
306 | + |
307 | def show(self, req, id): |
308 | """Return data about the given image id.""" |
309 | try: |
310 | |
311 | === modified file 'tests/functional/test_curl_api.py' |
312 | --- tests/functional/test_curl_api.py 2011-05-27 14:29:59 +0000 |
313 | +++ tests/functional/test_curl_api.py 2011-05-31 13:23:37 +0000 |
314 | @@ -1001,3 +1001,119 @@ |
315 | self.assertEqual(image["name"], "My Image!") |
316 | |
317 | self.stop_servers() |
318 | + |
319 | + def test_limited_images(self): |
320 | + """ |
321 | + Ensure marker and limit query params work |
322 | + """ |
323 | + self.cleanup() |
324 | + self.start_servers() |
325 | + |
326 | + api_port = self.api_port |
327 | + registry_port = self.registry_port |
328 | + |
329 | + # 0. GET /images |
330 | + # Verify no public images |
331 | + cmd = "curl http://0.0.0.0:%d/v1/images" % api_port |
332 | + |
333 | + exitcode, out, err = execute(cmd) |
334 | + |
335 | + self.assertEqual(0, exitcode) |
336 | + self.assertEqual('{"images": []}', out.strip()) |
337 | + |
338 | + # 1. POST /images with three public images with various attributes |
339 | + cmd = ("curl -i -X POST " |
340 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
341 | + "-H 'X-Image-Meta-Name: Image1' " |
342 | + "-H 'X-Image-Meta-Is-Public: True' " |
343 | + "http://0.0.0.0:%d/v1/images") % api_port |
344 | + |
345 | + exitcode, out, err = execute(cmd) |
346 | + self.assertEqual(0, exitcode) |
347 | + |
348 | + lines = out.split("\r\n") |
349 | + status_line = lines[0] |
350 | + |
351 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
352 | + |
353 | + cmd = ("curl -i -X POST " |
354 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
355 | + "-H 'X-Image-Meta-Name: Image2' " |
356 | + "-H 'X-Image-Meta-Is-Public: True' " |
357 | + "http://0.0.0.0:%d/v1/images") % api_port |
358 | + |
359 | + exitcode, out, err = execute(cmd) |
360 | + self.assertEqual(0, exitcode) |
361 | + |
362 | + lines = out.split("\r\n") |
363 | + status_line = lines[0] |
364 | + |
365 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
366 | + |
367 | + cmd = ("curl -i -X POST " |
368 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
369 | + "-H 'X-Image-Meta-Name: Image3' " |
370 | + "-H 'X-Image-Meta-Is-Public: True' " |
371 | + "http://0.0.0.0:%d/v1/images") % api_port |
372 | + |
373 | + exitcode, out, err = execute(cmd) |
374 | + self.assertEqual(0, exitcode) |
375 | + |
376 | + lines = out.split("\r\n") |
377 | + status_line = lines[0] |
378 | + |
379 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
380 | + |
381 | + # 2. GET /images with limit of 2 |
382 | + # Verify only two images were returned |
383 | + cmd = "curl http://0.0.0.0:%d/v1/images?limit=2" % api_port |
384 | + |
385 | + exitcode, out, err = execute(cmd) |
386 | + |
387 | + self.assertEqual(0, exitcode) |
388 | + images = json.loads(out.strip())['images'] |
389 | + |
390 | + self.assertEqual(len(images), 2) |
391 | + self.assertEqual(int(images[0]['id']), 3) |
392 | + self.assertEqual(int(images[1]['id']), 2) |
393 | + |
394 | + # 3. GET /images with marker |
395 | + # Verify only two images were returned |
396 | + cmd = "curl http://0.0.0.0:%d/v1/images?marker=3" % api_port |
397 | + |
398 | + exitcode, out, err = execute(cmd) |
399 | + |
400 | + self.assertEqual(0, exitcode) |
401 | + images = json.loads(out.strip())['images'] |
402 | + |
403 | + self.assertEqual(len(images), 2) |
404 | + self.assertEqual(int(images[0]['id']), 2) |
405 | + self.assertEqual(int(images[1]['id']), 1) |
406 | + |
407 | + # 4. GET /images with marker and limit |
408 | + # Verify only one image was returned with the correct id |
409 | + cmd = ("curl 'http://0.0.0.0:%d/v1/images?" |
410 | + "limit=1&marker=2'" % api_port) |
411 | + |
412 | + exitcode, out, err = execute(cmd) |
413 | + |
414 | + self.assertEqual(0, exitcode) |
415 | + images = json.loads(out.strip())['images'] |
416 | + |
417 | + self.assertEqual(len(images), 1) |
418 | + self.assertEqual(int(images[0]['id']), 1) |
419 | + |
420 | + # 5. GET /images/detail with marker and limit |
421 | + # Verify only one image was returned with the correct id |
422 | + cmd = ("curl 'http://0.0.0.0:%d/v1/images/detail?" |
423 | + "limit=1&marker=3'" % api_port) |
424 | + |
425 | + exitcode, out, err = execute(cmd) |
426 | + |
427 | + self.assertEqual(0, exitcode) |
428 | + images = json.loads(out.strip())['images'] |
429 | + |
430 | + self.assertEqual(len(images), 1) |
431 | + self.assertEqual(int(images[0]['id']), 2) |
432 | + |
433 | + self.stop_servers() |
434 | |
435 | === modified file 'tests/stubs.py' |
436 | --- tests/stubs.py 2011-05-16 13:20:53 +0000 |
437 | +++ tests/stubs.py 2011-05-31 13:23:37 +0000 |
438 | @@ -19,6 +19,7 @@ |
439 | |
440 | import datetime |
441 | import httplib |
442 | +import operator |
443 | import os |
444 | import shutil |
445 | import StringIO |
446 | @@ -322,8 +323,10 @@ |
447 | values['deleted'] = False |
448 | values['properties'] = values.get('properties', {}) |
449 | values['location'] = values.get('location') |
450 | - values['created_at'] = datetime.datetime.utcnow() |
451 | - values['updated_at'] = datetime.datetime.utcnow() |
452 | + |
453 | + now = datetime.datetime.utcnow() |
454 | + values['created_at'] = values.get('created_at', now) |
455 | + values['updated_at'] = values.get('updated_at', now) |
456 | values['deleted_at'] = None |
457 | |
458 | props = [] |
459 | @@ -334,8 +337,8 @@ |
460 | p['name'] = k |
461 | p['value'] = v |
462 | p['deleted'] = False |
463 | - p['created_at'] = datetime.datetime.utcnow() |
464 | - p['updated_at'] = datetime.datetime.utcnow() |
465 | + p['created_at'] = now |
466 | + p['updated_at'] = now |
467 | p['deleted_at'] = None |
468 | props.append(p) |
469 | |
470 | @@ -386,7 +389,8 @@ |
471 | else: |
472 | return images[0] |
473 | |
474 | - def image_get_all_public(self, _context, filters): |
475 | + def image_get_all_public(self, _context, filters=None, |
476 | + marker=None, limit=1000): |
477 | images = [f for f in self.images if f['is_public'] == True] |
478 | |
479 | if 'size_min' in filters: |
480 | @@ -411,7 +415,30 @@ |
481 | for k, v in filters.items(): |
482 | images = [f for f in images if f[k] == v] |
483 | |
484 | - return images |
485 | + def image_cmp(x, y): |
486 | + if x['created_at'] > y['created_at']: |
487 | + return 1 |
488 | + elif x['created_at'] == y['created_at']: |
489 | + if x['id'] > y['id']: |
490 | + return 1 |
491 | + else: |
492 | + return -1 |
493 | + else: |
494 | + return -1 |
495 | + |
496 | + images = sorted(images, cmp=image_cmp) |
497 | + images.reverse() |
498 | + |
499 | + if marker == None: |
500 | + start_index = 0 |
501 | + else: |
502 | + start_index = -1 |
503 | + for i, image in enumerate(images): |
504 | + if image['id'] == marker: |
505 | + start_index = i + 1 |
506 | + break |
507 | + |
508 | + return images[start_index:start_index + limit] |
509 | |
510 | fake_datastore = FakeDatastore() |
511 | stubs.Set(glance.registry.db.api, 'image_create', |
512 | |
513 | === modified file 'tests/unit/test_api.py' |
514 | --- tests/unit/test_api.py 2011-05-27 02:24:55 +0000 |
515 | +++ tests/unit/test_api.py 2011-05-31 13:23:37 +0000 |
516 | @@ -15,6 +15,7 @@ |
517 | # License for the specific language governing permissions and limitations |
518 | # under the License. |
519 | |
520 | +import datetime |
521 | import hashlib |
522 | import httplib |
523 | import os |
524 | @@ -88,6 +89,158 @@ |
525 | for k, v in fixture.iteritems(): |
526 | self.assertEquals(v, images[0][k]) |
527 | |
528 | + def test_get_index_marker(self): |
529 | + """Tests that the /images registry API returns list of |
530 | + public images that conforms to a marker query param |
531 | + |
532 | + """ |
533 | + |
534 | + time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5) |
535 | + time2 = datetime.datetime.utcnow() |
536 | + |
537 | + extra_fixture = {'id': 3, |
538 | + 'status': 'active', |
539 | + 'is_public': True, |
540 | + 'disk_format': 'vhd', |
541 | + 'container_format': 'ovf', |
542 | + 'name': 'new name! #123', |
543 | + 'size': 19, |
544 | + 'checksum': None, |
545 | + 'created_at': time1} |
546 | + |
547 | + glance.registry.db.api.image_create(None, extra_fixture) |
548 | + |
549 | + extra_fixture = {'id': 4, |
550 | + 'status': 'active', |
551 | + 'is_public': True, |
552 | + 'disk_format': 'vhd', |
553 | + 'container_format': 'ovf', |
554 | + 'name': 'new name! #123', |
555 | + 'size': 20, |
556 | + 'checksum': None, |
557 | + 'created_at': time1} |
558 | + |
559 | + glance.registry.db.api.image_create(None, extra_fixture) |
560 | + |
561 | + extra_fixture = {'id': 5, |
562 | + 'status': 'active', |
563 | + 'is_public': True, |
564 | + 'disk_format': 'vhd', |
565 | + 'container_format': 'ovf', |
566 | + 'name': 'new name! #123', |
567 | + 'size': 20, |
568 | + 'checksum': None, |
569 | + 'created_at': time2} |
570 | + |
571 | + glance.registry.db.api.image_create(None, extra_fixture) |
572 | + |
573 | + req = webob.Request.blank('/images?marker=4') |
574 | + res = req.get_response(self.api) |
575 | + res_dict = json.loads(res.body) |
576 | + self.assertEquals(res.status_int, 200) |
577 | + |
578 | + images = res_dict['images'] |
579 | + # should be sorted by created_at desc, id desc |
580 | + # page should start after marker 4 |
581 | + self.assertEquals(len(images), 3) |
582 | + self.assertEquals(int(images[0]['id']), 3) |
583 | + self.assertEquals(int(images[1]['id']), 5) |
584 | + self.assertEquals(int(images[2]['id']), 2) |
585 | + |
586 | + def test_get_index_limit(self): |
587 | + """Tests that the /images registry API returns list of |
588 | + public images that conforms to a limit query param |
589 | + |
590 | + """ |
591 | + extra_fixture = {'id': 3, |
592 | + 'status': 'active', |
593 | + 'is_public': True, |
594 | + 'disk_format': 'vhd', |
595 | + 'container_format': 'ovf', |
596 | + 'name': 'new name! #123', |
597 | + 'size': 19, |
598 | + 'checksum': None} |
599 | + |
600 | + glance.registry.db.api.image_create(None, extra_fixture) |
601 | + |
602 | + extra_fixture = {'id': 4, |
603 | + 'status': 'active', |
604 | + 'is_public': True, |
605 | + 'disk_format': 'vhd', |
606 | + 'container_format': 'ovf', |
607 | + 'name': 'new name! #123', |
608 | + 'size': 20, |
609 | + 'checksum': None} |
610 | + |
611 | + glance.registry.db.api.image_create(None, extra_fixture) |
612 | + |
613 | + req = webob.Request.blank('/images?limit=1') |
614 | + res = req.get_response(self.api) |
615 | + res_dict = json.loads(res.body) |
616 | + self.assertEquals(res.status_int, 200) |
617 | + |
618 | + images = res_dict['images'] |
619 | + self.assertEquals(len(images), 1) |
620 | + |
621 | + # expect list to be sorted by created_at desc |
622 | + self.assertTrue(int(images[0]['id']), 4) |
623 | + |
624 | + def test_get_index_limit_negative(self): |
625 | + """Tests that the /images registry API returns list of |
626 | + public images that conforms to a limit query param |
627 | + |
628 | + """ |
629 | + req = webob.Request.blank('/images?limit=-1') |
630 | + res = req.get_response(self.api) |
631 | + self.assertEquals(res.status_int, 400) |
632 | + |
633 | + def test_get_index_limit_non_int(self): |
634 | + """Tests that the /images registry API returns list of |
635 | + public images that conforms to a limit query param |
636 | + |
637 | + """ |
638 | + req = webob.Request.blank('/images?limit=a') |
639 | + res = req.get_response(self.api) |
640 | + self.assertEquals(res.status_int, 400) |
641 | + |
642 | + def test_get_index_limit_marker(self): |
643 | + """Tests that the /images registry API returns list of |
644 | + public images that conforms to limit and marker query params |
645 | + |
646 | + """ |
647 | + extra_fixture = {'id': 3, |
648 | + 'status': 'active', |
649 | + 'is_public': True, |
650 | + 'disk_format': 'vhd', |
651 | + 'container_format': 'ovf', |
652 | + 'name': 'new name! #123', |
653 | + 'size': 19, |
654 | + 'checksum': None} |
655 | + |
656 | + glance.registry.db.api.image_create(None, extra_fixture) |
657 | + |
658 | + extra_fixture = {'id': 4, |
659 | + 'status': 'active', |
660 | + 'is_public': True, |
661 | + 'disk_format': 'vhd', |
662 | + 'container_format': 'ovf', |
663 | + 'name': 'new name! #123', |
664 | + 'size': 20, |
665 | + 'checksum': None} |
666 | + |
667 | + glance.registry.db.api.image_create(None, extra_fixture) |
668 | + |
669 | + req = webob.Request.blank('/images?marker=3&limit=1') |
670 | + res = req.get_response(self.api) |
671 | + res_dict = json.loads(res.body) |
672 | + self.assertEquals(res.status_int, 200) |
673 | + |
674 | + images = res_dict['images'] |
675 | + self.assertEquals(len(images), 1) |
676 | + |
677 | + # expect list to be sorted by created_at desc |
678 | + self.assertEqual(int(images[0]['id']), 2) |
679 | + |
680 | def test_get_index_filter_name(self): |
681 | """Tests that the /images registry API returns list of |
682 | public images that have a specific name. This is really a sanity |
683 | @@ -157,6 +310,46 @@ |
684 | for k, v in fixture.iteritems(): |
685 | self.assertEquals(v, images[0][k]) |
686 | |
687 | + def test_get_details_limit_marker(self): |
688 | + """Tests that the /images/details registry API returns list of |
689 | + public images that conforms to limit and marker query params. |
690 | + This functionality is tested more thoroughly on /images, this is |
691 | + just a sanity check |
692 | + |
693 | + """ |
694 | + extra_fixture = {'id': 3, |
695 | + 'status': 'active', |
696 | + 'is_public': True, |
697 | + 'disk_format': 'vhd', |
698 | + 'container_format': 'ovf', |
699 | + 'name': 'new name! #123', |
700 | + 'size': 19, |
701 | + 'checksum': None} |
702 | + |
703 | + glance.registry.db.api.image_create(None, extra_fixture) |
704 | + |
705 | + extra_fixture = {'id': 4, |
706 | + 'status': 'active', |
707 | + 'is_public': True, |
708 | + 'disk_format': 'vhd', |
709 | + 'container_format': 'ovf', |
710 | + 'name': 'new name! #123', |
711 | + 'size': 20, |
712 | + 'checksum': None} |
713 | + |
714 | + glance.registry.db.api.image_create(None, extra_fixture) |
715 | + |
716 | + req = webob.Request.blank('/images/detail?marker=3&limit=1') |
717 | + res = req.get_response(self.api) |
718 | + res_dict = json.loads(res.body) |
719 | + self.assertEquals(res.status_int, 200) |
720 | + |
721 | + images = res_dict['images'] |
722 | + self.assertEquals(len(images), 1) |
723 | + |
724 | + # expect list to be sorted by created_at desc |
725 | + self.assertEqual(int(images[0]['id']), 2) |
726 | + |
727 | def test_get_details_filter_name(self): |
728 | """Tests that the /images/detail registry API returns list of |
729 | public images that have a specific name |
730 | |
731 | === modified file 'tests/unit/test_clients.py' |
732 | --- tests/unit/test_clients.py 2011-05-24 19:07:19 +0000 |
733 | +++ tests/unit/test_clients.py 2011-05-31 13:23:37 +0000 |
734 | @@ -70,6 +70,92 @@ |
735 | for k, v in fixture.items(): |
736 | self.assertEquals(v, images[0][k]) |
737 | |
738 | + def test_get_image_index_marker(self): |
739 | + """Test correct set of images returned with marker param.""" |
740 | + extra_fixture = {'id': 3, |
741 | + 'status': 'saving', |
742 | + 'is_public': True, |
743 | + 'disk_format': 'vhd', |
744 | + 'container_format': 'ovf', |
745 | + 'name': 'new name! #123', |
746 | + 'size': 19, |
747 | + 'checksum': None} |
748 | + |
749 | + glance.registry.db.api.image_create(None, extra_fixture) |
750 | + |
751 | + extra_fixture = {'id': 4, |
752 | + 'status': 'saving', |
753 | + 'is_public': True, |
754 | + 'disk_format': 'vhd', |
755 | + 'container_format': 'ovf', |
756 | + 'name': 'new name! #125', |
757 | + 'size': 19, |
758 | + 'checksum': None} |
759 | + |
760 | + glance.registry.db.api.image_create(None, extra_fixture) |
761 | + |
762 | + images = self.client.get_images(marker=4) |
763 | + self.assertEquals(len(images), 2) |
764 | + |
765 | + for image in images: |
766 | + self.assertTrue(image['id'] < 4) |
767 | + |
768 | + def test_get_image_index_limit(self): |
769 | + """Test correct number of images returned with limit param.""" |
770 | + extra_fixture = {'id': 3, |
771 | + 'status': 'saving', |
772 | + 'is_public': True, |
773 | + 'disk_format': 'vhd', |
774 | + 'container_format': 'ovf', |
775 | + 'name': 'new name! #123', |
776 | + 'size': 19, |
777 | + 'checksum': None} |
778 | + |
779 | + glance.registry.db.api.image_create(None, extra_fixture) |
780 | + |
781 | + extra_fixture = {'id': 4, |
782 | + 'status': 'saving', |
783 | + 'is_public': True, |
784 | + 'disk_format': 'vhd', |
785 | + 'container_format': 'ovf', |
786 | + 'name': 'new name! #125', |
787 | + 'size': 19, |
788 | + 'checksum': None} |
789 | + |
790 | + glance.registry.db.api.image_create(None, extra_fixture) |
791 | + |
792 | + images = self.client.get_images(limit=2) |
793 | + self.assertEquals(len(images), 2) |
794 | + |
795 | + def test_get_image_index_marker_limit(self): |
796 | + """Test correct set of images returned with marker/limit params.""" |
797 | + extra_fixture = {'id': 3, |
798 | + 'status': 'saving', |
799 | + 'is_public': True, |
800 | + 'disk_format': 'vhd', |
801 | + 'container_format': 'ovf', |
802 | + 'name': 'new name! #123', |
803 | + 'size': 19, |
804 | + 'checksum': None} |
805 | + |
806 | + glance.registry.db.api.image_create(None, extra_fixture) |
807 | + |
808 | + extra_fixture = {'id': 4, |
809 | + 'status': 'saving', |
810 | + 'is_public': True, |
811 | + 'disk_format': 'vhd', |
812 | + 'container_format': 'ovf', |
813 | + 'name': 'new name! #125', |
814 | + 'size': 19, |
815 | + 'checksum': None} |
816 | + |
817 | + glance.registry.db.api.image_create(None, extra_fixture) |
818 | + |
819 | + images = self.client.get_images(marker=3, limit=1) |
820 | + self.assertEquals(len(images), 1) |
821 | + |
822 | + self.assertEquals(images[0]['id'], 2) |
823 | + |
824 | def test_get_image_index_by_name(self): |
825 | """Test correct set of public, name-filtered image returned. This |
826 | is just a sanity check, we test the details call more in-depth.""" |
827 | @@ -108,6 +194,35 @@ |
828 | for k, v in fixture.items(): |
829 | self.assertEquals(v, images[0][k]) |
830 | |
831 | + def test_get_image_details_marker_limit(self): |
832 | + """Test correct set of images returned with marker/limit params.""" |
833 | + extra_fixture = {'id': 3, |
834 | + 'status': 'saving', |
835 | + 'is_public': True, |
836 | + 'disk_format': 'vhd', |
837 | + 'container_format': 'ovf', |
838 | + 'name': 'new name! #123', |
839 | + 'size': 19, |
840 | + 'checksum': None} |
841 | + |
842 | + glance.registry.db.api.image_create(None, extra_fixture) |
843 | + |
844 | + extra_fixture = {'id': 4, |
845 | + 'status': 'saving', |
846 | + 'is_public': True, |
847 | + 'disk_format': 'vhd', |
848 | + 'container_format': 'ovf', |
849 | + 'name': 'new name! #125', |
850 | + 'size': 19, |
851 | + 'checksum': None} |
852 | + |
853 | + glance.registry.db.api.image_create(None, extra_fixture) |
854 | + |
855 | + images = self.client.get_images_detailed(marker=3, limit=1) |
856 | + self.assertEquals(len(images), 1) |
857 | + |
858 | + self.assertEquals(images[0]['id'], 2) |
859 | + |
860 | def test_get_image_details_by_name(self): |
861 | """Tests that a detailed call can be filtered by name""" |
862 | extra_fixture = {'id': 3, |
863 | @@ -457,6 +572,92 @@ |
864 | for k, v in fixture.items(): |
865 | self.assertEquals(v, images[0][k]) |
866 | |
867 | + def test_get_image_index_marker(self): |
868 | + """Test correct set of public images returned with marker param.""" |
869 | + extra_fixture = {'id': 3, |
870 | + 'status': 'saving', |
871 | + 'is_public': True, |
872 | + 'disk_format': 'vhd', |
873 | + 'container_format': 'ovf', |
874 | + 'name': 'new name! #123', |
875 | + 'size': 19, |
876 | + 'checksum': None} |
877 | + |
878 | + glance.registry.db.api.image_create(None, extra_fixture) |
879 | + |
880 | + extra_fixture = {'id': 4, |
881 | + 'status': 'saving', |
882 | + 'is_public': True, |
883 | + 'disk_format': 'vhd', |
884 | + 'container_format': 'ovf', |
885 | + 'name': 'new name! #125', |
886 | + 'size': 19, |
887 | + 'checksum': None} |
888 | + |
889 | + glance.registry.db.api.image_create(None, extra_fixture) |
890 | + |
891 | + images = self.client.get_images(marker=4) |
892 | + self.assertEquals(len(images), 2) |
893 | + |
894 | + for image in images: |
895 | + self.assertTrue(image['id'] < 4) |
896 | + |
897 | + def test_get_image_index_limit(self): |
898 | + """Test correct number of public images returned with limit param.""" |
899 | + extra_fixture = {'id': 3, |
900 | + 'status': 'saving', |
901 | + 'is_public': True, |
902 | + 'disk_format': 'vhd', |
903 | + 'container_format': 'ovf', |
904 | + 'name': 'new name! #123', |
905 | + 'size': 19, |
906 | + 'checksum': None} |
907 | + |
908 | + glance.registry.db.api.image_create(None, extra_fixture) |
909 | + |
910 | + extra_fixture = {'id': 4, |
911 | + 'status': 'saving', |
912 | + 'is_public': True, |
913 | + 'disk_format': 'vhd', |
914 | + 'container_format': 'ovf', |
915 | + 'name': 'new name! #125', |
916 | + 'size': 19, |
917 | + 'checksum': None} |
918 | + |
919 | + glance.registry.db.api.image_create(None, extra_fixture) |
920 | + |
921 | + images = self.client.get_images(limit=2) |
922 | + self.assertEquals(len(images), 2) |
923 | + |
924 | + def test_get_image_index_marker_limit(self): |
925 | + """Test correct set of images returned with marker/limit params.""" |
926 | + extra_fixture = {'id': 3, |
927 | + 'status': 'saving', |
928 | + 'is_public': True, |
929 | + 'disk_format': 'vhd', |
930 | + 'container_format': 'ovf', |
931 | + 'name': 'new name! #123', |
932 | + 'size': 19, |
933 | + 'checksum': None} |
934 | + |
935 | + glance.registry.db.api.image_create(None, extra_fixture) |
936 | + |
937 | + extra_fixture = {'id': 4, |
938 | + 'status': 'saving', |
939 | + 'is_public': True, |
940 | + 'disk_format': 'vhd', |
941 | + 'container_format': 'ovf', |
942 | + 'name': 'new name! #125', |
943 | + 'size': 19, |
944 | + 'checksum': None} |
945 | + |
946 | + glance.registry.db.api.image_create(None, extra_fixture) |
947 | + |
948 | + images = self.client.get_images(marker=3, limit=1) |
949 | + self.assertEquals(len(images), 1) |
950 | + |
951 | + self.assertEquals(images[0]['id'], 2) |
952 | + |
953 | def test_get_image_index_by_base_attribute(self): |
954 | """Tests that an index call can be filtered by a base attribute""" |
955 | extra_fixture = {'id': 3, |
956 | @@ -522,6 +723,35 @@ |
957 | for k, v in expected.items(): |
958 | self.assertEquals(v, images[0][k]) |
959 | |
960 | + def test_get_image_details_marker_limit(self): |
961 | + """Test detailed calls are filtered by marker/limit params.""" |
962 | + extra_fixture = {'id': 3, |
963 | + 'status': 'saving', |
964 | + 'is_public': True, |
965 | + 'disk_format': 'vhd', |
966 | + 'container_format': 'ovf', |
967 | + 'name': 'new name! #123', |
968 | + 'size': 19, |
969 | + 'checksum': None} |
970 | + |
971 | + glance.registry.db.api.image_create(None, extra_fixture) |
972 | + |
973 | + extra_fixture = {'id': 4, |
974 | + 'status': 'saving', |
975 | + 'is_public': True, |
976 | + 'disk_format': 'vhd', |
977 | + 'container_format': 'ovf', |
978 | + 'name': 'new name! #125', |
979 | + 'size': 19, |
980 | + 'checksum': None} |
981 | + |
982 | + glance.registry.db.api.image_create(None, extra_fixture) |
983 | + |
984 | + images = self.client.get_images_detailed(marker=3, limit=1) |
985 | + self.assertEquals(len(images), 1) |
986 | + |
987 | + self.assertEquals(images[0]['id'], 2) |
988 | + |
989 | def test_get_image_details_by_base_attribute(self): |
990 | """Tests that a detailed call can be filtered by a base attribute""" |
991 | extra_fixture = {'id': 3, |
Hi Brian,
Once again, great work. Couple little things:
1) Something I did not think about for the results filtering MP, as well as this one, is that we will want to bump the minor version since we are adding functionality to the API in these two MPs.
You can do this in a separate merge proposal, I suppose. Basically, what needs to happen is that we need a way of describing the 1.1 API (1.0 + filters and limits) in the /versions endpoint.
The reason this is important is that the API result for /images has changed: we go from returning ALL public images in no particular order to returning SOME public images in a particular order (by created_at DESC). So, we should bump the minor version of the API to indicate to clients that this has been done.
Since we are in the middle of the D1 milestone, I'm not too concerned about having the results-filter branch, then this branch, then the API 1.1 branch going into trunk in that particular order. I'll handle the versioning stuff next week when I return from vacation.
2) I'd prefer to set the default limit to < 1000. I'm thinking 25 or 50 would be a more appropriate default limit. Also, please make this a constant somewhere, not just a number in the method signature...
3) Unless I'm mistaken these functions:
50 +def limit_collectio n(items, marker, limit): collection( self, req, images):
243 + def _limit_
in glance.common.wsgi and glance. registry. server, respectively, can be removed. You've pushed (correctly) the limiting and offset down into the registry db query.
4) This:
114 if filters != None: urlencode( filters)
115 - action = "/images?%s" % urllib.
116 + params = filters
117 else:
118 - action = "/images"
119 + params = {}
Might be better as:
params = filters or {}
5)
184 + order_by( models. Image.id)
I think I would prefer:
order_by( desc(models. Image.created_ at))
6)
192 + if marker != None: models. Image.id > marker)
193 + query = query.filter(
Should be:
if marker != None: marker)
query = query.offset(
That way, SQLAlchemy will pass the marker as-is to the query as a LIMIT X OFFSET Y. This allows more than just the Image.id column to be used for ordering, and will allow you to add custom ordering to the API ;)
-jay