Merge lp:~rconradharris/glance/allow_upload_on_put into lp:~hudson-openstack/glance/trunk
- allow_upload_on_put
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 39 |
Merged at revision: | 35 |
Proposed branch: | lp:~rconradharris/glance/allow_upload_on_put |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
870 lines (+242/-177) 16 files modified
doc/source/client.rst (+4/-0) doc/source/glanceapi.rst (+16/-0) glance/client.py (+20/-17) glance/common/utils.py (+1/-2) glance/registry/client.py (+6/-2) glance/registry/db/sqlalchemy/api.py (+17/-15) glance/registry/db/sqlalchemy/models.py (+11/-11) glance/registry/server.py (+1/-1) glance/server.py (+110/-70) glance/store/__init__.py (+4/-6) glance/store/backends/__init__.py (+4/-6) glance/store/filesystem.py (+0/-1) tests/stubs.py (+4/-4) tests/unit/test_api.py (+8/-4) tests/unit/test_clients.py (+34/-36) tests/unit/test_registry_api.py (+2/-2) |
To merge this branch: | bzr merge lp:~rconradharris/glance/allow_upload_on_put |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Review via email: mp+45492@code.launchpad.net |
Commit message
Description of the change
This merge is in conjunction with lp:~rconradharris/nova/xs-snap-return-image-id-before-snapshot
The patch does the following:
* Image Create (POST) is broken up into 3 steps (reserve, upload and activate); reserve is used to allow the OpenStack API to return metadata about an image where the data has not been uploaded yet
* Image Update (PUT) now takes image data as the request body and metadata as headers (just like POST); state is enforced so that image data can only be uploaded once.
* Image statuses were changed to match the OpenStack API (queued, saving, active, killed); NOTE: preparing is not used
* update_image and add_image client calls now return metadata instead of just the id
Rick Harris (rconradharris) wrote : | # |
> The above will read an entire image into the exception when the exception is raised during
> add_image()
This is reading the response body (not the request body). In the case of an exception, a non-200 OK, the response body presumably--and in the cases I tested-- contain a helpful error message.
I can't think, off hand, of a case when we'd have an exception AND return the full-image data.
> Please do also update the docs in docs/source/ to match
Good call, will do.
- 38. By Rick Harris
-
Merging trunk
- 39. By Rick Harris
-
Updating docs
Rick Harris (rconradharris) wrote : | # |
Made some small tweaks to the docs.
Jay Pipes (jaypipes) wrote : | # |
w00t.
FYI, the only reason I raised the flag about returning res.read() is because I originally had res.read() in there and during review it was requested that I remove it. ;)
-jay
Preview Diff
1 | === modified file 'doc/source/client.rst' |
2 | --- doc/source/client.rst 2010-12-23 17:31:47 +0000 |
3 | +++ doc/source/client.rst 2011-01-11 17:43:19 +0000 |
4 | @@ -133,6 +133,10 @@ |
5 | The `image_meta` argument is a mapping containing various image metadata. The |
6 | `image_data` argument is the disk image data. |
7 | |
8 | +If the data is not yet available, but you would like to reserve a slot in |
9 | +Glance to hold the image, you can create a 'queued' by omitting the |
10 | +`image_data` parameter. |
11 | + |
12 | The list of metadata that `image_meta` can contain are listed below. |
13 | |
14 | * `name` |
15 | |
16 | === modified file 'doc/source/glanceapi.rst' |
17 | --- doc/source/glanceapi.rst 2010-12-23 17:31:47 +0000 |
18 | +++ doc/source/glanceapi.rst 2011-01-11 17:43:19 +0000 |
19 | @@ -292,3 +292,19 @@ |
20 | be attached to the image. However, keep in mind that the 8K limit on the |
21 | size of all HTTP headers sent in a request will effectively limit the number |
22 | of image properties. |
23 | + |
24 | + |
25 | +Updating an Image |
26 | +***************** |
27 | + |
28 | +Glance will view as image metadata any HTTP header that it receives in a |
29 | +`PUT` request where the header key is prefixed with the strings |
30 | +`x-image-meta-` and `x-image-meta-property-`. |
31 | + |
32 | +If an image was previously reserved, and thus is in the `queued` state, then |
33 | +image data can be added by including it as the request body. If the image |
34 | +already as data associated with it (e.g. not in the `queued` state), then |
35 | +including a request body will result in a `409 Conflict` exception. |
36 | + |
37 | +On success, the `PUT` request will return the image metadata encoded as `HTTP` |
38 | +headers. |
39 | |
40 | === modified file 'glance/client.py' |
41 | --- glance/client.py 2011-01-04 20:32:07 +0000 |
42 | +++ glance/client.py 2011-01-11 17:43:19 +0000 |
43 | @@ -120,11 +120,11 @@ |
44 | elif status_code == httplib.NOT_FOUND: |
45 | raise exception.NotFound |
46 | elif status_code == httplib.CONFLICT: |
47 | - raise exception.Duplicate |
48 | + raise exception.Duplicate(res.read()) |
49 | elif status_code == httplib.BAD_REQUEST: |
50 | - raise exception.BadInputError |
51 | + raise exception.BadInputError(res.read()) |
52 | else: |
53 | - raise Exception("Unknown error occurred! %d" % status_code) |
54 | + raise Exception("Unknown error occurred! %s" % res.__dict__) |
55 | |
56 | except (socket.error, IOError), e: |
57 | raise ClientConnectionError("Unable to connect to " |
58 | @@ -203,12 +203,12 @@ |
59 | image = util.get_image_meta_from_headers(res) |
60 | return image |
61 | |
62 | - def add_image(self, image_meta, image_data=None): |
63 | + def add_image(self, image_meta=None, image_data=None): |
64 | """ |
65 | Tells Glance about an image's metadata as well |
66 | as optionally the image_data itself |
67 | |
68 | - :param image_meta: Mapping of information about the |
69 | + :param image_meta: Optional Mapping of information about the |
70 | image |
71 | :param image_data: Optional string of raw image data |
72 | or file-like object that can be |
73 | @@ -216,11 +216,9 @@ |
74 | |
75 | :retval The newly-stored image's metadata. |
76 | """ |
77 | - if not image_data and 'location' not in image_meta.keys(): |
78 | - raise exception.Invalid("You must either specify a location " |
79 | - "for the image or supply the actual " |
80 | - "image data when adding an image to " |
81 | - "Glance") |
82 | + if image_meta is None: |
83 | + image_meta = {} |
84 | + |
85 | if image_data: |
86 | if hasattr(image_data, 'read'): |
87 | # TODO(jaypipes): This is far from efficient. Implement |
88 | @@ -242,17 +240,22 @@ |
89 | |
90 | res = self.do_request("POST", "/images", body, headers) |
91 | data = json.loads(res.read()) |
92 | - return data['image']['id'] |
93 | + return data['image'] |
94 | |
95 | - def update_image(self, image_id, image_metadata): |
96 | + def update_image(self, image_id, image_meta=None, image_data=None): |
97 | """ |
98 | Updates Glance's information about an image |
99 | """ |
100 | - if 'image' not in image_metadata.keys(): |
101 | - image_metadata = dict(image=image_metadata) |
102 | - body = json.dumps(image_metadata) |
103 | - self.do_request("PUT", "/images/%s" % image_id, body) |
104 | - return True |
105 | + if image_meta: |
106 | + if 'image' not in image_meta: |
107 | + image_meta = dict(image=image_meta) |
108 | + |
109 | + headers = util.image_meta_to_http_headers(image_meta or {}) |
110 | + |
111 | + body = image_data |
112 | + res = self.do_request("PUT", "/images/%s" % image_id, body, headers) |
113 | + data = json.loads(res.read()) |
114 | + return data['image'] |
115 | |
116 | def delete_image(self, image_id): |
117 | """ |
118 | |
119 | === modified file 'glance/common/utils.py' |
120 | --- glance/common/utils.py 2011-01-02 02:47:59 +0000 |
121 | +++ glance/common/utils.py 2011-01-11 17:43:19 +0000 |
122 | @@ -141,8 +141,7 @@ |
123 | |
124 | def generate_mac(): |
125 | mac = [0x02, 0x16, 0x3e, random.randint(0x00, 0x7f), |
126 | - random.randint(0x00, 0xff), random.randint(0x00, 0xff) |
127 | - ] |
128 | + random.randint(0x00, 0xff), random.randint(0x00, 0xff)] |
129 | return ':'.join(map(lambda x: "%02x" % x, mac)) |
130 | |
131 | |
132 | |
133 | === modified file 'glance/registry/client.py' |
134 | --- glance/registry/client.py 2011-01-02 02:26:58 +0000 |
135 | +++ glance/registry/client.py 2011-01-11 17:43:19 +0000 |
136 | @@ -93,9 +93,13 @@ |
137 | """ |
138 | if 'image' not in image_metadata.keys(): |
139 | image_metadata = dict(image=image_metadata) |
140 | + |
141 | body = json.dumps(image_metadata) |
142 | - self.do_request("PUT", "/images/%s" % image_id, body) |
143 | - return True |
144 | + |
145 | + res = self.do_request("PUT", "/images/%s" % image_id, body) |
146 | + data = json.loads(res.read()) |
147 | + image = data['image'] |
148 | + return image |
149 | |
150 | def delete_image(self, image_id): |
151 | """ |
152 | |
153 | === modified file 'glance/registry/db/sqlalchemy/api.py' |
154 | --- glance/registry/db/sqlalchemy/api.py 2011-01-02 02:47:59 +0000 |
155 | +++ glance/registry/db/sqlalchemy/api.py 2011-01-11 17:43:19 +0000 |
156 | @@ -65,11 +65,11 @@ |
157 | def image_get(context, image_id): |
158 | session = get_session() |
159 | try: |
160 | - return session.query(models.Image |
161 | - ).options(joinedload(models.Image.properties) |
162 | - ).filter_by(deleted=_deleted(context) |
163 | - ).filter_by(id=image_id |
164 | - ).one() |
165 | + return session.query(models.Image).\ |
166 | + options(joinedload(models.Image.properties)).\ |
167 | + filter_by(deleted=_deleted(context)).\ |
168 | + filter_by(id=image_id).\ |
169 | + one() |
170 | except exc.NoResultFound: |
171 | new_exc = exception.NotFound("No model for id %s" % image_id) |
172 | raise new_exc.__class__, new_exc, sys.exc_info()[2] |
173 | @@ -77,19 +77,19 @@ |
174 | |
175 | def image_get_all(context): |
176 | session = get_session() |
177 | - return session.query(models.Image |
178 | - ).options(joinedload(models.Image.properties) |
179 | - ).filter_by(deleted=_deleted(context) |
180 | - ).all() |
181 | + return session.query(models.Image).\ |
182 | + options(joinedload(models.Image.properties)).\ |
183 | + filter_by(deleted=_deleted(context)).\ |
184 | + all() |
185 | |
186 | |
187 | def image_get_all_public(context, public): |
188 | session = get_session() |
189 | - return session.query(models.Image |
190 | - ).options(joinedload(models.Image.properties) |
191 | - ).filter_by(deleted=_deleted(context) |
192 | - ).filter_by(is_public=public |
193 | - ).all() |
194 | + return session.query(models.Image).\ |
195 | + options(joinedload(models.Image.properties)).\ |
196 | + filter_by(deleted=_deleted(context)).\ |
197 | + filter_by(is_public=public).\ |
198 | + all() |
199 | |
200 | |
201 | def image_get_by_str(context, str_id): |
202 | @@ -129,7 +129,9 @@ |
203 | with session.begin(): |
204 | _drop_protected_attrs(models.Image, values) |
205 | |
206 | - values['size'] = int(values['size']) |
207 | + if 'size' in values: |
208 | + values['size'] = int(values['size']) |
209 | + |
210 | values['is_public'] = bool(values.get('is_public', False)) |
211 | properties = values.pop('properties', {}) |
212 | |
213 | |
214 | === modified file 'glance/registry/db/sqlalchemy/models.py' |
215 | --- glance/registry/db/sqlalchemy/models.py 2011-01-02 02:47:59 +0000 |
216 | +++ glance/registry/db/sqlalchemy/models.py 2011-01-11 17:43:19 +0000 |
217 | @@ -58,18 +58,18 @@ |
218 | """Get all objects of this type""" |
219 | if not session: |
220 | session = get_session() |
221 | - return session.query(cls |
222 | - ).filter_by(deleted=deleted |
223 | - ).all() |
224 | + return session.query(cls).\ |
225 | + filter_by(deleted=deleted).\ |
226 | + all() |
227 | |
228 | @classmethod |
229 | def count(cls, session=None, deleted=False): |
230 | """Count objects of this type""" |
231 | if not session: |
232 | session = get_session() |
233 | - return session.query(cls |
234 | - ).filter_by(deleted=deleted |
235 | - ).count() |
236 | + return session.query(cls).\ |
237 | + filter_by(deleted=deleted).\ |
238 | + count() |
239 | |
240 | @classmethod |
241 | def find(cls, obj_id, session=None, deleted=False): |
242 | @@ -77,10 +77,10 @@ |
243 | if not session: |
244 | session = get_session() |
245 | try: |
246 | - return session.query(cls |
247 | - ).filter_by(id=obj_id |
248 | - ).filter_by(deleted=deleted |
249 | - ).one() |
250 | + return session.query(cls).\ |
251 | + filter_by(id=obj_id).\ |
252 | + filter_by(deleted=deleted).\ |
253 | + one() |
254 | except exc.NoResultFound: |
255 | new_exc = exception.NotFound("No model for id %s" % obj_id) |
256 | raise new_exc.__class__, new_exc, sys.exc_info()[2] |
257 | @@ -160,7 +160,7 @@ |
258 | |
259 | @validates('status') |
260 | def validate_status(self, key, status): |
261 | - if not status in ('available', 'pending', 'disabled'): |
262 | + if not status in ('active', 'queued', 'killed', 'saving'): |
263 | raise exception.Invalid("Invalid status '%s' for image." % status) |
264 | return status |
265 | |
266 | |
267 | === modified file 'glance/registry/server.py' |
268 | --- glance/registry/server.py 2010-12-26 04:50:01 +0000 |
269 | +++ glance/registry/server.py 2011-01-11 17:43:19 +0000 |
270 | @@ -106,7 +106,7 @@ |
271 | image_data = json.loads(req.body)['image'] |
272 | |
273 | # Ensure the image has a status set |
274 | - image_data.setdefault('status', 'available') |
275 | + image_data.setdefault('status', 'active') |
276 | |
277 | context = None |
278 | try: |
279 | |
280 | === modified file 'glance/server.py' |
281 | --- glance/server.py 2010-12-26 04:56:10 +0000 |
282 | +++ glance/server.py 2011-01-11 17:43:19 +0000 |
283 | @@ -162,17 +162,96 @@ |
284 | util.inject_image_meta_into_headers(res, image) |
285 | return req.get_response(res) |
286 | |
287 | + def _reserve(self, req): |
288 | + image_meta = util.get_image_meta_from_headers(req) |
289 | + image_meta['status'] = 'queued' |
290 | + |
291 | + try: |
292 | + image_meta = registry.add_image_metadata(image_meta) |
293 | + return image_meta |
294 | + except exception.Duplicate: |
295 | + msg = "An image with identifier %s already exists"\ |
296 | + % image_meta['id'] |
297 | + logging.error(msg) |
298 | + raise HTTPConflict(msg, request=req) |
299 | + except exception.Invalid: |
300 | + raise HTTPBadRequest() |
301 | + |
302 | + def _upload(self, req, image_meta): |
303 | + content_type = req.headers.get('content-type', 'notset') |
304 | + if content_type != 'application/octet-stream': |
305 | + raise HTTPBadRequest( |
306 | + "Content-Type must be application/octet-stream") |
307 | + |
308 | + image_store = req.headers.get( |
309 | + 'x-image-meta-store', FLAGS.default_store) |
310 | + |
311 | + store = self.get_store_or_400(req, image_store) |
312 | + |
313 | + image_meta['status'] = 'saving' |
314 | + registry.update_image_metadata(image_meta['id'], image_meta) |
315 | + |
316 | + try: |
317 | + location = store.add(image_meta['id'], req.body) |
318 | + return location |
319 | + except exception.Duplicate, e: |
320 | + logging.error("Error adding image to store: %s", str(e)) |
321 | + raise HTTPConflict(str(e), request=req) |
322 | + |
323 | + def _activate(self, req, image_meta, location): |
324 | + image_meta['location'] = location |
325 | + image_meta['status'] = 'active' |
326 | + registry.update_image_metadata(image_meta['id'], image_meta) |
327 | + |
328 | + def _kill(self, req, image_meta): |
329 | + image_meta['status'] = 'killed' |
330 | + registry.update_image_metadata(image_meta['id'], image_meta) |
331 | + |
332 | + def _safe_kill(self, req, image_meta): |
333 | + """Mark image killed without raising exceptions if it fails. |
334 | + |
335 | + Since _kill is meant to be called from exceptions handlers, it should |
336 | + not raise itself, rather it should just log its error. |
337 | + """ |
338 | + try: |
339 | + self._kill(req, image_meta) |
340 | + except Exception, e: |
341 | + logging.error("Unable to kill image %s: %s", |
342 | + image_meta['id'], repr(e)) |
343 | + |
344 | + def _upload_and_activate(self, req, image_meta): |
345 | + try: |
346 | + location = self._upload(req, image_meta) |
347 | + self._activate(req, image_meta, location) |
348 | + except Exception, e: |
349 | + # NOTE(sirp): _safe_kill uses httplib which, in turn, uses |
350 | + # Eventlet's GreenSocket. Eventlet subsequently clears exceptions |
351 | + # by calling `sys.exc_clear()`. This is why we have to `raise e` |
352 | + # instead of `raise` |
353 | + self._safe_kill(req, image_meta) |
354 | + raise e |
355 | + |
356 | def create(self, req): |
357 | """ |
358 | - Adds a new image to Glance. The body of the request may be a |
359 | - mime-encoded image data. Metadata about the image is sent via |
360 | - HTTP Headers. |
361 | - |
362 | - If the metadata about the image does not include a location |
363 | - to find the image, or if the image location is not valid, |
364 | - the request body *must* be encoded as application/octet-stream |
365 | - and be the image data itself, otherwise an HTTPBadRequest is |
366 | - returned. |
367 | + Adds a new image to Glance. Three scenarios exist when creating an |
368 | + image: |
369 | + |
370 | + 1. If the image data is available for upload, create can be passed the |
371 | + image data as the request body and the metadata as the request |
372 | + headers. The image will initially be 'queued', during upload it |
373 | + will be in the 'saving' status, and then 'killed' or 'active' |
374 | + depending on whether the upload completed successfully. |
375 | + |
376 | + 2. If the image data exists somewhere else, you can pass in the source |
377 | + using the x-image-meta-location header |
378 | + |
379 | + 3. If the image data is not available yet, but you'd like reserve a |
380 | + spot for it, you can omit the data and a record will be created in |
381 | + the 'queued' state. This exists primarily to maintain backwards |
382 | + compatibility with OpenStack/Rackspace API semantics. |
383 | + |
384 | + The request body *must* be encoded as application/octet-stream, |
385 | + otherwise an HTTPBadRequest is returned. |
386 | |
387 | Upon a successful save of the image data and metadata, a response |
388 | containing metadata about the image is returned, including its |
389 | @@ -184,62 +263,16 @@ |
390 | and the request body is not application/octet-stream |
391 | image data. |
392 | """ |
393 | - |
394 | - # Verify the request and headers before we generate a new id |
395 | - |
396 | - image_in_body = False |
397 | - image_store = None |
398 | - header_keys = [k.lower() for k in req.headers.keys()] |
399 | - if 'x-image-meta-location' not in header_keys: |
400 | - if ('content-type' not in header_keys or |
401 | - req.headers['content-type'] != 'application/octet-stream'): |
402 | - raise HTTPBadRequest("Image location was not specified in " |
403 | - "headers and the request body was not " |
404 | - "mime-encoded as application/" |
405 | - "octet-stream.", request=req) |
406 | - else: |
407 | - if 'x-image-meta-store' in header_keys: |
408 | - image_store = req.headers['x-image-meta-store'] |
409 | - image_status = 'pending' # set to available when stored... |
410 | - image_in_body = True |
411 | + image_meta = self._reserve(req) |
412 | + |
413 | + if req.body: |
414 | + self._upload_and_activate(req, image_meta) |
415 | else: |
416 | - image_location = req.headers['x-image-meta-location'] |
417 | - image_store = get_store_from_location(image_location) |
418 | - image_status = 'available' |
419 | - |
420 | - # If image is the request body, validate that the requested |
421 | - # or default store is capable of storing the image data... |
422 | - if not image_store: |
423 | - image_store = FLAGS.default_store |
424 | - if image_in_body: |
425 | - store = self.get_store_or_400(req, image_store) |
426 | - |
427 | - image_meta = util.get_image_meta_from_headers(req) |
428 | - |
429 | - image_meta['status'] = image_status |
430 | - image_meta['store'] = image_store |
431 | - try: |
432 | - image_meta = registry.add_image_metadata(image_meta) |
433 | - |
434 | - if image_in_body: |
435 | - try: |
436 | - location = store.add(image_meta['id'], req.body) |
437 | - except exception.Duplicate, e: |
438 | - logging.error("Error adding image to store: %s", str(e)) |
439 | - return HTTPConflict(str(e), request=req) |
440 | - image_meta['status'] = 'available' |
441 | - image_meta['location'] = location |
442 | - registry.update_image_metadata(image_meta['id'], image_meta) |
443 | - |
444 | - return dict(image=image_meta) |
445 | - |
446 | - except exception.Duplicate: |
447 | - msg = "An image with identifier %s already exists"\ |
448 | - % image_meta['id'] |
449 | - logging.error(msg) |
450 | - return HTTPConflict(msg, request=req) |
451 | - except exception.Invalid: |
452 | - return HTTPBadRequest() |
453 | + if 'x-image-meta-location' in req.headers: |
454 | + location = req.headers['x-image-meta-location'] |
455 | + self._activate(req, image_meta, location) |
456 | + |
457 | + return dict(image=image_meta) |
458 | |
459 | def update(self, req, id): |
460 | """ |
461 | @@ -248,14 +281,21 @@ |
462 | :param request: The WSGI/Webob Request object |
463 | :param id: The opaque image identifier |
464 | |
465 | - :retval Returns the updated image information as a mapping, |
466 | + :retval Returns the updated image information as a mapping |
467 | |
468 | """ |
469 | - image = self.get_image_meta_or_404(req, id) |
470 | - |
471 | - image_data = json.loads(req.body)['image'] |
472 | - updated_image = registry.update_image_metadata(id, image_data) |
473 | - return dict(image=updated_image) |
474 | + orig_image_meta = self.get_image_meta_or_404(req, id) |
475 | + new_image_meta = util.get_image_meta_from_headers(req) |
476 | + |
477 | + if req.body and (orig_image_meta['status'] != 'queued'): |
478 | + raise HTTPConflict("Cannot upload to an unqueued image") |
479 | + |
480 | + image_meta = registry.update_image_metadata(id, new_image_meta) |
481 | + |
482 | + if req.body: |
483 | + self._upload_and_activate(req, image_meta) |
484 | + |
485 | + return dict(image=image_meta) |
486 | |
487 | def delete(self, req, id): |
488 | """ |
489 | |
490 | === modified file 'glance/store/__init__.py' |
491 | --- glance/store/__init__.py 2010-12-20 18:38:56 +0000 |
492 | +++ glance/store/__init__.py 2011-01-11 17:43:19 +0000 |
493 | @@ -56,12 +56,10 @@ |
494 | from glance.store.swift import SwiftBackend |
495 | from glance.store.filesystem import FilesystemBackend |
496 | |
497 | - BACKENDS = { |
498 | - "file": FilesystemBackend, |
499 | - "http": HTTPBackend, |
500 | - "https": HTTPBackend, |
501 | - "swift": SwiftBackend |
502 | - } |
503 | + BACKENDS = {"file": FilesystemBackend, |
504 | + "http": HTTPBackend, |
505 | + "https": HTTPBackend, |
506 | + "swift": SwiftBackend} |
507 | |
508 | try: |
509 | return BACKENDS[backend] |
510 | |
511 | === modified file 'glance/store/backends/__init__.py' |
512 | --- glance/store/backends/__init__.py 2011-01-02 02:47:59 +0000 |
513 | +++ glance/store/backends/__init__.py 2011-01-11 17:43:19 +0000 |
514 | @@ -87,12 +87,10 @@ |
515 | from glance.store.backends.http import HTTPBackend |
516 | from glance.store.backends.swift import SwiftBackend |
517 | |
518 | - BACKENDS = { |
519 | - "file": FilesystemBackend, |
520 | - "http": HTTPBackend, |
521 | - "https": HTTPBackend, |
522 | - "swift": SwiftBackend |
523 | - } |
524 | + BACKENDS = {"file": FilesystemBackend, |
525 | + "http": HTTPBackend, |
526 | + "https": HTTPBackend, |
527 | + "swift": SwiftBackend} |
528 | |
529 | try: |
530 | return BACKENDS[backend] |
531 | |
532 | === modified file 'glance/store/filesystem.py' |
533 | --- glance/store/filesystem.py 2011-01-02 02:47:59 +0000 |
534 | +++ glance/store/filesystem.py 2011-01-11 17:43:19 +0000 |
535 | @@ -114,7 +114,6 @@ |
536 | |
537 | :retval The location that was written, with file:// scheme prepended |
538 | """ |
539 | - |
540 | datadir = FLAGS.filesystem_store_datadir |
541 | |
542 | if not os.path.exists(datadir): |
543 | |
544 | === modified file 'tests/stubs.py' |
545 | --- tests/stubs.py 2011-01-04 22:00:37 +0000 |
546 | +++ tests/stubs.py 2011-01-11 17:43:19 +0000 |
547 | @@ -279,7 +279,7 @@ |
548 | FIXTURES = [ |
549 | {'id': 1, |
550 | 'name': 'fake image #1', |
551 | - 'status': 'available', |
552 | + 'status': 'active', |
553 | 'type': 'kernel', |
554 | 'is_public': False, |
555 | 'created_at': datetime.datetime.utcnow(), |
556 | @@ -291,7 +291,7 @@ |
557 | 'properties': []}, |
558 | {'id': 2, |
559 | 'name': 'fake image #2', |
560 | - 'status': 'available', |
561 | + 'status': 'active', |
562 | 'type': 'kernel', |
563 | 'is_public': True, |
564 | 'created_at': datetime.datetime.utcnow(), |
565 | @@ -302,7 +302,7 @@ |
566 | 'location': "file:///tmp/glance-tests/2", |
567 | 'properties': []}] |
568 | |
569 | - VALID_STATUSES = ('available', 'disabled', 'pending') |
570 | + VALID_STATUSES = ('active', 'killed', 'queued', 'saving') |
571 | |
572 | def __init__(self): |
573 | self.images = FakeDatastore.FIXTURES |
574 | @@ -317,7 +317,7 @@ |
575 | values['id']) |
576 | |
577 | if 'status' not in values.keys(): |
578 | - values['status'] = 'available' |
579 | + values['status'] = 'active' |
580 | else: |
581 | if not values['status'] in self.VALID_STATUSES: |
582 | raise exception.Invalid("Invalid status '%s' for image" % |
583 | |
584 | === modified file 'tests/unit/test_api.py' |
585 | --- tests/unit/test_api.py 2011-01-04 22:00:37 +0000 |
586 | +++ tests/unit/test_api.py 2011-01-11 17:43:19 +0000 |
587 | @@ -15,6 +15,7 @@ |
588 | # License for the specific language governing permissions and limitations |
589 | # under the License. |
590 | |
591 | +import httplib |
592 | import json |
593 | import unittest |
594 | |
595 | @@ -87,7 +88,7 @@ |
596 | 'name': 'fake image #2', |
597 | 'is_public': True, |
598 | 'type': 'kernel', |
599 | - 'status': 'available' |
600 | + 'status': 'active' |
601 | } |
602 | req = webob.Request.blank('/images/detail') |
603 | res = req.get_response(rserver.API()) |
604 | @@ -125,7 +126,7 @@ |
605 | self.assertEquals(3, res_dict['image']['id']) |
606 | |
607 | # Test status was updated properly |
608 | - self.assertEquals('available', res_dict['image']['status']) |
609 | + self.assertEquals('active', res_dict['image']['status']) |
610 | |
611 | def test_create_image_with_bad_status(self): |
612 | """Tests proper exception is raised if a bad status is set""" |
613 | @@ -251,7 +252,7 @@ |
614 | self.stubs.UnsetAll() |
615 | |
616 | def test_add_image_no_location_no_image_as_body(self): |
617 | - """Tests raises BadRequest for no body and no loc header""" |
618 | + """Tests creates a queued image for no body and no loc header""" |
619 | fixture_headers = {'x-image-meta-store': 'file', |
620 | 'x-image-meta-name': 'fake image #3'} |
621 | |
622 | @@ -260,7 +261,10 @@ |
623 | for k, v in fixture_headers.iteritems(): |
624 | req.headers[k] = v |
625 | res = req.get_response(server.API()) |
626 | - self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) |
627 | + self.assertEquals(res.status_int, httplib.OK) |
628 | + |
629 | + res_body = json.loads(res.body)['image'] |
630 | + self.assertEquals('queued', res_body['status']) |
631 | |
632 | def test_add_image_bad_store(self): |
633 | """Tests raises BadRequest for invalid store header""" |
634 | |
635 | === modified file 'tests/unit/test_clients.py' |
636 | --- tests/unit/test_clients.py 2011-01-04 22:00:37 +0000 |
637 | +++ tests/unit/test_clients.py 2011-01-11 17:43:19 +0000 |
638 | @@ -78,7 +78,7 @@ |
639 | 'name': 'fake image #2', |
640 | 'is_public': True, |
641 | 'type': 'kernel', |
642 | - 'status': 'available', |
643 | + 'status': 'active', |
644 | 'size': 19, |
645 | 'location': "file:///tmp/glance-tests/2", |
646 | 'properties': {}} |
647 | @@ -87,7 +87,7 @@ |
648 | 'name': 'fake image #2', |
649 | 'is_public': True, |
650 | 'type': 'kernel', |
651 | - 'status': 'available', |
652 | + 'status': 'active', |
653 | 'size': 19, |
654 | 'location': "file:///tmp/glance-tests/2", |
655 | 'properties': {}} |
656 | @@ -104,7 +104,7 @@ |
657 | 'name': 'fake image #2', |
658 | 'is_public': True, |
659 | 'type': 'kernel', |
660 | - 'status': 'available', |
661 | + 'status': 'active', |
662 | 'size': 19, |
663 | 'location': "file:///tmp/glance-tests/2", |
664 | 'properties': {}} |
665 | @@ -113,7 +113,7 @@ |
666 | 'name': 'fake image #2', |
667 | 'is_public': True, |
668 | 'type': 'kernel', |
669 | - 'status': 'available', |
670 | + 'status': 'active', |
671 | 'size': 19, |
672 | 'location': "file:///tmp/glance-tests/2", |
673 | 'properties': {}} |
674 | @@ -152,7 +152,7 @@ |
675 | |
676 | # Test status was updated properly |
677 | self.assertTrue('status' in data.keys()) |
678 | - self.assertEquals('available', data['status']) |
679 | + self.assertEquals('active', data['status']) |
680 | |
681 | def test_add_image_with_properties(self): |
682 | """Tests that we can add image metadata with properties""" |
683 | @@ -181,7 +181,7 @@ |
684 | |
685 | # Test status was updated properly |
686 | self.assertTrue('status' in new_image.keys()) |
687 | - self.assertEquals('available', new_image['status']) |
688 | + self.assertEquals('active', new_image['status']) |
689 | |
690 | def test_add_image_already_exists(self): |
691 | """Tests proper exception is raised if image with ID already exists""" |
692 | @@ -293,7 +293,7 @@ |
693 | 'name': 'fake image #2', |
694 | 'is_public': True, |
695 | 'type': 'kernel', |
696 | - 'status': 'available', |
697 | + 'status': 'active', |
698 | 'size': 19, |
699 | 'location': "file:///tmp/glance-tests/2", |
700 | 'properties': {}} |
701 | @@ -330,7 +330,7 @@ |
702 | 'name': 'fake image #2', |
703 | 'is_public': True, |
704 | 'type': 'kernel', |
705 | - 'status': 'available', |
706 | + 'status': 'active', |
707 | 'size': 19, |
708 | 'location': "file:///tmp/glance-tests/2", |
709 | 'properties': {}} |
710 | @@ -339,7 +339,7 @@ |
711 | 'name': 'fake image #2', |
712 | 'is_public': True, |
713 | 'type': 'kernel', |
714 | - 'status': 'available', |
715 | + 'status': 'active', |
716 | 'size': 19, |
717 | 'location': "file:///tmp/glance-tests/2", |
718 | 'properties': {}} |
719 | @@ -356,7 +356,7 @@ |
720 | 'name': 'fake image #2', |
721 | 'is_public': True, |
722 | 'type': 'kernel', |
723 | - 'status': 'available', |
724 | + 'status': 'active', |
725 | 'size': 19, |
726 | 'location': "file:///tmp/glance-tests/2", |
727 | 'properties': {}} |
728 | @@ -365,7 +365,7 @@ |
729 | 'name': 'fake image #2', |
730 | 'is_public': True, |
731 | 'type': 'kernel', |
732 | - 'status': 'available', |
733 | + 'status': 'active', |
734 | 'size': 19, |
735 | 'location': "file:///tmp/glance-tests/2", |
736 | 'properties': {}} |
737 | @@ -383,15 +383,14 @@ |
738 | 42) |
739 | |
740 | def test_add_image_without_location_or_raw_data(self): |
741 | - """Tests client throws Invalid if missing both location and raw data""" |
742 | + """Tests client returns image as queued""" |
743 | fixture = {'name': 'fake public image', |
744 | 'is_public': True, |
745 | 'type': 'kernel' |
746 | } |
747 | - |
748 | - self.assertRaises(exception.Invalid, |
749 | - self.client.add_image, |
750 | - fixture) |
751 | + |
752 | + image_meta = self.client.add_image(fixture) |
753 | + self.assertEquals('queued', image_meta['status']) |
754 | |
755 | def test_add_image_basic(self): |
756 | """Tests that we can add image metadata and returns the new id""" |
757 | @@ -401,11 +400,12 @@ |
758 | 'size': 19, |
759 | 'location': "file:///tmp/glance-tests/2", |
760 | } |
761 | - |
762 | - new_id = self.client.add_image(fixture) |
763 | + |
764 | + new_image = self.client.add_image(fixture) |
765 | + new_image_id = new_image['id'] |
766 | |
767 | # Test ID auto-assigned properly |
768 | - self.assertEquals(3, new_id) |
769 | + self.assertEquals(3, new_image_id) |
770 | |
771 | # Test all other attributes set |
772 | data = self.client.get_image_meta(3) |
773 | @@ -415,7 +415,7 @@ |
774 | |
775 | # Test status was updated properly |
776 | self.assertTrue('status' in data.keys()) |
777 | - self.assertEquals('available', data['status']) |
778 | + self.assertEquals('active', data['status']) |
779 | |
780 | def test_add_image_with_properties(self): |
781 | """Tests that we can add image metadata with properties""" |
782 | @@ -433,11 +433,12 @@ |
783 | 'location': "file:///tmp/glance-tests/2", |
784 | 'properties': {'distro': 'Ubuntu 10.04 LTS'} |
785 | } |
786 | - |
787 | - new_id = self.client.add_image(fixture) |
788 | + |
789 | + new_image = self.client.add_image(fixture) |
790 | + new_image_id = new_image['id'] |
791 | |
792 | # Test ID auto-assigned properly |
793 | - self.assertEquals(3, new_id) |
794 | + self.assertEquals(3, new_image_id) |
795 | |
796 | # Test all other attributes set |
797 | data = self.client.get_image_meta(3) |
798 | @@ -446,8 +447,8 @@ |
799 | self.assertEquals(v, data[k]) |
800 | |
801 | # Test status was updated properly |
802 | - self.assertTrue('status' in data.keys()) |
803 | - self.assertEquals('available', data['status']) |
804 | + self.assertTrue('status' in data) |
805 | + self.assertEquals('active', data['status']) |
806 | |
807 | def test_add_image_already_exists(self): |
808 | """Tests proper exception is raised if image with ID already exists""" |
809 | @@ -474,11 +475,8 @@ |
810 | 'location': "file:///tmp/glance-tests/2", |
811 | } |
812 | |
813 | - new_id = self.client.add_image(fixture) |
814 | - |
815 | - data = self.client.get_image_meta(new_id) |
816 | - |
817 | - self.assertEquals(data['status'], 'available') |
818 | + new_image = self.client.add_image(fixture) |
819 | + self.assertEquals(new_image['status'], 'active') |
820 | |
821 | def test_add_image_with_image_data_as_string(self): |
822 | """Tests can add image by passing image data as string""" |
823 | @@ -491,9 +489,9 @@ |
824 | |
825 | image_data_fixture = r"chunk0000remainder" |
826 | |
827 | - new_id = self.client.add_image(fixture, image_data_fixture) |
828 | - |
829 | - self.assertEquals(3, new_id) |
830 | + new_image = self.client.add_image(fixture, image_data_fixture) |
831 | + new_image_id = new_image['id'] |
832 | + self.assertEquals(3, new_image_id) |
833 | |
834 | new_meta, new_image_chunks = self.client.get_image(3) |
835 | |
836 | @@ -525,9 +523,9 @@ |
837 | tmp_file.write(image_data_fixture) |
838 | tmp_file.close() |
839 | |
840 | - new_id = self.client.add_image(fixture, open(tmp_image_filepath)) |
841 | - |
842 | - self.assertEquals(3, new_id) |
843 | + new_image = self.client.add_image(fixture, open(tmp_image_filepath)) |
844 | + new_image_id = new_image['id'] |
845 | + self.assertEquals(3, new_image_id) |
846 | |
847 | if os.path.exists(tmp_image_filepath): |
848 | os.unlink(tmp_image_filepath) |
849 | |
850 | === modified file 'tests/unit/test_registry_api.py' |
851 | --- tests/unit/test_registry_api.py 2011-01-04 22:00:37 +0000 |
852 | +++ tests/unit/test_registry_api.py 2011-01-11 17:43:19 +0000 |
853 | @@ -80,7 +80,7 @@ |
854 | 'name': 'fake image #2', |
855 | 'is_public': True, |
856 | 'type': 'kernel', |
857 | - 'status': 'available' |
858 | + 'status': 'active' |
859 | } |
860 | req = webob.Request.blank('/images/detail') |
861 | res = req.get_response(server.API()) |
862 | @@ -118,7 +118,7 @@ |
863 | self.assertEquals(3, res_dict['image']['id']) |
864 | |
865 | # Test status was updated properly |
866 | - self.assertEquals('available', res_dict['image']['status']) |
867 | + self.assertEquals('active', res_dict['image']['status']) |
868 | |
869 | def test_create_image_with_bad_status(self): |
870 | """Tests proper exception is raised if a bad status is set""" |
Hi Rick!
Great work! Here's a couple small things to fix, though :)
1)
8 - raise exception.Duplicate Duplicate( res.read( ))
9 + raise exception.
11 - raise exception. BadInputError BadInputError( res.read( ))
12 + raise exception.
The above will read an entire image into the exception when the exception is raised during add_image()! :) Prolly best to remove that...
2)
You did a great job updating the docstrings in glance.server. Please do also update the docs in docs/source/ to match :)
Thanks!
jay