Merge lp:~rackspace-titan/glance/api-pagination-limiting into lp:~hudson-openstack/glance/trunk

Proposed by Brian Waldon
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
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

Description of the change

Adding support for marker/limit query params from api, through registry client/api, and implementing at registry db api layer

To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) 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.

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_collection(items, marker, limit):
243 + def _limit_collection(self, req, images):

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:
115 - action = "/images?%s" % urllib.urlencode(filters)
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:
193 + query = query.filter(models.Image.id > marker)

Should be:

if marker != None:
    query = query.offset(marker)

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

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :
Download full text (3.2 KiB)

> 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_collection(items, marker, limit):
> 243 + def _limit_collection(self, req, images):
>
> 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.

Good catch. Removed them.

> 4) This:
>
> 114 if filters != None:
> 115 - action = "/images?%s" % urllib.urlencode(filters)
> 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(models.Image.id)
>
> I think I would prefer:
>
> order_by(desc(models.Image.created_at))

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(models.Image.id > marker)
>
> Should be:
>
> if marker != None:
> query = query.offset(marker)
>
> 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 ...

Read more...

Revision history for this message
Jay Pipes (jaypipes) wrote :
Download full text (3.5 KiB)

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_collection(items, marker, limit):
>> 243     + def _limit_collection(self, req, images):
>>
>> 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.
>
> Good catch. Removed them.

Cool.

>> 4) This:
>>
>> 114     if filters != None:
>> 115     - action = "/images?%s" % urllib.urlencode(filters)
>> 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(models.Image.id)
>>
>> I think I would prefer:
>>
>> order_by(desc(models.Image.created_at))
>
> 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(models.Image.id > marker)
>>
>> Should be:
>>
>> if marker != None:
>>     query = query.offset(marker)
>>
>> 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...

Read more...

Revision history for this message
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.

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

sqlalchemy query is still bad. I didn't test it thoroughly enough

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

Washenberger helped me out. Should work now. Blame him if it doesn't.

Revision history for this message
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.Image.created_at < marker_created_at,
177 + and_(models.Image.created_at == marker_created_at,
178 + models.Image.id < marker)))

Should just be:

query = query.filter(models.Image.id > marker)

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

Revision history for this message
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(models.Image.id > marker)
>
> 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.

Revision history for this message
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

Revision history for this message
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.

Revision history for this message
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

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

Added support to glance/client.py

Revision history for this message
Dan Prince (dan-prince) wrote :

Looks good to me.

review: Approve
Revision history for this message
Mark Washenberger (markwash) wrote :

This sqlalchemy looks exactly like what we talked about. Clear eyes full hearts can't lose.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'glance/api/v1/images.py'
--- glance/api/v1/images.py 2011-05-17 13:27:09 +0000
+++ glance/api/v1/images.py 2011-05-31 13:23:37 +0000
@@ -92,8 +92,15 @@
92 'size': <SIZE>}, ...92 'size': <SIZE>}, ...
93 ]}93 ]}
94 """94 """
95 filters = self._get_filters(req)95 params = {'filters': self._get_filters(req)}
96 images = registry.get_images_list(self.options, filters)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)
97 return dict(images=images)104 return dict(images=images)
98105
99 def detail(self, req):106 def detail(self, req):
@@ -118,8 +125,15 @@
118 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...125 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
119 ]}126 ]}
120 """127 """
121 filters = self._get_filters(req)128 params = {'filters': self._get_filters(req)}
122 images = registry.get_images_detail(self.options, filters)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)
123 return dict(images=images)137 return dict(images=images)
124138
125 def _get_filters(self, req):139 def _get_filters(self, req):
126140
=== modified file 'glance/client.py'
--- glance/client.py 2011-05-24 19:47:59 +0000
+++ glance/client.py 2011-05-31 13:23:37 +0000
@@ -209,19 +209,41 @@
209 return super(V1Client, self).do_request(method, action, body,209 return super(V1Client, self).do_request(method, action, body,
210 headers, params)210 headers, params)
211211
212 def get_images(self, filters=None):212 def get_images(self, filters=None, marker=None, limit=None):
213 """213 """
214 Returns a list of image id/name mappings from Registry214 Returns a list of image id/name mappings from Registry
215
216 :param filters: dictionary of attributes by which the resulting
217 collection of images should be filtered
218 :param marker: id after which to start the page of images
219 :param limit: maximum number of items to return
215 """220 """
216 res = self.do_request("GET", "/images", params=filters)221
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)
217 data = json.loads(res.read())['images']228 data = json.loads(res.read())['images']
218 return data229 return data
219230
220 def get_images_detailed(self, filters=None):231 def get_images_detailed(self, filters=None, marker=None, limit=None):
221 """232 """
222 Returns a list of detailed image data mappings from Registry233 Returns a list of detailed image data mappings from Registry
234
235 :param filters: dictionary of attributes by which the resulting
236 collection of images should be filtered
237 :param marker: id after which to start the page of images
238 :param limit: maximum number of items to return
223 """239 """
224 res = self.do_request("GET", "/images/detail", params=filters)240
241 params = filters or {}
242 if marker:
243 params['marker'] = marker
244 if limit:
245 params['limit'] = limit
246 res = self.do_request("GET", "/images/detail", params=params)
225 data = json.loads(res.read())['images']247 data = json.loads(res.read())['images']
226 return data248 return data
227249
228250
=== modified file 'glance/registry/__init__.py'
--- glance/registry/__init__.py 2011-05-15 01:05:04 +0000
+++ glance/registry/__init__.py 2011-05-31 13:23:37 +0000
@@ -32,14 +32,14 @@
32 return client.RegistryClient(host, port)32 return client.RegistryClient(host, port)
3333
3434
35def get_images_list(options, filters):35def get_images_list(options, **kwargs):
36 c = get_registry_client(options)36 c = get_registry_client(options)
37 return c.get_images(filters)37 return c.get_images(**kwargs)
3838
3939
40def get_images_detail(options, filters):40def get_images_detail(options, **kwargs):
41 c = get_registry_client(options)41 c = get_registry_client(options)
42 return c.get_images_detailed(filters)42 return c.get_images_detailed(**kwargs)
4343
4444
45def get_image_metadata(options, image_id):45def get_image_metadata(options, image_id):
4646
=== modified file 'glance/registry/client.py'
--- glance/registry/client.py 2011-05-24 19:47:59 +0000
+++ glance/registry/client.py 2011-05-31 13:23:37 +0000
@@ -44,19 +44,43 @@
44 port = port or self.DEFAULT_PORT44 port = port or self.DEFAULT_PORT
45 super(RegistryClient, self).__init__(host, port, use_ssl)45 super(RegistryClient, self).__init__(host, port, use_ssl)
4646
47 def get_images(self, filters=None):47 def get_images(self, filters=None, marker=None, limit=None):
48 """48 """
49 Returns a list of image id/name mappings from Registry49 Returns a list of image id/name mappings from Registry
50
51 :param filters: dict of keys & expected values to filter results
52 :param marker: image id after which to start page
53 :param limit: max number of images to return
50 """54 """
51 res = self.do_request("GET", "/images", params=filters)55 params = filters or {}
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)
52 data = json.loads(res.read())['images']64 data = json.loads(res.read())['images']
53 return data65 return data
5466
55 def get_images_detailed(self, filters=None):67 def get_images_detailed(self, filters=None, marker=None, limit=None):
56 """68 """
57 Returns a list of detailed image data mappings from Registry69 Returns a list of detailed image data mappings from Registry
70
71 :param filters: dict of keys & expected values to filter results
72 :param marker: image id after which to start page
73 :param limit: max number of images to return
58 """74 """
59 res = self.do_request("GET", "/images/detail", params=filters)75 params = filters or {}
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)
60 data = json.loads(res.read())['images']84 data = json.loads(res.read())['images']
61 return data85 return data
6286
6387
=== modified file 'glance/registry/db/api.py'
--- glance/registry/db/api.py 2011-05-27 02:24:55 +0000
+++ glance/registry/db/api.py 2011-05-31 13:23:37 +0000
@@ -23,11 +23,12 @@
2323
24import logging24import logging
2525
26from sqlalchemy import create_engine26from sqlalchemy import 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
30from sqlalchemy.orm import sessionmaker30from sqlalchemy.orm import sessionmaker
31from sqlalchemy.sql import or_, and_
3132
32from glance.common import config33from glance.common import config
33from glance.common import exception34from glance.common import exception
@@ -127,23 +128,26 @@
127 raise exception.NotFound("No image found with ID %s" % image_id)128 raise exception.NotFound("No image found with ID %s" % image_id)
128129
129130
130def image_get_all_public(context, filters=None):131def image_get_all_public(context, filters=None, marker=None, limit=None):
131 """Get all public images that match zero or more filters.132 """Get all public images that match zero or more filters.
132133
133 :param filters: dict of filter keys and values. If a 'properties'134 :param filters: dict of filter keys and values. If a 'properties'
134 key is present, it is treated as a dict of key/value135 key is present, it is treated as a dict of key/value
135 filters on the image properties attribute136 filters on the image properties attribute
137 :param marker: image id after which to start page
138 :param limit: maximum number of images to return
136139
137 """140 """
138 if filters == None:141 filters = filters or {}
139 filters = {}
140142
141 session = get_session()143 session = get_session()
142 query = session.query(models.Image).\144 query = session.query(models.Image).\
143 options(joinedload(models.Image.properties)).\145 options(joinedload(models.Image.properties)).\
144 filter_by(deleted=_deleted(context)).\146 filter_by(deleted=_deleted(context)).\
145 filter_by(is_public=True).\147 filter_by(is_public=True).\
146 filter(models.Image.status != 'killed')148 filter(models.Image.status != 'killed').\
149 order_by(desc(models.Image.created_at)).\
150 order_by(desc(models.Image.id))
147151
148 if 'size_min' in filters:152 if 'size_min' in filters:
149 query = query.filter(models.Image.size >= filters['size_min'])153 query = query.filter(models.Image.size >= filters['size_min'])
@@ -159,6 +163,17 @@
159 for (k, v) in filters.items():163 for (k, v) in filters.items():
160 query = query.filter(getattr(models.Image, k) == v)164 query = query.filter(getattr(models.Image, k) == v)
161165
166 if marker != None:
167 # images returned should be created before the image defined by marker
168 marker_created_at = image_get(context, marker).created_at
169 query = query.filter(
170 or_(models.Image.created_at < marker_created_at,
171 and_(models.Image.created_at == marker_created_at,
172 models.Image.id < marker)))
173
174 if limit != None:
175 query = query.limit(limit)
176
162 return query.all()177 return query.all()
163178
164179
165180
=== modified file 'glance/registry/server.py'
--- glance/registry/server.py 2011-05-16 13:20:53 +0000
+++ glance/registry/server.py 2011-05-31 13:23:37 +0000
@@ -39,6 +39,8 @@
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
42MAX_ITEM_LIMIT = 25
43
4244
43class Controller(wsgi.Controller):45class Controller(wsgi.Controller):
44 """Controller for the reference implementation registry server"""46 """Controller for the reference implementation registry server"""
@@ -67,7 +69,15 @@
67 }69 }
6870
69 """71 """
70 images = db_api.image_get_all_public(None, self._get_filters(req))72 params = {
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)
7181
72 results = []82 results = []
73 for image in images:83 for image in images:
@@ -89,7 +99,15 @@
89 all image model fields.99 all image model fields.
90100
91 """101 """
92 images = db_api.image_get_all_public(None, self._get_filters(req))102 params = {
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)
93111
94 image_dicts = [make_image_dict(i) for i in images]112 image_dicts = [make_image_dict(i) for i in images]
95 return dict(images=image_dicts)113 return dict(images=image_dicts)
@@ -116,6 +134,26 @@
116134
117 return filters135 return filters
118136
137 def _get_limit(self, req):
138 """Parse a limit query param into something usable."""
139 try:
140 limit = int(req.str_params.get('limit', MAX_ITEM_LIMIT))
141 except ValueError:
142 raise exc.HTTPBadRequest("limit param must be an integer")
143
144 if limit < 0:
145 raise exc.HTTPBadRequest("limit param must be positive")
146
147 return min(MAX_ITEM_LIMIT, limit)
148
149 def _get_marker(self, req):
150 """Parse a marker query param into something usable."""
151 try:
152 marker = int(req.str_params.get('marker', None))
153 except ValueError:
154 raise exc.HTTPBadRequest("marker param must be an integer")
155 return marker
156
119 def show(self, req, id):157 def show(self, req, id):
120 """Return data about the given image id."""158 """Return data about the given image id."""
121 try:159 try:
122160
=== modified file 'tests/functional/test_curl_api.py'
--- tests/functional/test_curl_api.py 2011-05-27 14:29:59 +0000
+++ tests/functional/test_curl_api.py 2011-05-31 13:23:37 +0000
@@ -1001,3 +1001,119 @@
1001 self.assertEqual(image["name"], "My Image!")1001 self.assertEqual(image["name"], "My Image!")
10021002
1003 self.stop_servers()1003 self.stop_servers()
1004
1005 def test_limited_images(self):
1006 """
1007 Ensure marker and limit query params work
1008 """
1009 self.cleanup()
1010 self.start_servers()
1011
1012 api_port = self.api_port
1013 registry_port = self.registry_port
1014
1015 # 0. GET /images
1016 # Verify no public images
1017 cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
1018
1019 exitcode, out, err = execute(cmd)
1020
1021 self.assertEqual(0, exitcode)
1022 self.assertEqual('{"images": []}', out.strip())
1023
1024 # 1. POST /images with three public images with various attributes
1025 cmd = ("curl -i -X POST "
1026 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1027 "-H 'X-Image-Meta-Name: Image1' "
1028 "-H 'X-Image-Meta-Is-Public: True' "
1029 "http://0.0.0.0:%d/v1/images") % api_port
1030
1031 exitcode, out, err = execute(cmd)
1032 self.assertEqual(0, exitcode)
1033
1034 lines = out.split("\r\n")
1035 status_line = lines[0]
1036
1037 self.assertEqual("HTTP/1.1 201 Created", status_line)
1038
1039 cmd = ("curl -i -X POST "
1040 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1041 "-H 'X-Image-Meta-Name: Image2' "
1042 "-H 'X-Image-Meta-Is-Public: True' "
1043 "http://0.0.0.0:%d/v1/images") % api_port
1044
1045 exitcode, out, err = execute(cmd)
1046 self.assertEqual(0, exitcode)
1047
1048 lines = out.split("\r\n")
1049 status_line = lines[0]
1050
1051 self.assertEqual("HTTP/1.1 201 Created", status_line)
1052
1053 cmd = ("curl -i -X POST "
1054 "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
1055 "-H 'X-Image-Meta-Name: Image3' "
1056 "-H 'X-Image-Meta-Is-Public: True' "
1057 "http://0.0.0.0:%d/v1/images") % api_port
1058
1059 exitcode, out, err = execute(cmd)
1060 self.assertEqual(0, exitcode)
1061
1062 lines = out.split("\r\n")
1063 status_line = lines[0]
1064
1065 self.assertEqual("HTTP/1.1 201 Created", status_line)
1066
1067 # 2. GET /images with limit of 2
1068 # Verify only two images were returned
1069 cmd = "curl http://0.0.0.0:%d/v1/images?limit=2" % api_port
1070
1071 exitcode, out, err = execute(cmd)
1072
1073 self.assertEqual(0, exitcode)
1074 images = json.loads(out.strip())['images']
1075
1076 self.assertEqual(len(images), 2)
1077 self.assertEqual(int(images[0]['id']), 3)
1078 self.assertEqual(int(images[1]['id']), 2)
1079
1080 # 3. GET /images with marker
1081 # Verify only two images were returned
1082 cmd = "curl http://0.0.0.0:%d/v1/images?marker=3" % api_port
1083
1084 exitcode, out, err = execute(cmd)
1085
1086 self.assertEqual(0, exitcode)
1087 images = json.loads(out.strip())['images']
1088
1089 self.assertEqual(len(images), 2)
1090 self.assertEqual(int(images[0]['id']), 2)
1091 self.assertEqual(int(images[1]['id']), 1)
1092
1093 # 4. GET /images with marker and limit
1094 # Verify only one image was returned with the correct id
1095 cmd = ("curl 'http://0.0.0.0:%d/v1/images?"
1096 "limit=1&marker=2'" % api_port)
1097
1098 exitcode, out, err = execute(cmd)
1099
1100 self.assertEqual(0, exitcode)
1101 images = json.loads(out.strip())['images']
1102
1103 self.assertEqual(len(images), 1)
1104 self.assertEqual(int(images[0]['id']), 1)
1105
1106 # 5. GET /images/detail with marker and limit
1107 # Verify only one image was returned with the correct id
1108 cmd = ("curl 'http://0.0.0.0:%d/v1/images/detail?"
1109 "limit=1&marker=3'" % api_port)
1110
1111 exitcode, out, err = execute(cmd)
1112
1113 self.assertEqual(0, exitcode)
1114 images = json.loads(out.strip())['images']
1115
1116 self.assertEqual(len(images), 1)
1117 self.assertEqual(int(images[0]['id']), 2)
1118
1119 self.stop_servers()
10041120
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2011-05-16 13:20:53 +0000
+++ tests/stubs.py 2011-05-31 13:23:37 +0000
@@ -19,6 +19,7 @@
1919
20import datetime20import datetime
21import httplib21import httplib
22import operator
22import os23import os
23import shutil24import shutil
24import StringIO25import StringIO
@@ -322,8 +323,10 @@
322 values['deleted'] = False323 values['deleted'] = False
323 values['properties'] = values.get('properties', {})324 values['properties'] = values.get('properties', {})
324 values['location'] = values.get('location')325 values['location'] = values.get('location')
325 values['created_at'] = datetime.datetime.utcnow()326
326 values['updated_at'] = datetime.datetime.utcnow()327 now = datetime.datetime.utcnow()
328 values['created_at'] = values.get('created_at', now)
329 values['updated_at'] = values.get('updated_at', now)
327 values['deleted_at'] = None330 values['deleted_at'] = None
328331
329 props = []332 props = []
@@ -334,8 +337,8 @@
334 p['name'] = k337 p['name'] = k
335 p['value'] = v338 p['value'] = v
336 p['deleted'] = False339 p['deleted'] = False
337 p['created_at'] = datetime.datetime.utcnow()340 p['created_at'] = now
338 p['updated_at'] = datetime.datetime.utcnow()341 p['updated_at'] = now
339 p['deleted_at'] = None342 p['deleted_at'] = None
340 props.append(p)343 props.append(p)
341344
@@ -386,7 +389,8 @@
386 else:389 else:
387 return images[0]390 return images[0]
388391
389 def image_get_all_public(self, _context, filters):392 def image_get_all_public(self, _context, filters=None,
393 marker=None, limit=1000):
390 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]
391395
392 if 'size_min' in filters:396 if 'size_min' in filters:
@@ -411,7 +415,30 @@
411 for k, v in filters.items():415 for k, v in filters.items():
412 images = [f for f in images if f[k] == v]416 images = [f for f in images if f[k] == v]
413417
414 return images418 def image_cmp(x, y):
419 if x['created_at'] > y['created_at']:
420 return 1
421 elif x['created_at'] == y['created_at']:
422 if x['id'] > y['id']:
423 return 1
424 else:
425 return -1
426 else:
427 return -1
428
429 images = sorted(images, cmp=image_cmp)
430 images.reverse()
431
432 if marker == None:
433 start_index = 0
434 else:
435 start_index = -1
436 for i, image in enumerate(images):
437 if image['id'] == marker:
438 start_index = i + 1
439 break
440
441 return images[start_index:start_index + limit]
415442
416 fake_datastore = FakeDatastore()443 fake_datastore = FakeDatastore()
417 stubs.Set(glance.registry.db.api, 'image_create',444 stubs.Set(glance.registry.db.api, 'image_create',
418445
=== modified file 'tests/unit/test_api.py'
--- tests/unit/test_api.py 2011-05-27 02:24:55 +0000
+++ tests/unit/test_api.py 2011-05-31 13:23:37 +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 hashlib19import hashlib
19import httplib20import httplib
20import os21import os
@@ -88,6 +89,158 @@
88 for k, v in fixture.iteritems():89 for k, v in fixture.iteritems():
89 self.assertEquals(v, images[0][k])90 self.assertEquals(v, images[0][k])
9091
92 def test_get_index_marker(self):
93 """Tests that the /images registry API returns list of
94 public images that conforms to a marker query param
95
96 """
97
98 time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
99 time2 = datetime.datetime.utcnow()
100
101 extra_fixture = {'id': 3,
102 'status': 'active',
103 'is_public': True,
104 'disk_format': 'vhd',
105 'container_format': 'ovf',
106 'name': 'new name! #123',
107 'size': 19,
108 'checksum': None,
109 'created_at': time1}
110
111 glance.registry.db.api.image_create(None, extra_fixture)
112
113 extra_fixture = {'id': 4,
114 'status': 'active',
115 'is_public': True,
116 'disk_format': 'vhd',
117 'container_format': 'ovf',
118 'name': 'new name! #123',
119 'size': 20,
120 'checksum': None,
121 'created_at': time1}
122
123 glance.registry.db.api.image_create(None, extra_fixture)
124
125 extra_fixture = {'id': 5,
126 'status': 'active',
127 'is_public': True,
128 'disk_format': 'vhd',
129 'container_format': 'ovf',
130 'name': 'new name! #123',
131 'size': 20,
132 'checksum': None,
133 'created_at': time2}
134
135 glance.registry.db.api.image_create(None, extra_fixture)
136
137 req = webob.Request.blank('/images?marker=4')
138 res = req.get_response(self.api)
139 res_dict = json.loads(res.body)
140 self.assertEquals(res.status_int, 200)
141
142 images = res_dict['images']
143 # should be sorted by created_at desc, id desc
144 # page should start after marker 4
145 self.assertEquals(len(images), 3)
146 self.assertEquals(int(images[0]['id']), 3)
147 self.assertEquals(int(images[1]['id']), 5)
148 self.assertEquals(int(images[2]['id']), 2)
149
150 def test_get_index_limit(self):
151 """Tests that the /images registry API returns list of
152 public images that conforms to a limit query param
153
154 """
155 extra_fixture = {'id': 3,
156 'status': 'active',
157 'is_public': True,
158 'disk_format': 'vhd',
159 'container_format': 'ovf',
160 'name': 'new name! #123',
161 'size': 19,
162 'checksum': None}
163
164 glance.registry.db.api.image_create(None, extra_fixture)
165
166 extra_fixture = {'id': 4,
167 'status': 'active',
168 'is_public': True,
169 'disk_format': 'vhd',
170 'container_format': 'ovf',
171 'name': 'new name! #123',
172 'size': 20,
173 'checksum': None}
174
175 glance.registry.db.api.image_create(None, extra_fixture)
176
177 req = webob.Request.blank('/images?limit=1')
178 res = req.get_response(self.api)
179 res_dict = json.loads(res.body)
180 self.assertEquals(res.status_int, 200)
181
182 images = res_dict['images']
183 self.assertEquals(len(images), 1)
184
185 # expect list to be sorted by created_at desc
186 self.assertTrue(int(images[0]['id']), 4)
187
188 def test_get_index_limit_negative(self):
189 """Tests that the /images registry API returns list of
190 public images that conforms to a limit query param
191
192 """
193 req = webob.Request.blank('/images?limit=-1')
194 res = req.get_response(self.api)
195 self.assertEquals(res.status_int, 400)
196
197 def test_get_index_limit_non_int(self):
198 """Tests that the /images registry API returns list of
199 public images that conforms to a limit query param
200
201 """
202 req = webob.Request.blank('/images?limit=a')
203 res = req.get_response(self.api)
204 self.assertEquals(res.status_int, 400)
205
206 def test_get_index_limit_marker(self):
207 """Tests that the /images registry API returns list of
208 public images that conforms to limit and marker query params
209
210 """
211 extra_fixture = {'id': 3,
212 'status': 'active',
213 'is_public': True,
214 'disk_format': 'vhd',
215 'container_format': 'ovf',
216 'name': 'new name! #123',
217 'size': 19,
218 'checksum': None}
219
220 glance.registry.db.api.image_create(None, extra_fixture)
221
222 extra_fixture = {'id': 4,
223 'status': 'active',
224 'is_public': True,
225 'disk_format': 'vhd',
226 'container_format': 'ovf',
227 'name': 'new name! #123',
228 'size': 20,
229 'checksum': None}
230
231 glance.registry.db.api.image_create(None, extra_fixture)
232
233 req = webob.Request.blank('/images?marker=3&limit=1')
234 res = req.get_response(self.api)
235 res_dict = json.loads(res.body)
236 self.assertEquals(res.status_int, 200)
237
238 images = res_dict['images']
239 self.assertEquals(len(images), 1)
240
241 # expect list to be sorted by created_at desc
242 self.assertEqual(int(images[0]['id']), 2)
243
91 def test_get_index_filter_name(self):244 def test_get_index_filter_name(self):
92 """Tests that the /images registry API returns list of245 """Tests that the /images registry API returns list of
93 public images that have a specific name. This is really a sanity246 public images that have a specific name. This is really a sanity
@@ -157,6 +310,46 @@
157 for k, v in fixture.iteritems():310 for k, v in fixture.iteritems():
158 self.assertEquals(v, images[0][k])311 self.assertEquals(v, images[0][k])
159312
313 def test_get_details_limit_marker(self):
314 """Tests that the /images/details registry API returns list of
315 public images that conforms to limit and marker query params.
316 This functionality is tested more thoroughly on /images, this is
317 just a sanity check
318
319 """
320 extra_fixture = {'id': 3,
321 'status': 'active',
322 'is_public': True,
323 'disk_format': 'vhd',
324 'container_format': 'ovf',
325 'name': 'new name! #123',
326 'size': 19,
327 'checksum': None}
328
329 glance.registry.db.api.image_create(None, extra_fixture)
330
331 extra_fixture = {'id': 4,
332 'status': 'active',
333 'is_public': True,
334 'disk_format': 'vhd',
335 'container_format': 'ovf',
336 'name': 'new name! #123',
337 'size': 20,
338 'checksum': None}
339
340 glance.registry.db.api.image_create(None, extra_fixture)
341
342 req = webob.Request.blank('/images/detail?marker=3&limit=1')
343 res = req.get_response(self.api)
344 res_dict = json.loads(res.body)
345 self.assertEquals(res.status_int, 200)
346
347 images = res_dict['images']
348 self.assertEquals(len(images), 1)
349
350 # expect list to be sorted by created_at desc
351 self.assertEqual(int(images[0]['id']), 2)
352
160 def test_get_details_filter_name(self):353 def test_get_details_filter_name(self):
161 """Tests that the /images/detail registry API returns list of354 """Tests that the /images/detail registry API returns list of
162 public images that have a specific name355 public images that have a specific name
163356
=== modified file 'tests/unit/test_clients.py'
--- tests/unit/test_clients.py 2011-05-24 19:07:19 +0000
+++ tests/unit/test_clients.py 2011-05-31 13:23:37 +0000
@@ -70,6 +70,92 @@
70 for k, v in fixture.items():70 for k, v in fixture.items():
71 self.assertEquals(v, images[0][k])71 self.assertEquals(v, images[0][k])
7272
73 def test_get_image_index_marker(self):
74 """Test correct set of images returned with marker param."""
75 extra_fixture = {'id': 3,
76 'status': 'saving',
77 'is_public': True,
78 'disk_format': 'vhd',
79 'container_format': 'ovf',
80 'name': 'new name! #123',
81 'size': 19,
82 'checksum': None}
83
84 glance.registry.db.api.image_create(None, extra_fixture)
85
86 extra_fixture = {'id': 4,
87 'status': 'saving',
88 'is_public': True,
89 'disk_format': 'vhd',
90 'container_format': 'ovf',
91 'name': 'new name! #125',
92 'size': 19,
93 'checksum': None}
94
95 glance.registry.db.api.image_create(None, extra_fixture)
96
97 images = self.client.get_images(marker=4)
98 self.assertEquals(len(images), 2)
99
100 for image in images:
101 self.assertTrue(image['id'] < 4)
102
103 def test_get_image_index_limit(self):
104 """Test correct number of images returned with limit param."""
105 extra_fixture = {'id': 3,
106 'status': 'saving',
107 'is_public': True,
108 'disk_format': 'vhd',
109 'container_format': 'ovf',
110 'name': 'new name! #123',
111 'size': 19,
112 'checksum': None}
113
114 glance.registry.db.api.image_create(None, extra_fixture)
115
116 extra_fixture = {'id': 4,
117 'status': 'saving',
118 'is_public': True,
119 'disk_format': 'vhd',
120 'container_format': 'ovf',
121 'name': 'new name! #125',
122 'size': 19,
123 'checksum': None}
124
125 glance.registry.db.api.image_create(None, extra_fixture)
126
127 images = self.client.get_images(limit=2)
128 self.assertEquals(len(images), 2)
129
130 def test_get_image_index_marker_limit(self):
131 """Test correct set of images returned with marker/limit params."""
132 extra_fixture = {'id': 3,
133 'status': 'saving',
134 'is_public': True,
135 'disk_format': 'vhd',
136 'container_format': 'ovf',
137 'name': 'new name! #123',
138 'size': 19,
139 'checksum': None}
140
141 glance.registry.db.api.image_create(None, extra_fixture)
142
143 extra_fixture = {'id': 4,
144 'status': 'saving',
145 'is_public': True,
146 'disk_format': 'vhd',
147 'container_format': 'ovf',
148 'name': 'new name! #125',
149 'size': 19,
150 'checksum': None}
151
152 glance.registry.db.api.image_create(None, extra_fixture)
153
154 images = self.client.get_images(marker=3, limit=1)
155 self.assertEquals(len(images), 1)
156
157 self.assertEquals(images[0]['id'], 2)
158
73 def test_get_image_index_by_name(self):159 def test_get_image_index_by_name(self):
74 """Test correct set of public, name-filtered image returned. This160 """Test correct set of public, name-filtered image returned. This
75 is just a sanity check, we test the details call more in-depth."""161 is just a sanity check, we test the details call more in-depth."""
@@ -108,6 +194,35 @@
108 for k, v in fixture.items():194 for k, v in fixture.items():
109 self.assertEquals(v, images[0][k])195 self.assertEquals(v, images[0][k])
110196
197 def test_get_image_details_marker_limit(self):
198 """Test correct set of images returned with marker/limit params."""
199 extra_fixture = {'id': 3,
200 'status': 'saving',
201 'is_public': True,
202 'disk_format': 'vhd',
203 'container_format': 'ovf',
204 'name': 'new name! #123',
205 'size': 19,
206 'checksum': None}
207
208 glance.registry.db.api.image_create(None, extra_fixture)
209
210 extra_fixture = {'id': 4,
211 'status': 'saving',
212 'is_public': True,
213 'disk_format': 'vhd',
214 'container_format': 'ovf',
215 'name': 'new name! #125',
216 'size': 19,
217 'checksum': None}
218
219 glance.registry.db.api.image_create(None, extra_fixture)
220
221 images = self.client.get_images_detailed(marker=3, limit=1)
222 self.assertEquals(len(images), 1)
223
224 self.assertEquals(images[0]['id'], 2)
225
111 def test_get_image_details_by_name(self):226 def test_get_image_details_by_name(self):
112 """Tests that a detailed call can be filtered by name"""227 """Tests that a detailed call can be filtered by name"""
113 extra_fixture = {'id': 3,228 extra_fixture = {'id': 3,
@@ -457,6 +572,92 @@
457 for k, v in fixture.items():572 for k, v in fixture.items():
458 self.assertEquals(v, images[0][k])573 self.assertEquals(v, images[0][k])
459574
575 def test_get_image_index_marker(self):
576 """Test correct set of public images returned with marker param."""
577 extra_fixture = {'id': 3,
578 'status': 'saving',
579 'is_public': True,
580 'disk_format': 'vhd',
581 'container_format': 'ovf',
582 'name': 'new name! #123',
583 'size': 19,
584 'checksum': None}
585
586 glance.registry.db.api.image_create(None, extra_fixture)
587
588 extra_fixture = {'id': 4,
589 'status': 'saving',
590 'is_public': True,
591 'disk_format': 'vhd',
592 'container_format': 'ovf',
593 'name': 'new name! #125',
594 'size': 19,
595 'checksum': None}
596
597 glance.registry.db.api.image_create(None, extra_fixture)
598
599 images = self.client.get_images(marker=4)
600 self.assertEquals(len(images), 2)
601
602 for image in images:
603 self.assertTrue(image['id'] < 4)
604
605 def test_get_image_index_limit(self):
606 """Test correct number of public images returned with limit param."""
607 extra_fixture = {'id': 3,
608 'status': 'saving',
609 'is_public': True,
610 'disk_format': 'vhd',
611 'container_format': 'ovf',
612 'name': 'new name! #123',
613 'size': 19,
614 'checksum': None}
615
616 glance.registry.db.api.image_create(None, extra_fixture)
617
618 extra_fixture = {'id': 4,
619 'status': 'saving',
620 'is_public': True,
621 'disk_format': 'vhd',
622 'container_format': 'ovf',
623 'name': 'new name! #125',
624 'size': 19,
625 'checksum': None}
626
627 glance.registry.db.api.image_create(None, extra_fixture)
628
629 images = self.client.get_images(limit=2)
630 self.assertEquals(len(images), 2)
631
632 def test_get_image_index_marker_limit(self):
633 """Test correct set of images returned with marker/limit params."""
634 extra_fixture = {'id': 3,
635 'status': 'saving',
636 'is_public': True,
637 'disk_format': 'vhd',
638 'container_format': 'ovf',
639 'name': 'new name! #123',
640 'size': 19,
641 'checksum': None}
642
643 glance.registry.db.api.image_create(None, extra_fixture)
644
645 extra_fixture = {'id': 4,
646 'status': 'saving',
647 'is_public': True,
648 'disk_format': 'vhd',
649 'container_format': 'ovf',
650 'name': 'new name! #125',
651 'size': 19,
652 'checksum': None}
653
654 glance.registry.db.api.image_create(None, extra_fixture)
655
656 images = self.client.get_images(marker=3, limit=1)
657 self.assertEquals(len(images), 1)
658
659 self.assertEquals(images[0]['id'], 2)
660
460 def test_get_image_index_by_base_attribute(self):661 def test_get_image_index_by_base_attribute(self):
461 """Tests that an index call can be filtered by a base attribute"""662 """Tests that an index call can be filtered by a base attribute"""
462 extra_fixture = {'id': 3,663 extra_fixture = {'id': 3,
@@ -522,6 +723,35 @@
522 for k, v in expected.items():723 for k, v in expected.items():
523 self.assertEquals(v, images[0][k])724 self.assertEquals(v, images[0][k])
524725
726 def test_get_image_details_marker_limit(self):
727 """Test detailed calls are filtered by marker/limit params."""
728 extra_fixture = {'id': 3,
729 'status': 'saving',
730 'is_public': True,
731 'disk_format': 'vhd',
732 'container_format': 'ovf',
733 'name': 'new name! #123',
734 'size': 19,
735 'checksum': None}
736
737 glance.registry.db.api.image_create(None, extra_fixture)
738
739 extra_fixture = {'id': 4,
740 'status': 'saving',
741 'is_public': True,
742 'disk_format': 'vhd',
743 'container_format': 'ovf',
744 'name': 'new name! #125',
745 'size': 19,
746 'checksum': None}
747
748 glance.registry.db.api.image_create(None, extra_fixture)
749
750 images = self.client.get_images_detailed(marker=3, limit=1)
751 self.assertEquals(len(images), 1)
752
753 self.assertEquals(images[0]['id'], 2)
754
525 def test_get_image_details_by_base_attribute(self):755 def test_get_image_details_by_base_attribute(self):
526 """Tests that a detailed call can be filtered by a base attribute"""756 """Tests that a detailed call can be filtered by a base attribute"""
527 extra_fixture = {'id': 3,757 extra_fixture = {'id': 3,

Subscribers

People subscribed via source and target branches