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

Proposed by Brian Waldon
Status: Merged
Approved by: Dan Prince
Approved revision: 139
Merged at revision: 132
Proposed branch: lp:~rackspace-titan/glance/api-results-filtering
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 1095 lines (+860/-38)
9 files modified
glance/api/v1/images.py (+21/-2)
glance/registry/__init__.py (+8/-8)
glance/registry/client.py (+16/-15)
glance/registry/db/api.py (+29/-5)
glance/registry/server.py (+31/-4)
tests/functional/test_curl_api.py (+210/-0)
tests/stubs.py (+26/-3)
tests/unit/test_api.py (+363/-0)
tests/unit/test_clients.py (+156/-1)
To merge this branch: bzr merge lp:~rackspace-titan/glance/api-results-filtering
Reviewer Review Type Date Requested Status
Jay Pipes (community) Approve
Dan Prince (community) Approve
Review via email: mp+61056@code.launchpad.net

Description of the change

Adding support for api query filtering
- equality testing on select attributes: name, status, container_format, disk_format
- relative comparison of size attribute with size_min, size_max
- equality testing on user-defined properties (preface property name with "property-" in query)

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

I could use some help in registry/db/api.py with the sqlalchemy code. See the TODO

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

162 + #TODO(bcwaldon): use an actual sqlalchemy query to accomplish this
163 + def prop_filter(key, value):
164 + def func(image):
165 + for prop in image.properties:
166 + if prop.deleted == False and \
167 + prop.name == key and \
168 + prop.value == value:
169 + return True
170 + return False
171 + return func
172 +
173 + for (k, v) in filters.items():
174 + if k.startswith('property-'):
175 + _k = k[9:]
176 + images = filter(prop_filter(_k, v), images)

I should really test this before suggesting it, but:

for (k, v) in filters.items():
 if k.startswith('property-'):
  _k = k[9:]
  query.filter(models.Images.properties.any(and_(
   models.ImageProperty.name == _k,
   models.ImageProperty.value == v)))

Also maybe

for (k, v) in filters.items():
 if k.startswith('property-'):
  _k = k[9:]
  query.filter(models.Images.properties.any(name=_k, value=v))

At the end I think you can then just return query.all()

136. By Brian Waldon

consolidating image_get_all_public and image_get_filtered in registry db api

137. By Brian Waldon

making registry db api filters more structured; adding in a bit of sqlalchemy code to filter image properties more efficiently

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

Thanks, Mark. Your second suggestion ended up working.

I did some refactoring of where the filter query params are parsed, as well.

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

I'm probably gonna get shot up but here it goes... (metadata vs. properties)
--

I think you've added an inconsistency in the HTTP API's by using 'property-'. While we call them properties internally we usually refer to them as 'meta' or metadata in the external interfaces to glance.

The bin/glance utility for example uses the following (snips):

    update Updates an image's metadata in Glance
    clear Removes all images and metadata from Glance

Additionally we update metadata(aka properties) with HTTP headers that look like this 'X-Image-Meta-Is-Public: True'.

Given these examples how would you feel about using 'meta-' instead of 'property-' for consistency?

---

Also a minor nit with the import statements in glance/registry/client.py. Most of those import statements can now be removed since they are now defined and used only within the BaseClient class.

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

>
> I'm probably gonna get shot up but here it goes... (metadata vs. properties)
> --
>
> I think you've added an inconsistency in the HTTP API's by using 'property-'.
> While we call them properties internally we usually refer to them as 'meta' or
> metadata in the external interfaces to glance.
>
> The bin/glance utility for example uses the following (snips):
>
> update Updates an image's metadata in Glance
> clear Removes all images and metadata from Glance
>
> Additionally we update metadata(aka properties) with HTTP headers that look
> like this 'X-Image-Meta-Is-Public: True'.
>
> Given these examples how would you feel about using 'meta-' instead of
> 'property-' for consistency?
>

I definitely understand why you would be concerned, but I think you may have overlooked one thing. I only use "property-" for user-defined image metadata, which you provide in the headers with "X-Image-Meta-Property..." The image data structure even uses a dictionary called "properties" to store the values I am referring to. The "X-Image-Meta-" header is reserved for actual image attributes, such as "name" and "status".

> Also a minor nit with the import statements in glance/registry/client.py. Most
> of those import statements can now be removed since they are now defined and
> used only within the BaseClient class.

Good catch. Removed those I found to be unnecessary.

138. By Brian Waldon

removing some unnecessary imports

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

Sure. So why not make it 'meta-property-' for consistency then?

I realize this makes the URL for filtering a bit longer but I really like the consistency. If Glance could make up its mind and just go for either 'metadata' and/or 'properties' this sort of thing might be easier.

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

> Sure. So why not make it 'meta-property-' for consistency then?
>
> I realize this makes the URL for filtering a bit longer but I really like the
> consistency. If Glance could make up its mind and just go for either
> 'metadata' and/or 'properties' this sort of thing might be easier.

I'm still a bit confused. If I were to make it 'meta-property-' then I would have to make the other filters 'meta-name', 'meta-status', and so on. I don't think it is as confusing as you are making it out to be.

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

Sure. I no confusion. Just consistency concerns. Thanks for the clarification. I'm good with this. Good work.

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

> Sure. So why not make it 'meta-property-' for consistency then?
>
> I realize this makes the URL for filtering a bit longer but I really like the
> consistency. If Glance could make up its mind and just go for either
> 'metadata' and/or 'properties' this sort of thing might be easier.

It's not about making up our minds. :) It's because they are different things. I would have preferred not using the term metadata at all, but that's what is used for "base image attributes". "properties" are the custom key-value attribute pairs attached to the image.

-jay

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

Hey Brian,

Excellent work overall. Just one thing to note, though...

34 + """Return a dictionary of query param filters from the request
35 +
36 + :param req: the Request object coming from the wsgi layer

In Glance, much of the code uses the docstring style:

"""
Return a dictionary of query param filters from the request

:param req: the Request object coming from the wsgi layer
...
"""

Feel free to use it too if you prefer it. ;) I personally can't stand the Nova style, as I think it makes things harder to read.

-jay

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

> Hey Brian,
>
> Excellent work overall. Just one thing to note, though...
>
> 34 + """Return a dictionary of query param filters from the request
> 35 +
> 36 + :param req: the Request object coming from the wsgi layer
>
> In Glance, much of the code uses the docstring style:
>
> """
> Return a dictionary of query param filters from the request
>
> :param req: the Request object coming from the wsgi layer
> ...
> """
>
> Feel free to use it too if you prefer it. ;) I personally can't stand the Nova
> style, as I think it makes things harder to read.
>
> -jay

Made the change. I do prefer your style, and typically keep with whatever style is already established in the file. I guess I was in nova-mode.

139. By Brian Waldon

docstring fix

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'glance/api/v1/images.py'
2--- glance/api/v1/images.py 2011-05-11 23:03:51 +0000
3+++ glance/api/v1/images.py 2011-05-17 13:32:06 +0000
4@@ -42,6 +42,9 @@
5
6 logger = logging.getLogger('glance.api.v1.images')
7
8+SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
9+ 'size_min', 'size_max']
10+
11
12 class Controller(wsgi.Controller):
13
14@@ -89,7 +92,8 @@
15 'size': <SIZE>}, ...
16 ]}
17 """
18- images = registry.get_images_list(self.options)
19+ filters = self._get_filters(req)
20+ images = registry.get_images_list(self.options, filters)
21 return dict(images=images)
22
23 def detail(self, req):
24@@ -114,9 +118,24 @@
25 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
26 ]}
27 """
28- images = registry.get_images_detail(self.options)
29+ filters = self._get_filters(req)
30+ images = registry.get_images_detail(self.options, filters)
31 return dict(images=images)
32
33+ def _get_filters(self, req):
34+ """
35+ Return a dictionary of query param filters from the request
36+
37+ :param req: the Request object coming from the wsgi layer
38+ :retval a dict of key/value filters
39+ """
40+ filters = {}
41+ for param in req.str_params:
42+ if param in SUPPORTED_FILTERS or param.startswith('property-'):
43+ filters[param] = req.str_params.get(param)
44+
45+ return filters
46+
47 def meta(self, req, id):
48 """
49 Returns metadata about an image in the HTTP headers of the
50
51=== modified file 'glance/registry/__init__.py'
52--- glance/registry/__init__.py 2011-03-29 14:27:24 +0000
53+++ glance/registry/__init__.py 2011-05-17 13:32:06 +0000
54@@ -32,14 +32,14 @@
55 return client.RegistryClient(host, port)
56
57
58-def get_images_list(options):
59- c = get_registry_client(options)
60- return c.get_images()
61-
62-
63-def get_images_detail(options):
64- c = get_registry_client(options)
65- return c.get_images_detailed()
66+def get_images_list(options, filters):
67+ c = get_registry_client(options)
68+ return c.get_images(filters)
69+
70+
71+def get_images_detail(options, filters):
72+ c = get_registry_client(options)
73+ return c.get_images_detailed(filters)
74
75
76 def get_image_metadata(options, image_id):
77
78=== modified file 'glance/registry/client.py'
79--- glance/registry/client.py 2011-03-29 14:27:24 +0000
80+++ glance/registry/client.py 2011-05-17 13:32:06 +0000
81@@ -20,14 +20,9 @@
82 the Glance Registry API
83 """
84
85-import httplib
86 import json
87-import logging
88-import urlparse
89-import socket
90-import sys
91+import urllib
92
93-from glance.common import exception
94 from glance.client import BaseClient
95
96
97@@ -49,28 +44,34 @@
98 port = port or self.DEFAULT_PORT
99 super(RegistryClient, self).__init__(host, port, use_ssl)
100
101- def get_images(self):
102+ def get_images(self, filters=None):
103 """
104 Returns a list of image id/name mappings from Registry
105 """
106- res = self.do_request("GET", "/images")
107+ if filters != None:
108+ action = "/images?%s" % urllib.urlencode(filters)
109+ else:
110+ action = "/images"
111+
112+ res = self.do_request("GET", action)
113 data = json.loads(res.read())['images']
114 return data
115
116- def get_images_detailed(self):
117+ def get_images_detailed(self, filters=None):
118 """
119 Returns a list of detailed image data mappings from Registry
120 """
121- res = self.do_request("GET", "/images/detail")
122+ if filters != None:
123+ action = "/images/detail?%s" % urllib.urlencode(filters)
124+ else:
125+ action = "/images/detail"
126+
127+ res = self.do_request("GET", action)
128 data = json.loads(res.read())['images']
129 return data
130
131 def get_image(self, image_id):
132- """
133- Returns a mapping of image metadata from Registry
134-
135- :raises exception.NotFound if image is not in registry
136- """
137+ """Returns a mapping of image metadata from Registry"""
138 res = self.do_request("GET", "/images/%s" % image_id)
139 data = json.loads(res.read())['image']
140 return data
141
142=== modified file 'glance/registry/db/api.py'
143--- glance/registry/db/api.py 2011-05-04 15:03:28 +0000
144+++ glance/registry/db/api.py 2011-05-17 13:32:06 +0000
145@@ -139,15 +139,39 @@
146 raise exception.NotFound("No image found with ID %s" % image_id)
147
148
149-def image_get_all_public(context):
150- """Get all public images."""
151+def image_get_all_public(context, filters=None):
152+ """Get all public images that match zero or more filters.
153+
154+ :param filters: dict of filter keys and values. If a 'properties'
155+ key is present, it is treated as a dict of key/value
156+ filters on the image properties attribute
157+
158+ """
159+ if filters == None:
160+ filters = {}
161+
162 session = get_session()
163- return session.query(models.Image).\
164+ query = session.query(models.Image).\
165 options(joinedload(models.Image.properties)).\
166 filter_by(deleted=_deleted(context)).\
167 filter_by(is_public=True).\
168- filter(models.Image.status != 'killed').\
169- all()
170+ filter(models.Image.status != 'killed')
171+
172+ if 'size_min' in filters:
173+ query = query.filter(models.Image.size >= filters['size_min'])
174+ del filters['size_min']
175+
176+ if 'size_max' in filters:
177+ query = query.filter(models.Image.size <= filters['size_max'])
178+ del filters['size_max']
179+
180+ for (k, v) in filters.pop('properties', {}).items():
181+ query = query.filter(models.Image.properties.any(name=k, value=v))
182+
183+ for (k, v) in filters.items():
184+ query = query.filter(getattr(models.Image, k) == v)
185+
186+ return query.all()
187
188
189 def _drop_protected_attrs(model_class, values):
190
191=== modified file 'glance/registry/server.py'
192--- glance/registry/server.py 2011-04-07 19:07:36 +0000
193+++ glance/registry/server.py 2011-05-17 13:32:06 +0000
194@@ -36,6 +36,9 @@
195 'disk_format', 'container_format',
196 'checksum']
197
198+SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
199+ 'size_min', 'size_max']
200+
201
202 class Controller(wsgi.Controller):
203 """Controller for the reference implementation registry server"""
204@@ -45,7 +48,7 @@
205 db_api.configure_db(options)
206
207 def index(self, req):
208- """Return basic information for all public, non-deleted images
209+ """Return a basic filtered list of public, non-deleted images
210
211 :param req: the Request object coming from the wsgi layer
212 :retval a mapping of the following form::
213@@ -64,7 +67,8 @@
214 }
215
216 """
217- images = db_api.image_get_all_public(None)
218+ images = db_api.image_get_all_public(None, self._get_filters(req))
219+
220 results = []
221 for image in images:
222 result = {}
223@@ -74,7 +78,7 @@
224 return dict(images=results)
225
226 def detail(self, req):
227- """Return detailed information for all public, non-deleted images
228+ """Return a filtered list of public, non-deleted images in detail
229
230 :param req: the Request object coming from the wsgi layer
231 :retval a mapping of the following form::
232@@ -85,10 +89,33 @@
233 all image model fields.
234
235 """
236- images = db_api.image_get_all_public(None)
237+ images = db_api.image_get_all_public(None, self._get_filters(req))
238+
239 image_dicts = [make_image_dict(i) for i in images]
240 return dict(images=image_dicts)
241
242+ def _get_filters(self, req):
243+ """Return a dictionary of query param filters from the request
244+
245+ :param req: the Request object coming from the wsgi layer
246+ :retval a dict of key/value filters
247+
248+ """
249+ filters = {}
250+ properties = {}
251+
252+ for param in req.str_params:
253+ if param in SUPPORTED_FILTERS:
254+ filters[param] = req.str_params.get(param)
255+ if param.startswith('property-'):
256+ _param = param[9:]
257+ properties[_param] = req.str_params.get(param)
258+
259+ if len(properties) > 0:
260+ filters['properties'] = properties
261+
262+ return filters
263+
264 def show(self, req, id):
265 """Return data about the given image id."""
266 try:
267
268=== modified file 'tests/functional/test_curl_api.py'
269--- tests/functional/test_curl_api.py 2011-05-13 22:28:51 +0000
270+++ tests/functional/test_curl_api.py 2011-05-17 13:32:06 +0000
271@@ -787,3 +787,213 @@
272 "Could not find '%s' in '%s'" % (expected, out))
273
274 self.stop_servers()
275+
276+ def test_filtered_images(self):
277+ """
278+ Set up three test images and ensure each query param filter works
279+ """
280+ self.cleanup()
281+ self.start_servers()
282+
283+ api_port = self.api_port
284+ registry_port = self.registry_port
285+
286+ # 0. GET /images
287+ # Verify no public images
288+ cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
289+
290+ exitcode, out, err = execute(cmd)
291+
292+ self.assertEqual(0, exitcode)
293+ self.assertEqual('{"images": []}', out.strip())
294+
295+ # 1. POST /images with three public images with various attributes
296+ cmd = ("curl -i -X POST "
297+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
298+ "-H 'X-Image-Meta-Name: Image1' "
299+ "-H 'X-Image-Meta-Status: active' "
300+ "-H 'X-Image-Meta-Container-Format: ovf' "
301+ "-H 'X-Image-Meta-Disk-Format: vdi' "
302+ "-H 'X-Image-Meta-Size: 19' "
303+ "-H 'X-Image-Meta-Is-Public: True' "
304+ "-H 'X-Image-Meta-Property-pants: are on' "
305+ "http://0.0.0.0:%d/v1/images") % api_port
306+
307+ exitcode, out, err = execute(cmd)
308+ self.assertEqual(0, exitcode)
309+
310+ lines = out.split("\r\n")
311+ status_line = lines[0]
312+
313+ self.assertEqual("HTTP/1.1 201 Created", status_line)
314+
315+ cmd = ("curl -i -X POST "
316+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
317+ "-H 'X-Image-Meta-Name: My Image!' "
318+ "-H 'X-Image-Meta-Status: active' "
319+ "-H 'X-Image-Meta-Container-Format: ovf' "
320+ "-H 'X-Image-Meta-Disk-Format: vhd' "
321+ "-H 'X-Image-Meta-Size: 20' "
322+ "-H 'X-Image-Meta-Is-Public: True' "
323+ "-H 'X-Image-Meta-Property-pants: are on' "
324+ "http://0.0.0.0:%d/v1/images") % api_port
325+
326+ exitcode, out, err = execute(cmd)
327+ self.assertEqual(0, exitcode)
328+
329+ lines = out.split("\r\n")
330+ status_line = lines[0]
331+
332+ self.assertEqual("HTTP/1.1 201 Created", status_line)
333+ cmd = ("curl -i -X POST "
334+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
335+ "-H 'X-Image-Meta-Name: My Image!' "
336+ "-H 'X-Image-Meta-Status: saving' "
337+ "-H 'X-Image-Meta-Container-Format: ami' "
338+ "-H 'X-Image-Meta-Disk-Format: ami' "
339+ "-H 'X-Image-Meta-Size: 21' "
340+ "-H 'X-Image-Meta-Is-Public: True' "
341+ "-H 'X-Image-Meta-Property-pants: are off' "
342+ "http://0.0.0.0:%d/v1/images") % api_port
343+
344+ exitcode, out, err = execute(cmd)
345+ self.assertEqual(0, exitcode)
346+
347+ lines = out.split("\r\n")
348+ status_line = lines[0]
349+
350+ self.assertEqual("HTTP/1.1 201 Created", status_line)
351+
352+ # 2. GET /images
353+ # Verify three public images
354+ cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
355+
356+ exitcode, out, err = execute(cmd)
357+
358+ self.assertEqual(0, exitcode)
359+ images = json.loads(out.strip())
360+
361+ self.assertEqual(len(images["images"]), 3)
362+
363+ # 3. GET /images with name filter
364+ # Verify correct images returned with name
365+ cmd = "curl http://0.0.0.0:%d/v1/images?name=My%%20Image!" % api_port
366+
367+ exitcode, out, err = execute(cmd)
368+
369+ self.assertEqual(0, exitcode)
370+ images = json.loads(out.strip())
371+
372+ self.assertEqual(len(images["images"]), 2)
373+ for image in images["images"]:
374+ self.assertEqual(image["name"], "My Image!")
375+
376+ # 4. GET /images with status filter
377+ # Verify correct images returned with status
378+ cmd = ("curl http://0.0.0.0:%d/v1/images/detail?status=queued"
379+ % api_port)
380+
381+ exitcode, out, err = execute(cmd)
382+
383+ self.assertEqual(0, exitcode)
384+ images = json.loads(out.strip())
385+
386+ self.assertEqual(len(images["images"]), 3)
387+ for image in images["images"]:
388+ self.assertEqual(image["status"], "queued")
389+
390+ cmd = ("curl http://0.0.0.0:%d/v1/images/detail?status=active"
391+ % api_port)
392+
393+ exitcode, out, err = execute(cmd)
394+
395+ self.assertEqual(0, exitcode)
396+ images = json.loads(out.strip())
397+
398+ self.assertEqual(len(images["images"]), 0)
399+
400+ # 5. GET /images with container_format filter
401+ # Verify correct images returned with container_format
402+ cmd = ("curl http://0.0.0.0:%d/v1/images?container_format=ovf"
403+ % api_port)
404+
405+ exitcode, out, err = execute(cmd)
406+
407+ self.assertEqual(0, exitcode)
408+ images = json.loads(out.strip())
409+
410+ self.assertEqual(len(images["images"]), 2)
411+ for image in images["images"]:
412+ self.assertEqual(image["container_format"], "ovf")
413+
414+ # 6. GET /images with disk_format filter
415+ # Verify correct images returned with disk_format
416+ cmd = ("curl http://0.0.0.0:%d/v1/images?disk_format=vdi"
417+ % api_port)
418+
419+ exitcode, out, err = execute(cmd)
420+
421+ self.assertEqual(0, exitcode)
422+ images = json.loads(out.strip())
423+
424+ self.assertEqual(len(images["images"]), 1)
425+ for image in images["images"]:
426+ self.assertEqual(image["disk_format"], "vdi")
427+
428+ # 7. GET /images with size_max filter
429+ # Verify correct images returned with size <= expected
430+ cmd = ("curl http://0.0.0.0:%d/v1/images?size_max=20"
431+ % api_port)
432+
433+ exitcode, out, err = execute(cmd)
434+
435+ self.assertEqual(0, exitcode)
436+ images = json.loads(out.strip())
437+
438+ self.assertEqual(len(images["images"]), 2)
439+ for image in images["images"]:
440+ self.assertTrue(image["size"] <= 20)
441+
442+ # 8. GET /images with size_min filter
443+ # Verify correct images returned with size >= expected
444+ cmd = ("curl http://0.0.0.0:%d/v1/images?size_min=20"
445+ % api_port)
446+
447+ exitcode, out, err = execute(cmd)
448+
449+ self.assertEqual(0, exitcode)
450+ images = json.loads(out.strip())
451+
452+ self.assertEqual(len(images["images"]), 2)
453+ for image in images["images"]:
454+ self.assertTrue(image["size"] >= 20)
455+
456+ # 9. GET /images with property filter
457+ # Verify correct images returned with property
458+ cmd = ("curl http://0.0.0.0:%d/v1/images/detail?"
459+ "property-pants=are%%20on" % api_port)
460+
461+ exitcode, out, err = execute(cmd)
462+
463+ self.assertEqual(0, exitcode)
464+ images = json.loads(out.strip())
465+
466+ self.assertEqual(len(images["images"]), 2)
467+ for image in images["images"]:
468+ self.assertEqual(image["properties"]["pants"], "are on")
469+
470+ # 10. GET /images with property filter and name filter
471+ # Verify correct images returned with property and name
472+ # Make sure you quote the url when using more than one param!
473+ cmd = ("curl 'http://0.0.0.0:%d/v1/images/detail?"
474+ "name=My%%20Image!&property-pants=are%%20on'" % api_port)
475+
476+ exitcode, out, err = execute(cmd)
477+
478+ self.assertEqual(0, exitcode)
479+ images = json.loads(out.strip())
480+
481+ self.assertEqual(len(images["images"]), 1)
482+ for image in images["images"]:
483+ self.assertEqual(image["properties"]["pants"], "are on")
484+ self.assertEqual(image["name"], "My Image!")
485
486=== modified file 'tests/stubs.py'
487--- tests/stubs.py 2011-05-11 23:03:51 +0000
488+++ tests/stubs.py 2011-05-17 13:32:06 +0000
489@@ -386,9 +386,32 @@
490 else:
491 return images[0]
492
493- def image_get_all_public(self, _context, public=True):
494- return [f for f in self.images
495- if f['is_public'] == public]
496+ def image_get_all_public(self, _context, filters):
497+ images = [f for f in self.images if f['is_public'] == True]
498+
499+ if 'size_min' in filters:
500+ size_min = int(filters.pop('size_min'))
501+ images = [f for f in images if int(f['size']) >= size_min]
502+
503+ if 'size_max' in filters:
504+ size_max = int(filters.pop('size_max'))
505+ images = [f for f in images if int(f['size']) <= size_max]
506+
507+ def _prop_filter(key, value):
508+ def _func(image):
509+ for prop in image['properties']:
510+ if prop['name'] == key:
511+ return prop['value'] == value
512+ return False
513+ return _func
514+
515+ for k, v in filters.pop('properties', {}).items():
516+ images = filter(_prop_filter(k, v), images)
517+
518+ for k, v in filters.items():
519+ images = [f for f in images if f[k] == v]
520+
521+ return images
522
523 fake_datastore = FakeDatastore()
524 stubs.Set(glance.registry.db.api, 'image_create',
525
526=== modified file 'tests/unit/test_api.py'
527--- tests/unit/test_api.py 2011-05-11 23:03:51 +0000
528+++ tests/unit/test_api.py 2011-05-17 13:32:06 +0000
529@@ -26,6 +26,7 @@
530
531 from glance.api import v1 as server
532 from glance.registry import server as rserver
533+import glance.registry.db.api
534 from tests import stubs
535
536 VERBOSE = False
537@@ -87,6 +88,50 @@
538 for k, v in fixture.iteritems():
539 self.assertEquals(v, images[0][k])
540
541+ def test_get_index_filter_name(self):
542+ """Tests that the /images registry API returns list of
543+ public images that have a specific name. This is really a sanity
544+ check, filtering is tested more in-depth using /images/detail
545+
546+ """
547+ fixture = {'id': 2,
548+ 'name': 'fake image #2',
549+ 'size': 19,
550+ 'checksum': None}
551+
552+ extra_fixture = {'id': 3,
553+ 'status': 'active',
554+ 'is_public': True,
555+ 'disk_format': 'vhd',
556+ 'container_format': 'ovf',
557+ 'name': 'new name! #123',
558+ 'size': 19,
559+ 'checksum': None}
560+
561+ glance.registry.db.api.image_create(None, extra_fixture)
562+
563+ extra_fixture = {'id': 4,
564+ 'status': 'active',
565+ 'is_public': True,
566+ 'disk_format': 'vhd',
567+ 'container_format': 'ovf',
568+ 'name': 'new name! #123',
569+ 'size': 20,
570+ 'checksum': None}
571+
572+ glance.registry.db.api.image_create(None, extra_fixture)
573+
574+ req = webob.Request.blank('/images?name=new name! #123')
575+ res = req.get_response(self.api)
576+ res_dict = json.loads(res.body)
577+ self.assertEquals(res.status_int, 200)
578+
579+ images = res_dict['images']
580+ self.assertEquals(len(images), 2)
581+
582+ for image in images:
583+ self.assertEqual('new name! #123', image['name'])
584+
585 def test_get_details(self):
586 """Tests that the /images/detail registry API returns
587 a mapping containing a list of detailed image information
588@@ -112,6 +157,324 @@
589 for k, v in fixture.iteritems():
590 self.assertEquals(v, images[0][k])
591
592+ def test_get_details_filter_name(self):
593+ """Tests that the /images/detail registry API returns list of
594+ public images that have a specific name
595+
596+ """
597+ extra_fixture = {'id': 3,
598+ 'status': 'active',
599+ 'is_public': True,
600+ 'disk_format': 'vhd',
601+ 'container_format': 'ovf',
602+ 'name': 'new name! #123',
603+ 'size': 19,
604+ 'checksum': None}
605+
606+ glance.registry.db.api.image_create(None, extra_fixture)
607+
608+ extra_fixture = {'id': 4,
609+ 'status': 'active',
610+ 'is_public': True,
611+ 'disk_format': 'vhd',
612+ 'container_format': 'ovf',
613+ 'name': 'new name! #123',
614+ 'size': 20,
615+ 'checksum': None}
616+
617+ glance.registry.db.api.image_create(None, extra_fixture)
618+
619+ req = webob.Request.blank('/images/detail?name=new name! #123')
620+ res = req.get_response(self.api)
621+ res_dict = json.loads(res.body)
622+ self.assertEquals(res.status_int, 200)
623+
624+ images = res_dict['images']
625+ self.assertEquals(len(images), 2)
626+
627+ for image in images:
628+ self.assertEqual('new name! #123', image['name'])
629+
630+ def test_get_details_filter_status(self):
631+ """Tests that the /images/detail registry API returns list of
632+ public images that have a specific status
633+
634+ """
635+ extra_fixture = {'id': 3,
636+ 'status': 'saving',
637+ 'is_public': True,
638+ 'disk_format': 'vhd',
639+ 'container_format': 'ovf',
640+ 'name': 'fake image #3',
641+ 'size': 19,
642+ 'checksum': None}
643+
644+ glance.registry.db.api.image_create(None, extra_fixture)
645+
646+ extra_fixture = {'id': 4,
647+ 'status': 'active',
648+ 'is_public': True,
649+ 'disk_format': 'vhd',
650+ 'container_format': 'ovf',
651+ 'name': 'fake image #4',
652+ 'size': 19,
653+ 'checksum': None}
654+
655+ glance.registry.db.api.image_create(None, extra_fixture)
656+
657+ req = webob.Request.blank('/images/detail?status=saving')
658+ res = req.get_response(self.api)
659+ res_dict = json.loads(res.body)
660+ self.assertEquals(res.status_int, 200)
661+
662+ images = res_dict['images']
663+ self.assertEquals(len(images), 1)
664+
665+ for image in images:
666+ self.assertEqual('saving', image['status'])
667+
668+ def test_get_details_filter_container_format(self):
669+ """Tests that the /images/detail registry API returns list of
670+ public images that have a specific container_format
671+
672+ """
673+ extra_fixture = {'id': 3,
674+ 'status': 'active',
675+ 'is_public': True,
676+ 'disk_format': 'vdi',
677+ 'container_format': 'ovf',
678+ 'name': 'fake image #3',
679+ 'size': 19,
680+ 'checksum': None}
681+
682+ glance.registry.db.api.image_create(None, extra_fixture)
683+
684+ extra_fixture = {'id': 4,
685+ 'status': 'active',
686+ 'is_public': True,
687+ 'disk_format': 'ami',
688+ 'container_format': 'ami',
689+ 'name': 'fake image #4',
690+ 'size': 19,
691+ 'checksum': None}
692+
693+ glance.registry.db.api.image_create(None, extra_fixture)
694+
695+ req = webob.Request.blank('/images/detail?container_format=ovf')
696+ res = req.get_response(self.api)
697+ res_dict = json.loads(res.body)
698+ self.assertEquals(res.status_int, 200)
699+
700+ images = res_dict['images']
701+ self.assertEquals(len(images), 2)
702+
703+ for image in images:
704+ self.assertEqual('ovf', image['container_format'])
705+
706+ def test_get_details_filter_disk_format(self):
707+ """Tests that the /images/detail registry API returns list of
708+ public images that have a specific disk_format
709+
710+ """
711+ extra_fixture = {'id': 3,
712+ 'status': 'active',
713+ 'is_public': True,
714+ 'disk_format': 'vhd',
715+ 'container_format': 'ovf',
716+ 'name': 'fake image #3',
717+ 'size': 19,
718+ 'checksum': None}
719+
720+ glance.registry.db.api.image_create(None, extra_fixture)
721+
722+ extra_fixture = {'id': 4,
723+ 'status': 'active',
724+ 'is_public': True,
725+ 'disk_format': 'ami',
726+ 'container_format': 'ami',
727+ 'name': 'fake image #4',
728+ 'size': 19,
729+ 'checksum': None}
730+
731+ glance.registry.db.api.image_create(None, extra_fixture)
732+
733+ req = webob.Request.blank('/images/detail?disk_format=vhd')
734+ res = req.get_response(self.api)
735+ res_dict = json.loads(res.body)
736+ self.assertEquals(res.status_int, 200)
737+
738+ images = res_dict['images']
739+ self.assertEquals(len(images), 2)
740+
741+ for image in images:
742+ self.assertEqual('vhd', image['disk_format'])
743+
744+ def test_get_details_filter_size_min(self):
745+ """Tests that the /images/detail registry API returns list of
746+ public images that have a size greater than or equal to size_min
747+
748+ """
749+ extra_fixture = {'id': 3,
750+ 'status': 'active',
751+ 'is_public': True,
752+ 'disk_format': 'vhd',
753+ 'container_format': 'ovf',
754+ 'name': 'fake image #3',
755+ 'size': 18,
756+ 'checksum': None}
757+
758+ glance.registry.db.api.image_create(None, extra_fixture)
759+
760+ extra_fixture = {'id': 4,
761+ 'status': 'active',
762+ 'is_public': True,
763+ 'disk_format': 'ami',
764+ 'container_format': 'ami',
765+ 'name': 'fake image #4',
766+ 'size': 20,
767+ 'checksum': None}
768+
769+ glance.registry.db.api.image_create(None, extra_fixture)
770+
771+ req = webob.Request.blank('/images/detail?size_min=19')
772+ res = req.get_response(self.api)
773+ res_dict = json.loads(res.body)
774+ self.assertEquals(res.status_int, 200)
775+
776+ images = res_dict['images']
777+ self.assertEquals(len(images), 2)
778+
779+ for image in images:
780+ self.assertTrue(image['size'] >= 19)
781+
782+ def test_get_details_filter_size_max(self):
783+ """Tests that the /images/detail registry API returns list of
784+ public images that have a size less than or equal to size_max
785+
786+ """
787+ extra_fixture = {'id': 3,
788+ 'status': 'active',
789+ 'is_public': True,
790+ 'disk_format': 'vhd',
791+ 'container_format': 'ovf',
792+ 'name': 'fake image #3',
793+ 'size': 18,
794+ 'checksum': None}
795+
796+ glance.registry.db.api.image_create(None, extra_fixture)
797+
798+ extra_fixture = {'id': 4,
799+ 'status': 'active',
800+ 'is_public': True,
801+ 'disk_format': 'ami',
802+ 'container_format': 'ami',
803+ 'name': 'fake image #4',
804+ 'size': 20,
805+ 'checksum': None}
806+
807+ glance.registry.db.api.image_create(None, extra_fixture)
808+
809+ req = webob.Request.blank('/images/detail?size_max=19')
810+ res = req.get_response(self.api)
811+ res_dict = json.loads(res.body)
812+ self.assertEquals(res.status_int, 200)
813+
814+ images = res_dict['images']
815+ self.assertEquals(len(images), 2)
816+
817+ for image in images:
818+ self.assertTrue(image['size'] <= 19)
819+
820+ def test_get_details_filter_size_min_max(self):
821+ """Tests that the /images/detail registry API returns list of
822+ public images that have a size less than or equal to size_max
823+ and greater than or equal to size_min
824+
825+ """
826+ extra_fixture = {'id': 3,
827+ 'status': 'active',
828+ 'is_public': True,
829+ 'disk_format': 'vhd',
830+ 'container_format': 'ovf',
831+ 'name': 'fake image #3',
832+ 'size': 18,
833+ 'checksum': None}
834+
835+ glance.registry.db.api.image_create(None, extra_fixture)
836+
837+ extra_fixture = {'id': 4,
838+ 'status': 'active',
839+ 'is_public': True,
840+ 'disk_format': 'ami',
841+ 'container_format': 'ami',
842+ 'name': 'fake image #4',
843+ 'size': 20,
844+ 'checksum': None}
845+
846+ glance.registry.db.api.image_create(None, extra_fixture)
847+
848+ extra_fixture = {'id': 5,
849+ 'status': 'active',
850+ 'is_public': True,
851+ 'disk_format': 'ami',
852+ 'container_format': 'ami',
853+ 'name': 'fake image #5',
854+ 'size': 6,
855+ 'checksum': None}
856+
857+ glance.registry.db.api.image_create(None, extra_fixture)
858+
859+ req = webob.Request.blank('/images/detail?size_min=18&size_max=19')
860+ res = req.get_response(self.api)
861+ res_dict = json.loads(res.body)
862+ self.assertEquals(res.status_int, 200)
863+
864+ images = res_dict['images']
865+ self.assertEquals(len(images), 2)
866+
867+ for image in images:
868+ self.assertTrue(image['size'] <= 19 and image['size'] >= 18)
869+
870+ def test_get_details_filter_property(self):
871+ """Tests that the /images/detail registry API returns list of
872+ public images that have a specific custom property
873+
874+ """
875+ extra_fixture = {'id': 3,
876+ 'status': 'active',
877+ 'is_public': True,
878+ 'disk_format': 'vhd',
879+ 'container_format': 'ovf',
880+ 'name': 'fake image #3',
881+ 'size': 19,
882+ 'checksum': None,
883+ 'properties': {'prop_123': 'v a'}}
884+
885+ glance.registry.db.api.image_create(None, extra_fixture)
886+
887+ extra_fixture = {'id': 4,
888+ 'status': 'active',
889+ 'is_public': True,
890+ 'disk_format': 'ami',
891+ 'container_format': 'ami',
892+ 'name': 'fake image #4',
893+ 'size': 19,
894+ 'checksum': None,
895+ 'properties': {'prop_123': 'v b'}}
896+
897+ glance.registry.db.api.image_create(None, extra_fixture)
898+
899+ req = webob.Request.blank('/images/detail?property-prop_123=v%20a')
900+ res = req.get_response(self.api)
901+ res_dict = json.loads(res.body)
902+ self.assertEquals(res.status_int, 200)
903+
904+ images = res_dict['images']
905+ self.assertEquals(len(images), 1)
906+
907+ for image in images:
908+ self.assertEqual('v a', image['properties']['prop_123'])
909+
910 def test_create_image(self):
911 """Tests that the /images POST registry API creates the image"""
912 fixture = {'name': 'fake public image',
913
914=== modified file 'tests/unit/test_clients.py'
915--- tests/unit/test_clients.py 2011-05-05 23:12:21 +0000
916+++ tests/unit/test_clients.py 2011-05-17 13:32:06 +0000
917@@ -24,8 +24,9 @@
918 import webob
919
920 from glance import client
921+from glance.common import exception
922+import glance.registry.db.api
923 from glance.registry import client as rclient
924-from glance.common import exception
925 from tests import stubs
926
927
928@@ -69,6 +70,26 @@
929 for k, v in fixture.items():
930 self.assertEquals(v, images[0][k])
931
932+ def test_get_image_index_by_name(self):
933+ """Test correct set of public, name-filtered image returned. This
934+ is just a sanity check, we test the details call more in-depth."""
935+ extra_fixture = {'id': 3,
936+ 'status': 'active',
937+ 'is_public': True,
938+ 'disk_format': 'vhd',
939+ 'container_format': 'ovf',
940+ 'name': 'new name! #123',
941+ 'size': 19,
942+ 'checksum': None}
943+
944+ glance.registry.db.api.image_create(None, extra_fixture)
945+
946+ images = self.client.get_images({'name': 'new name! #123'})
947+ self.assertEquals(len(images), 1)
948+
949+ for image in images:
950+ self.assertEquals('new name! #123', image['name'])
951+
952 def test_get_image_details(self):
953 """Tests that the detailed info about public images returned"""
954 fixture = {'id': 2,
955@@ -87,6 +108,140 @@
956 for k, v in fixture.items():
957 self.assertEquals(v, images[0][k])
958
959+ def test_get_image_details_by_name(self):
960+ """Tests that a detailed call can be filtered by name"""
961+ extra_fixture = {'id': 3,
962+ 'status': 'active',
963+ 'is_public': True,
964+ 'disk_format': 'vhd',
965+ 'container_format': 'ovf',
966+ 'name': 'new name! #123',
967+ 'size': 19,
968+ 'checksum': None}
969+
970+ glance.registry.db.api.image_create(None, extra_fixture)
971+
972+ images = self.client.get_images_detailed({'name': 'new name! #123'})
973+ self.assertEquals(len(images), 1)
974+
975+ for image in images:
976+ self.assertEquals('new name! #123', image['name'])
977+
978+ def test_get_image_details_by_status(self):
979+ """Tests that a detailed call can be filtered by status"""
980+ extra_fixture = {'id': 3,
981+ 'status': 'saving',
982+ 'is_public': True,
983+ 'disk_format': 'vhd',
984+ 'container_format': 'ovf',
985+ 'name': 'new name! #123',
986+ 'size': 19,
987+ 'checksum': None}
988+
989+ glance.registry.db.api.image_create(None, extra_fixture)
990+
991+ images = self.client.get_images_detailed({'status': 'saving'})
992+ self.assertEquals(len(images), 1)
993+
994+ for image in images:
995+ self.assertEquals('saving', image['status'])
996+
997+ def test_get_image_details_by_container_format(self):
998+ """Tests that a detailed call can be filtered by container_format"""
999+ extra_fixture = {'id': 3,
1000+ 'status': 'saving',
1001+ 'is_public': True,
1002+ 'disk_format': 'vhd',
1003+ 'container_format': 'ovf',
1004+ 'name': 'new name! #123',
1005+ 'size': 19,
1006+ 'checksum': None}
1007+
1008+ glance.registry.db.api.image_create(None, extra_fixture)
1009+
1010+ images = self.client.get_images_detailed({'container_format': 'ovf'})
1011+ self.assertEquals(len(images), 2)
1012+
1013+ for image in images:
1014+ self.assertEquals('ovf', image['container_format'])
1015+
1016+ def test_get_image_details_by_disk_format(self):
1017+ """Tests that a detailed call can be filtered by disk_format"""
1018+ extra_fixture = {'id': 3,
1019+ 'status': 'saving',
1020+ 'is_public': True,
1021+ 'disk_format': 'vhd',
1022+ 'container_format': 'ovf',
1023+ 'name': 'new name! #123',
1024+ 'size': 19,
1025+ 'checksum': None}
1026+
1027+ glance.registry.db.api.image_create(None, extra_fixture)
1028+
1029+ images = self.client.get_images_detailed({'disk_format': 'vhd'})
1030+ self.assertEquals(len(images), 2)
1031+
1032+ for image in images:
1033+ self.assertEquals('vhd', image['disk_format'])
1034+
1035+ def test_get_image_details_with_maximum_size(self):
1036+ """Tests that a detailed call can be filtered by size_max"""
1037+ extra_fixture = {'id': 3,
1038+ 'status': 'saving',
1039+ 'is_public': True,
1040+ 'disk_format': 'vhd',
1041+ 'container_format': 'ovf',
1042+ 'name': 'new name! #123',
1043+ 'size': 21,
1044+ 'checksum': None}
1045+
1046+ glance.registry.db.api.image_create(None, extra_fixture)
1047+
1048+ images = self.client.get_images_detailed({'size_max': 20})
1049+ self.assertEquals(len(images), 1)
1050+
1051+ for image in images:
1052+ self.assertTrue(image['size'] <= 20)
1053+
1054+ def test_get_image_details_with_minimum_size(self):
1055+ """Tests that a detailed call can be filtered by size_min"""
1056+ extra_fixture = {'id': 3,
1057+ 'status': 'saving',
1058+ 'is_public': True,
1059+ 'disk_format': 'vhd',
1060+ 'container_format': 'ovf',
1061+ 'name': 'new name! #123',
1062+ 'size': 20,
1063+ 'checksum': None}
1064+
1065+ glance.registry.db.api.image_create(None, extra_fixture)
1066+
1067+ images = self.client.get_images_detailed({'size_min': 20})
1068+ self.assertEquals(len(images), 1)
1069+
1070+ for image in images:
1071+ self.assertTrue(image['size'] >= 20)
1072+
1073+ def test_get_image_details_by_property(self):
1074+ """Tests that a detailed call can be filtered by a property"""
1075+ extra_fixture = {'id': 3,
1076+ 'status': 'saving',
1077+ 'is_public': True,
1078+ 'disk_format': 'vhd',
1079+ 'container_format': 'ovf',
1080+ 'name': 'new name! #123',
1081+ 'size': 19,
1082+ 'checksum': None,
1083+ 'properties': {'p a': 'v a'}}
1084+
1085+ glance.registry.db.api.image_create(None, extra_fixture)
1086+
1087+ images = self.client.get_images_detailed({'property-p a': 'v a'})
1088+ self.assertEquals(len(images), 1)
1089+
1090+ for image in images:
1091+ self.assertEquals('v a', image['properties']['p a'])
1092+
1093 def test_get_image(self):
1094 """Tests that the detailed info about an image returned"""
1095 fixture = {'id': 1,

Subscribers

People subscribed via source and target branches