Merge lp:~rackspace-titan/glance/api-results-filtering into lp:~hudson-openstack/glance/trunk
- api-results-filtering
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Dan Prince (community) | Approve | ||
Review via email: mp+61056@code.launchpad.net |
Commit message
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)
Brian Waldon (bcwaldon) wrote : | # |
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(
175 + _k = k[9:]
176 + images = filter(
I should really test this before suggesting it, but:
for (k, v) in filters.items():
if k.startswith(
_k = k[9:]
query.
models.
models.
Also maybe
for (k, v) in filters.items():
if k.startswith(
_k = k[9:]
query.
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
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.
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-
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/
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-
>
> 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-
> Also a minor nit with the import statements in glance/
> 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
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.
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.
Dan Prince (dan-prince) wrote : | # |
Sure. I no confusion. Just consistency concerns. Thanks for the clarification. I'm good with this. Good work.
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
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
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
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, |
I could use some help in registry/db/api.py with the sqlalchemy code. See the TODO