Merge lp:~jaypipes/glance/parallax-register-image into lp:~hudson-openstack/glance/trunk
- parallax-register-image
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Michael Gundlach |
Approved revision: | 15 |
Merged at revision: | 14 |
Proposed branch: | lp:~jaypipes/glance/parallax-register-image |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
515 lines (+310/-37) 8 files modified
glance/common/wsgi.py (+0/-1) glance/parallax/controllers.py (+58/-15) glance/parallax/db/sqlalchemy/api.py (+1/-1) glance/parallax/db/sqlalchemy/models.py (+17/-15) glance/teller/controllers.py (+1/-1) run_tests.sh (+4/-4) tests/stubs.py (+87/-0) tests/unit/test_parallax_api.py (+142/-0) |
To merge this branch: | bzr merge lp:~jaypipes/glance/parallax-register-image |
Related bugs: | |
Related blueprints: |
Implement Parallax's register-image API call
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Gundlach (community) | Approve | ||
Rick Harris (community) | Approve | ||
Review via email: mp+38258@code.launchpad.net |
Commit message
Description of the change
Implements Parallax API call to register a new image
* Adds unit test for Parallax API controller
* Adds stubouts for glance.
regarding images
* Adds unittest for bad status on image creation
* Adds --logging-
prevent extra output in running tests when tests complete successfully
Jay Pipes (jaypipes) wrote : | # |
> Why does ImageController
lol. :) Leftover from when I had some debugging statements in there. ::blush::
> 57: should be
> if 'status' not in image_data:
> or possibly 56/57/58 could be replaced with
> image_data.
Ah, nice. Thanks.
> 101: i assume you meant to remove support for GET /images/detail.
Yes, that does not comply with the Atom publishing protocol. No need for it AFAICT.
> Otherwise, lgtm.
Thanks! I'll push those little fixes later today :)
Michael Gundlach (gundlach) wrote : | # |
On Tue, Oct 12, 2010 at 4:36 PM, Jay Pipes <email address hidden> wrote:
> > 101: i assume you meant to remove support for GET /images/detail.
>
> Yes, that does not comply with the Atom publishing protocol. No need for
> it AFAICT.
>
We need it in order to comply as much as possible with the current Rackspace
API. GET /images should just return id and name, while /images/detail
returns all other available data. (same for GET /images/4 and GET
/images/4/detail .)
I think our goal was to conform to the RS API, not to conform to the Atom
protocol. Then again, we're planning to iterate on the API shortly after
Austin, so I'm not positive what this buys us.
cc'ing a couple of people from whom I've heard the "conform to the RS API"
message, to get their second opinions.
Michael
Rick Clark (dendrobates) wrote : | # |
On 10/13/2010 11:00 AM, Michael Gundlach wrote:
> I think our goal was to conform to the RS API, not to conform to the Atom
> protocol. Then again, we're planning to iterate on the API shortly after
> Austin, so I'm not positive what this buys us.
We need to conform as much as possible to the RS API until they release
1.1 and hand off API development to us. At that point we can
diverge/fix in future versions.
I am not commenting on the technical merits of this discussion though.
> cc'ing a couple of people from whom I've heard the "conform to the RS API"
> message, to get their second opinions.
>
> Michael
>
Rick Harris (rconradharris) wrote : | # |
++ on gundlach's cleanup suggestions, otherwise, lgtm.
Jay Pipes (jaypipes) wrote : | # |
cool, understood as far as RS API. fix coming shortly.
Jay Pipes (jaypipes) wrote : | # |
Alrighty, added the /images/detail call, a unit test for it, and gundlach's cleanups. Please mark the merge prop Approved if all meets expectations. :)
Preview Diff
1 | === modified file 'glance/common/wsgi.py' |
2 | --- glance/common/wsgi.py 2010-10-01 23:07:46 +0000 |
3 | +++ glance/common/wsgi.py 2010-10-13 18:50:33 +0000 |
4 | @@ -224,7 +224,6 @@ |
5 | Uses self._serialization_metadata if it exists, which is a dict mapping |
6 | MIME types to information needed to serialize to that type. |
7 | """ |
8 | - # FIXME(sirp): type(self) should just be `self` |
9 | _metadata = getattr(type(self), "_serialization_metadata", {}) |
10 | serializer = Serializer(request.environ, _metadata) |
11 | return serializer.to_content_type(data) |
12 | |
13 | === modified file 'glance/parallax/controllers.py' |
14 | --- glance/parallax/controllers.py 2010-10-11 18:28:35 +0000 |
15 | +++ glance/parallax/controllers.py 2010-10-13 18:50:33 +0000 |
16 | @@ -18,26 +18,52 @@ |
17 | Parllax Image controller |
18 | """ |
19 | |
20 | +import json |
21 | import routes |
22 | +from webob import exc |
23 | + |
24 | from glance.common import wsgi |
25 | from glance.common import exception |
26 | from glance.parallax import db |
27 | -from webob import exc |
28 | |
29 | |
30 | class ImageController(wsgi.Controller): |
31 | + |
32 | """Image Controller """ |
33 | |
34 | def index(self, req): |
35 | - """Return data for all public, non-deleted images """ |
36 | + """Return basic information for all public, non-deleted images |
37 | + |
38 | + :param req: the Request object coming from the wsgi layer |
39 | + :retval a mapping of the following form:: |
40 | + |
41 | + dict(images=[image_list]) |
42 | + |
43 | + Where image_list is a sequence of mappings:: |
44 | + |
45 | + {'id': image_id, 'name': image_name} |
46 | + |
47 | + """ |
48 | + images = db.image_get_all_public(None) |
49 | + image_dicts = [dict(id=i['id'], name=i['name']) for i in images] |
50 | + return dict(images=image_dicts) |
51 | + |
52 | + def detail(self, req): |
53 | + """Return detailed information for all public, non-deleted images |
54 | + |
55 | + :param req: the Request object coming from the wsgi layer |
56 | + :retval a mapping of the following form:: |
57 | + |
58 | + dict(images=[image_list]) |
59 | + |
60 | + Where image_list is a sequence of mappings containing |
61 | + all image model fields. |
62 | + |
63 | + """ |
64 | images = db.image_get_all_public(None) |
65 | image_dicts = [self._make_image_dict(i) for i in images] |
66 | return dict(images=image_dicts) |
67 | |
68 | - def detail(self, req): |
69 | - """Detail is not currently supported """ |
70 | - raise exc.HTTPNotImplemented() |
71 | - |
72 | def show(self, req, id): |
73 | """Return data about the given image id.""" |
74 | try: |
75 | @@ -52,8 +78,24 @@ |
76 | raise exc.HTTPNotImplemented() |
77 | |
78 | def create(self, req): |
79 | - """Create is not currently supported """ |
80 | - raise exc.HTTPNotImplemented() |
81 | + """Registers a new image with the registry. |
82 | + |
83 | + :param req: Request body. A JSON-ified dict of information about |
84 | + the image. |
85 | + |
86 | + :retval Returns the newly-created image information as a mapping, |
87 | + which will include the newly-created image's internal id |
88 | + in the 'id' field |
89 | + |
90 | + """ |
91 | + image_data = json.loads(req.body) |
92 | + |
93 | + # Ensure the image has a status set |
94 | + image_data.setdefault('status', 'available') |
95 | + |
96 | + context = None |
97 | + new_image = db.image_create(context, image_data) |
98 | + return dict(new_image) |
99 | |
100 | def update(self, req, id): |
101 | """Update is not currently supported """ |
102 | @@ -64,23 +106,23 @@ |
103 | """ Create a dict represenation of an image which we can use to |
104 | serialize the image. |
105 | """ |
106 | - def _fetch_attrs(obj, attrs): |
107 | - return dict([(a, getattr(obj, a)) for a in attrs]) |
108 | + def _fetch_attrs(d, attrs): |
109 | + return dict([(a, d[a]) for a in attrs]) |
110 | |
111 | # attributes common to all models |
112 | base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at', |
113 | 'deleted']) |
114 | |
115 | file_attrs = base_attrs | set(['location', 'size']) |
116 | - files = [_fetch_attrs(f, file_attrs) for f in image.files] |
117 | + files = [_fetch_attrs(f, file_attrs) for f in image['files']] |
118 | |
119 | # TODO(sirp): should this be a dict, or a list of dicts? |
120 | # A plain dict is more convenient, but list of dicts would provide |
121 | # access to created_at, etc |
122 | - metadata = dict((m.key, m.value) for m in image.metadata |
123 | - if not m.deleted) |
124 | + metadata = dict((m['key'], m['value']) for m in image['metadata'] |
125 | + if not m['deleted']) |
126 | |
127 | - image_attrs = base_attrs | set(['name', 'image_type', 'state', 'public']) |
128 | + image_attrs = base_attrs | set(['name', 'image_type', 'status', 'is_public']) |
129 | image_dict = _fetch_attrs(image, image_attrs) |
130 | |
131 | image_dict['files'] = files |
132 | @@ -95,5 +137,6 @@ |
133 | # TODO(sirp): should we add back the middleware for parallax? |
134 | mapper = routes.Mapper() |
135 | mapper.resource("image", "images", controller=ImageController(), |
136 | - collection={'detail': 'GET'}) |
137 | + collection={'detail': 'GET'}) |
138 | + mapper.connect("/", controller=ImageController(), action="index") |
139 | super(API, self).__init__(mapper) |
140 | |
141 | === modified file 'glance/parallax/db/sqlalchemy/api.py' |
142 | --- glance/parallax/db/sqlalchemy/api.py 2010-10-01 20:27:48 +0000 |
143 | +++ glance/parallax/db/sqlalchemy/api.py 2010-10-13 18:50:33 +0000 |
144 | @@ -95,7 +95,7 @@ |
145 | ).options(joinedload(models.Image.files) |
146 | ).options(joinedload(models.Image.metadata) |
147 | ).filter_by(deleted=_deleted(context) |
148 | - ).filter_by(public=public |
149 | + ).filter_by(is_public=public |
150 | ).all() |
151 | |
152 | |
153 | |
154 | === modified file 'glance/parallax/db/sqlalchemy/models.py' |
155 | --- glance/parallax/db/sqlalchemy/models.py 2010-10-01 19:51:29 +0000 |
156 | +++ glance/parallax/db/sqlalchemy/models.py 2010-10-13 18:50:33 +0000 |
157 | @@ -23,16 +23,13 @@ |
158 | import sys |
159 | import datetime |
160 | |
161 | -# TODO(vish): clean up these imports |
162 | -from sqlalchemy.orm import relationship, backref, exc, object_mapper |
163 | +from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates |
164 | from sqlalchemy import Column, Integer, String |
165 | from sqlalchemy import ForeignKey, DateTime, Boolean, Text |
166 | from sqlalchemy.ext.declarative import declarative_base |
167 | |
168 | from glance.common.db.sqlalchemy.session import get_session |
169 | |
170 | -# FIXME(sirp): confirm this is not needed |
171 | -#from common import auth |
172 | from glance.common import exception |
173 | from glance.common import flags |
174 | |
175 | @@ -40,6 +37,7 @@ |
176 | |
177 | BASE = declarative_base() |
178 | |
179 | + |
180 | #TODO(sirp): ModelBase should be moved out so Glance and Nova can share it |
181 | class ModelBase(object): |
182 | """Base class for Nova and Glance Models""" |
183 | @@ -128,18 +126,22 @@ |
184 | |
185 | id = Column(Integer, primary_key=True) |
186 | name = Column(String(255)) |
187 | - image_type = Column(String(255)) |
188 | - state = Column(String(255)) |
189 | - public = Column(Boolean, default=False) |
190 | + image_type = Column(String(30)) |
191 | + status = Column(String(30)) |
192 | + is_public = Column(Boolean, default=False) |
193 | |
194 | - #@validates('image_type') |
195 | - #def validate_image_type(self, key, image_type): |
196 | - # assert(image_type in ('machine', 'kernel', 'ramdisk', 'raw')) |
197 | - # |
198 | - #@validates('state') |
199 | - #def validate_state(self, key, state): |
200 | - # assert(state in ('available', 'pending', 'disabled')) |
201 | - # |
202 | + @validates('image_type') |
203 | + def validate_image_type(self, key, image_type): |
204 | + if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'): |
205 | + raise exception.Invalid("Invalid image type '%s' for image." % image_type) |
206 | + return image_type |
207 | + |
208 | + @validates('status') |
209 | + def validate_status(self, key, state): |
210 | + if not state in ('available', 'pending', 'disabled'): |
211 | + raise exception.Invalid("Invalid status '%s' for image." % status) |
212 | + return image_type |
213 | + |
214 | # TODO(sirp): should these be stored as metadata? |
215 | #user_id = Column(String(255)) |
216 | #project_id = Column(String(255)) |
217 | |
218 | === modified file 'glance/teller/controllers.py' |
219 | --- glance/teller/controllers.py 2010-10-11 18:28:35 +0000 |
220 | +++ glance/teller/controllers.py 2010-10-13 18:50:33 +0000 |
221 | @@ -95,7 +95,7 @@ |
222 | |
223 | |
224 | class API(wsgi.Router): |
225 | - """WSGI entry point for all Parallax requests.""" |
226 | + """WSGI entry point for all Teller requests.""" |
227 | |
228 | def __init__(self): |
229 | mapper = routes.Mapper() |
230 | |
231 | === modified file 'run_tests.sh' |
232 | --- run_tests.sh 2010-10-08 20:42:13 +0000 |
233 | +++ run_tests.sh 2010-10-13 18:50:33 +0000 |
234 | @@ -41,12 +41,12 @@ |
235 | |
236 | if [ $never_venv -eq 1 ]; then |
237 | # Just run the test suites in current environment |
238 | - python run_tests.py |
239 | + nosetests --logging-clear-handlers |
240 | exit |
241 | fi |
242 | |
243 | if [ -e ${venv} ]; then |
244 | - ${with_venv} nosetests |
245 | + ${with_venv} nosetests --logging-clear-handlers |
246 | else |
247 | if [ $always_venv -eq 1 ]; then |
248 | # Automatically install the virtualenv |
249 | @@ -58,9 +58,9 @@ |
250 | # Install the virtualenv and run the test suite in it |
251 | python tools/install_venv.py |
252 | else |
253 | - nosetests |
254 | + nosetests --logging-clear-handlers |
255 | exit |
256 | fi |
257 | fi |
258 | - ${with_venv} nosetests |
259 | + ${with_venv} nosetests --logging-clear-handlers |
260 | fi |
261 | |
262 | === modified file 'tests/stubs.py' |
263 | --- tests/stubs.py 2010-10-11 18:28:35 +0000 |
264 | +++ tests/stubs.py 2010-10-13 18:50:33 +0000 |
265 | @@ -17,12 +17,15 @@ |
266 | |
267 | """Stubouts, mocks and fixtures for the test suite""" |
268 | |
269 | +import datetime |
270 | import httplib |
271 | import StringIO |
272 | |
273 | import stubout |
274 | |
275 | +from glance.common import exception |
276 | import glance.teller.backends.swift |
277 | +import glance.parallax.db.sqlalchemy.api |
278 | |
279 | def stub_out_http_backend(stubs): |
280 | """Stubs out the httplib.HTTPRequest.getresponse to return |
281 | @@ -147,3 +150,87 @@ |
282 | fake_parallax_registry = FakeParallax() |
283 | stubs.Set(glance.teller.registries.Parallax, 'lookup', |
284 | fake_parallax_registry.lookup) |
285 | + |
286 | + |
287 | +def stub_out_parallax_db_image_api(stubs): |
288 | + """Stubs out the database set/fetch API calls for Parallax |
289 | + so the calls are routed to an in-memory dict. This helps us |
290 | + avoid having to manually clear or flush the SQLite database. |
291 | + |
292 | + The "datastore" always starts with this set of image fixtures. |
293 | + |
294 | + :param stubs: Set of stubout stubs |
295 | + |
296 | + """ |
297 | + class FakeDatastore(object): |
298 | + |
299 | + FIXTURES = [ |
300 | + {'id': 1, |
301 | + 'name': 'fake image #1', |
302 | + 'status': 'available', |
303 | + 'image_type': 'kernel', |
304 | + 'is_public': False, |
305 | + 'created_at': datetime.datetime.utcnow(), |
306 | + 'updated_at': datetime.datetime.utcnow(), |
307 | + 'deleted_at': None, |
308 | + 'deleted': False, |
309 | + 'files': [], |
310 | + 'metadata': []}, |
311 | + {'id': 2, |
312 | + 'name': 'fake image #2', |
313 | + 'status': 'available', |
314 | + 'image_type': 'kernel', |
315 | + 'is_public': True, |
316 | + 'created_at': datetime.datetime.utcnow(), |
317 | + 'updated_at': datetime.datetime.utcnow(), |
318 | + 'deleted_at': None, |
319 | + 'deleted': False, |
320 | + 'files': [], |
321 | + 'metadata': []}] |
322 | + |
323 | + VALID_STATUSES = ('available', 'disabled', 'pending') |
324 | + |
325 | + def __init__(self): |
326 | + self.images = self.FIXTURES |
327 | + self.next_id = 3 |
328 | + |
329 | + def image_create(self, _context, values): |
330 | + values['id'] = self.next_id |
331 | + |
332 | + if 'status' not in values.keys(): |
333 | + values['status'] = 'available' |
334 | + else: |
335 | + if not values['status'] in self.VALID_STATUSES: |
336 | + raise exception.Invalid("Invalid status '%s' for image" % values['status']) |
337 | + |
338 | + self.next_id += 1 |
339 | + self.images.extend(values) |
340 | + return values |
341 | + |
342 | + def image_destroy(self, _context, image_id): |
343 | + try: |
344 | + del self.images[image_id] |
345 | + except KeyError: |
346 | + new_exc = exception.NotFound("No model for id %s" % image_id) |
347 | + raise new_exc.__class__, new_exc, sys.exc_info()[2] |
348 | + |
349 | + def image_get(self, _context, image_id): |
350 | + if image_id not in self.images.keys() or self.images[image_id]['deleted']: |
351 | + new_exc = exception.NotFound("No model for id %s" % image_id) |
352 | + raise new_exc.__class__, new_exc, sys.exc_info()[2] |
353 | + else: |
354 | + return self.images[image_id] |
355 | + |
356 | + def image_get_all_public(self, _context, public): |
357 | + return [f for f in self.images |
358 | + if f['is_public'] == public] |
359 | + |
360 | + fake_datastore = FakeDatastore() |
361 | + stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_create', |
362 | + fake_datastore.image_create) |
363 | + stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_destroy', |
364 | + fake_datastore.image_destroy) |
365 | + stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get', |
366 | + fake_datastore.image_get) |
367 | + stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get_all_public', |
368 | + fake_datastore.image_get_all_public) |
369 | |
370 | === added file 'tests/unit/test_parallax_api.py' |
371 | --- tests/unit/test_parallax_api.py 1970-01-01 00:00:00 +0000 |
372 | +++ tests/unit/test_parallax_api.py 2010-10-13 18:50:33 +0000 |
373 | @@ -0,0 +1,142 @@ |
374 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
375 | + |
376 | +# Copyright 2010 OpenStack, LLC |
377 | +# All Rights Reserved. |
378 | +# |
379 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
380 | +# not use this file except in compliance with the License. You may obtain |
381 | +# a copy of the License at |
382 | +# |
383 | +# http://www.apache.org/licenses/LICENSE-2.0 |
384 | +# |
385 | +# Unless required by applicable law or agreed to in writing, software |
386 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
387 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
388 | +# License for the specific language governing permissions and limitations |
389 | +# under the License. |
390 | + |
391 | +import json |
392 | +import stubout |
393 | +import unittest |
394 | +import webob |
395 | + |
396 | +from glance.common import exception |
397 | +from glance.parallax import controllers |
398 | +from glance.parallax import db |
399 | +from tests import stubs |
400 | + |
401 | + |
402 | +class TestImageController(unittest.TestCase): |
403 | + def setUp(self): |
404 | + """Establish a clean test environment""" |
405 | + self.stubs = stubout.StubOutForTesting() |
406 | + self.image_controller = controllers.ImageController() |
407 | + stubs.stub_out_parallax_db_image_api(self.stubs) |
408 | + |
409 | + def tearDown(self): |
410 | + """Clear the test environment""" |
411 | + self.stubs.UnsetAll() |
412 | + |
413 | + def test_get_root(self): |
414 | + """Tests that the root parallax API returns "index", |
415 | + which is a list of public images |
416 | + |
417 | + """ |
418 | + fixture = {'id': 2, |
419 | + 'name': 'fake image #2'} |
420 | + req = webob.Request.blank('/') |
421 | + res = req.get_response(controllers.API()) |
422 | + res_dict = json.loads(res.body) |
423 | + self.assertEquals(res.status_int, 200) |
424 | + |
425 | + images = res_dict['images'] |
426 | + self.assertEquals(len(images), 1) |
427 | + |
428 | + for k,v in fixture.iteritems(): |
429 | + self.assertEquals(v, images[0][k]) |
430 | + |
431 | + def test_get_index(self): |
432 | + """Tests that the /images parallax API returns list of |
433 | + public images |
434 | + |
435 | + """ |
436 | + fixture = {'id': 2, |
437 | + 'name': 'fake image #2'} |
438 | + req = webob.Request.blank('/images') |
439 | + res = req.get_response(controllers.API()) |
440 | + res_dict = json.loads(res.body) |
441 | + self.assertEquals(res.status_int, 200) |
442 | + |
443 | + images = res_dict['images'] |
444 | + self.assertEquals(len(images), 1) |
445 | + |
446 | + for k,v in fixture.iteritems(): |
447 | + self.assertEquals(v, images[0][k]) |
448 | + |
449 | + def test_get_details(self): |
450 | + """Tests that the /images/detail parallax API returns |
451 | + a mapping containing a list of detailed image information |
452 | + |
453 | + """ |
454 | + fixture = {'id': 2, |
455 | + 'name': 'fake image #2', |
456 | + 'is_public': True, |
457 | + 'image_type': 'kernel', |
458 | + 'status': 'available' |
459 | + } |
460 | + req = webob.Request.blank('/images/detail') |
461 | + res = req.get_response(controllers.API()) |
462 | + res_dict = json.loads(res.body) |
463 | + self.assertEquals(res.status_int, 200) |
464 | + |
465 | + images = res_dict['images'] |
466 | + self.assertEquals(len(images), 1) |
467 | + |
468 | + for k,v in fixture.iteritems(): |
469 | + self.assertEquals(v, images[0][k]) |
470 | + |
471 | + def test_create_image(self): |
472 | + """Tests that the /images POST parallax API creates the image""" |
473 | + fixture = {'name': 'fake public image', |
474 | + 'is_public': True, |
475 | + 'image_type': 'kernel' |
476 | + } |
477 | + |
478 | + req = webob.Request.blank('/images') |
479 | + |
480 | + req.method = 'POST' |
481 | + req.body = json.dumps(fixture) |
482 | + |
483 | + res = req.get_response(controllers.API()) |
484 | + |
485 | + self.assertEquals(res.status_int, 200) |
486 | + |
487 | + res_dict = json.loads(res.body) |
488 | + |
489 | + for k,v in fixture.iteritems(): |
490 | + self.assertEquals(v, res_dict[k]) |
491 | + |
492 | + # Test ID auto-assigned properly |
493 | + self.assertEquals(3, res_dict['id']) |
494 | + |
495 | + # Test status was updated properly |
496 | + self.assertEquals('available', res_dict['status']) |
497 | + |
498 | + def test_create_image_with_bad_status(self): |
499 | + """Tests proper exception is raised if a bad status is set""" |
500 | + fixture = {'id': 3, |
501 | + 'name': 'fake public image', |
502 | + 'is_public': True, |
503 | + 'image_type': 'kernel', |
504 | + 'status': 'bad status' |
505 | + } |
506 | + |
507 | + req = webob.Request.blank('/images') |
508 | + |
509 | + req.method = 'POST' |
510 | + req.body = json.dumps(fixture) |
511 | + |
512 | + # TODO(jaypipes): Port Nova's Fault infrastructure |
513 | + # over to Glance to support exception catching into |
514 | + # standard HTTP errors. |
515 | + self.assertRaises(exception.Invalid, req.get_response, controllers.API()) |
Why does ImageController .__init_ _ exist?
57: should be data.setdefault ('status' , 'available')
if 'status' not in image_data:
or possibly 56/57/58 could be replaced with
image_
101: i assume you meant to remove support for GET /images/detail.
Otherwise, lgtm.