Merge lp:~jaypipes/glance/api-add-image into lp:~hudson-openstack/glance/trunk
- api-add-image
- Merge into trunk
Proposed by
Jay Pipes
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 30 |
Merged at revision: | 27 |
Proposed branch: | lp:~jaypipes/glance/api-add-image |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
858 lines (+543/-33) 11 files modified
doc/source/index.rst (+239/-0) glance/client.py (+19/-8) glance/common/exception.py (+5/-0) glance/common/flags.py (+2/-0) glance/registry/db/sqlalchemy/api.py (+23/-8) glance/server.py (+43/-10) glance/store/__init__.py (+1/-1) glance/store/filesystem.py (+37/-1) tests/stubs.py (+17/-1) tests/unit/test_api.py (+69/-2) tests/unit/test_clients.py (+88/-2) |
To merge this branch: | bzr merge lp:~jaypipes/glance/api-add-image |
Related bugs: | |
Related blueprints: |
Implement adding image data directly to Glance
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher MacGown (community) | Approve | ||
Rick Harris | Pending | ||
Review via email: mp+44355@code.launchpad.net |
Commit message
Description of the change
Enhances POST /images call to, you know, actually make it work...
Contains Rick's additions as well.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/source/index.rst' |
2 | --- doc/source/index.rst 2010-12-16 22:04:58 +0000 |
3 | +++ doc/source/index.rst 2010-12-21 15:27:59 +0000 |
4 | @@ -198,6 +198,122 @@ |
5 | .. toctree:: |
6 | :maxdepth: 1 |
7 | |
8 | + |
9 | +Adding a New Virtual Machine Image |
10 | +---------------------------------- |
11 | + |
12 | +We have created a new virtual machine image in some way (created a |
13 | +"golden image" or snapshotted/backed up an existing image) and we |
14 | +wish to do two things: |
15 | + |
16 | + * Store the disk image data in Glance |
17 | + * Store metadata about this image in Glance |
18 | + |
19 | +We can do the above two activities in a single call to the Glance API. |
20 | +Assuming, like in the examples above, that a Glance API server is running |
21 | +at `glance.openstack.org`, we issue a `POST` request to add an image to |
22 | +Glance:: |
23 | + |
24 | + POST http://glance.openstack.org/images/ |
25 | + |
26 | +The metadata about the image is sent to Glance in HTTP headers. The body |
27 | +of the HTTP request to the Glance API will be the MIME-encoded disk |
28 | +image data. |
29 | + |
30 | + |
31 | +Adding Image Metadata in HTTP Headers |
32 | +************************************* |
33 | + |
34 | +Glance will view as image metadata any HTTP header that it receives in a |
35 | +`POST` request where the header key is prefixed with the strings |
36 | +`x-image-meta-` and `x-image-meta-property-`. |
37 | + |
38 | +The list of metadata headers that Glance accepts are listed below. |
39 | + |
40 | + * `x-image-meta-name` |
41 | + |
42 | + This header is required. Its value should be the name of the image. |
43 | + |
44 | + Note that the name of an image *is not unique to a Glance node*. It |
45 | + would be an unrealistic expectation of users to know all the unique |
46 | + names of all other user's images. |
47 | + |
48 | + * `x-image-meta-id` |
49 | + |
50 | + This header is optional. |
51 | + |
52 | + When present, Glance will use the supplied identifier for the image. |
53 | + If the identifier already exists in that Glance node, then a |
54 | + `409 Conflict` will be returned by Glance. |
55 | + |
56 | + When this header is *not* present, Glance will generate an identifier |
57 | + for the image and return this identifier in the response (see below) |
58 | + |
59 | + * `x-image-meta-store` |
60 | + |
61 | + This header is optional. Valid values are one of `file` or `swift` |
62 | + |
63 | + When present, Glance will attempt to store the disk image data in the |
64 | + backing store indicated by the value of the header. If the Glance node |
65 | + does not support the backing store, Glance will return a `400 Bad Request`. |
66 | + |
67 | + When not present, Glance will store the disk image data in the backing |
68 | + store that is marked default. See the configuration option `default_store` |
69 | + for more information. |
70 | + |
71 | + * `x-image-meta-type` |
72 | + |
73 | + This header is required. Valid values are one of `kernel`, `machine`, `raw`, |
74 | + or `ramdisk`. |
75 | + |
76 | + * `x-image-meta-size` |
77 | + |
78 | + This header is optional. |
79 | + |
80 | + When present, Glance assumes that the expected size of the request body |
81 | + will be the value of this header. If the length in bytes of the request |
82 | + body *does not match* the value of this header, Glance will return a |
83 | + `400 Bad Request`. |
84 | + |
85 | + When not present, Glance will calculate the image's size based on the size |
86 | + of the request body. |
87 | + |
88 | + * `x-image-meta-is_public` |
89 | + |
90 | + This header is optional. |
91 | + |
92 | + When present, Glance converts the value of the header to a boolean value, |
93 | + so "on, 1, true" are all true values. When true, the image is marked as |
94 | + a public image, meaning that any user may view its metadata and may read |
95 | + the disk image from Glance. |
96 | + |
97 | + When not present, the image is assumed to be *not public* and specific to |
98 | + a user. |
99 | + |
100 | + * `x-image-meta-property-*` |
101 | + |
102 | + When Glance receives any HTTP header whose key begins with the string prefix |
103 | + `x-image-meta-property-`, Glance adds the key and value to a set of custom, |
104 | + free-form image properties stored with the image. The key is the |
105 | + lower-cased string following the prefix `x-image-meta-property-` with dashes |
106 | + and punctuation replaced with underscores. |
107 | + |
108 | + For example, if the following HTTP header were sent:: |
109 | + |
110 | + x-image-meta-property-distro Ubuntu 10.10 |
111 | + |
112 | + Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the |
113 | + image in Glance. |
114 | + |
115 | + There is no limit on the number of free-form key/value attributes that can |
116 | + be attached to the image. However, keep in mind that the 8K limit on the |
117 | + size of all HTTP headers sent in a request will effectively limit the number |
118 | + of image properties. |
119 | + |
120 | + |
121 | +.. toctree:: |
122 | + :maxdepth: 1 |
123 | + |
124 | Image Identifiers |
125 | ================= |
126 | |
127 | @@ -325,6 +441,129 @@ |
128 | .. toctree:: |
129 | :maxdepth: 1 |
130 | |
131 | + |
132 | +Adding a New Virtual Machine Image |
133 | +---------------------------------- |
134 | + |
135 | +We have created a new virtual machine image in some way (created a |
136 | +"golden image" or snapshotted/backed up an existing image) and we |
137 | +wish to do two things: |
138 | + |
139 | + * Store the disk image data in Glance |
140 | + * Store metadata about this image in Glance |
141 | + |
142 | +We can do the above two activities in a single call to the Glance client. |
143 | +Assuming, like in the examples above, that a Glance API server is running |
144 | +at `glance.openstack.org`, we issue a call to `glance.client.Client.add_image`. |
145 | + |
146 | +The method signature is as follows:: |
147 | + |
148 | + glance.client.Client.add_image(image_meta, image_data=None) |
149 | + |
150 | +The `image_meta` argument is a mapping containing various image metadata. The |
151 | +`image_data` argument is the disk image data. |
152 | + |
153 | +The list of metadata that `image_meta` can contain are listed below. |
154 | + |
155 | + * `name` |
156 | + |
157 | + This key/value is required. Its value should be the name of the image. |
158 | + |
159 | + Note that the name of an image *is not unique to a Glance node*. It |
160 | + would be an unrealistic expectation of users to know all the unique |
161 | + names of all other user's images. |
162 | + |
163 | + * `id` |
164 | + |
165 | + This key/value is optional. |
166 | + |
167 | + When present, Glance will use the supplied identifier for the image. |
168 | + If the identifier already exists in that Glance node, then a |
169 | + `glance.common.exception.Duplicate` will be raised. |
170 | + |
171 | + When this key/value is *not* present, Glance will generate an identifier |
172 | + for the image and return this identifier in the response (see below) |
173 | + |
174 | + * `store` |
175 | + |
176 | + This key/value is optional. Valid values are one of `file` or `swift` |
177 | + |
178 | + When present, Glance will attempt to store the disk image data in the |
179 | + backing store indicated by the value. If the Glance node does not support |
180 | + the backing store, Glance will raise a `glance.common.exception.BadRequest` |
181 | + |
182 | + When not present, Glance will store the disk image data in the backing |
183 | + store that is marked default. See the configuration option `default_store` |
184 | + for more information. |
185 | + |
186 | + * `type` |
187 | + |
188 | + This key/values is required. Valid values are one of `kernel`, `machine`, |
189 | + `raw`, or `ramdisk`. |
190 | + |
191 | + * `size` |
192 | + |
193 | + This key/value is optional. |
194 | + |
195 | + When present, Glance assumes that the expected size of the request body |
196 | + will be the value. If the length in bytes of the request body *does not |
197 | + match* the value, Glance will raise a `glance.common.exception.BadRequest` |
198 | + |
199 | + When not present, Glance will calculate the image's size based on the size |
200 | + of the request body. |
201 | + |
202 | + * `is_public` |
203 | + |
204 | + This key/value is optional. |
205 | + |
206 | + When present, Glance converts the value to a boolean value, so "on, 1, true" |
207 | + are all true values. When true, the image is marked as a public image, |
208 | + meaning that any user may view its metadata and may read the disk image from |
209 | + Glance. |
210 | + |
211 | + When not present, the image is assumed to be *not public* and specific to |
212 | + a user. |
213 | + |
214 | + * `properties` |
215 | + |
216 | + This key/value is optional. |
217 | + |
218 | + When present, the value is assumed to be a mapping of free-form key/value |
219 | + attributes to store with the image. |
220 | + |
221 | + For example, if the following is the value of the `properties` key in the |
222 | + `image_meta` argument:: |
223 | + |
224 | + {'distro': 'Ubuntu 10.10'} |
225 | + |
226 | + Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the |
227 | + image in Glance. |
228 | + |
229 | + There is no limit on the number of free-form key/value attributes that can |
230 | + be attached to the image with `properties`. However, keep in mind that there |
231 | + is a 8K limit on the size of all HTTP headers sent in a request and this |
232 | + number will effectively limit the number of image properties. |
233 | + |
234 | +As a complete example, the following code would add a new machine image to |
235 | +Glance:: |
236 | + |
237 | + from glance.client import Client |
238 | + |
239 | + c = Client("glance.openstack.org", 9292) |
240 | + |
241 | + meta = {'name': 'Ubuntu 10.10 5G', |
242 | + 'type': 'machine', |
243 | + 'is_public': True, |
244 | + 'properties': {'distro': 'Ubuntu 10.10'}} |
245 | + |
246 | + new_meta = c.add_image(meta, open('/path/to/image.tar.gz')) |
247 | + |
248 | + print 'Stored image. Got identifier: %s' % new_meta['id'] |
249 | + |
250 | + |
251 | +.. toctree:: |
252 | + :maxdepth: 1 |
253 | + |
254 | Indices and tables |
255 | ================== |
256 | |
257 | |
258 | === modified file 'glance/client.py' |
259 | --- glance/client.py 2010-12-16 23:19:23 +0000 |
260 | +++ glance/client.py 2010-12-21 15:27:59 +0000 |
261 | @@ -46,11 +46,6 @@ |
262 | pass |
263 | |
264 | |
265 | -class BadInputError(Exception): |
266 | - """Error resulting from a client sending bad input to a server""" |
267 | - pass |
268 | - |
269 | - |
270 | class ImageBodyIterator(object): |
271 | |
272 | """ |
273 | @@ -150,7 +145,7 @@ |
274 | elif status_code == httplib.CONFLICT: |
275 | raise exception.Duplicate |
276 | elif status_code == httplib.BAD_REQUEST: |
277 | - raise BadInputError |
278 | + raise exception.BadInputError |
279 | else: |
280 | raise Exception("Unknown error occurred! %d" % status_code) |
281 | |
282 | @@ -235,17 +230,33 @@ |
283 | """ |
284 | Tells Glance about an image's metadata as well |
285 | as optionally the image_data itself |
286 | + |
287 | + :param image_meta: Mapping of information about the |
288 | + image |
289 | + :param image_data: Optional string of raw image data |
290 | + or file-like object that can be |
291 | + used to read the image data |
292 | + |
293 | + :retval The newly-stored image's metadata. |
294 | """ |
295 | if not image_data and 'location' not in image_meta.keys(): |
296 | raise exception.Invalid("You must either specify a location " |
297 | "for the image or supply the actual " |
298 | "image data when adding an image to " |
299 | "Glance") |
300 | - body = image_data |
301 | headers = util.image_meta_to_http_headers(image_meta) |
302 | + if image_data: |
303 | + if hasattr(image_data, 'read'): |
304 | + # TODO(jaypipes): This is far from efficient. Implement |
305 | + # chunked transfer encoding if size is not in image_meta |
306 | + body = image_data.read() |
307 | + else: |
308 | + body = image_data |
309 | + headers['content-type'] = 'application/octet-stream' |
310 | + else: |
311 | + body = None |
312 | |
313 | res = self.do_request("POST", "/images", body, headers) |
314 | - # Registry returns a JSONified dict(image=image_info) |
315 | data = json.loads(res.read()) |
316 | return data['image']['id'] |
317 | |
318 | |
319 | === modified file 'glance/common/exception.py' |
320 | --- glance/common/exception.py 2010-11-23 18:12:11 +0000 |
321 | +++ glance/common/exception.py 2010-12-21 15:27:59 +0000 |
322 | @@ -70,6 +70,11 @@ |
323 | pass |
324 | |
325 | |
326 | +class BadInputError(Exception): |
327 | + """Error resulting from a client sending bad input to a server""" |
328 | + pass |
329 | + |
330 | + |
331 | def wrap_exception(f): |
332 | def _wrap(*args, **kw): |
333 | try: |
334 | |
335 | === modified file 'glance/common/flags.py' |
336 | --- glance/common/flags.py 2010-09-29 00:20:11 +0000 |
337 | +++ glance/common/flags.py 2010-12-21 15:27:59 +0000 |
338 | @@ -173,3 +173,5 @@ |
339 | 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"), |
340 | 'connection string for sql database') |
341 | DEFINE_bool('verbose', False, 'show debug output') |
342 | +DEFINE_string('default_store', 'file', |
343 | + 'Default storage backend. Default: "file"') |
344 | |
345 | === modified file 'glance/registry/db/sqlalchemy/api.py' |
346 | --- glance/registry/db/sqlalchemy/api.py 2010-12-18 18:00:21 +0000 |
347 | +++ glance/registry/db/sqlalchemy/api.py 2010-12-21 15:27:59 +0000 |
348 | @@ -52,10 +52,19 @@ |
349 | |
350 | |
351 | def image_create(_context, values): |
352 | + values['size'] = int(values['size']) |
353 | + values['is_public'] = bool(values.get('is_public', False)) |
354 | + properties = values.pop('properties', {}) |
355 | + |
356 | image_ref = models.Image() |
357 | image_ref.update(values) |
358 | image_ref.save() |
359 | - return image_ref |
360 | + |
361 | + for key, value in properties.iteritems(): |
362 | + prop_values = {'image_id': image_ref.id, 'key': key, 'value': value} |
363 | + image_property_create(_context, prop_values) |
364 | + |
365 | + return image_get(_context, image_ref.id) |
366 | |
367 | |
368 | def image_destroy(_context, image_id): |
369 | @@ -102,18 +111,25 @@ |
370 | def image_update(_context, image_id, values): |
371 | session = get_session() |
372 | with session.begin(): |
373 | + values['size'] = int(values['size']) |
374 | + values['is_public'] = bool(values.get('is_public', False)) |
375 | + properties = values.pop('properties', {}) |
376 | + |
377 | image_ref = models.Image.find(image_id, session=session) |
378 | image_ref.update(values) |
379 | image_ref.save(session=session) |
380 | |
381 | + for key, value in properties.iteritems(): |
382 | + prop_values = {'image_id': image_ref.id, 'key': key, 'value': value} |
383 | + image_property_create(_context, prop_values) |
384 | + |
385 | |
386 | ################### |
387 | |
388 | |
389 | def image_file_create(_context, values): |
390 | image_file_ref = models.ImageFile() |
391 | - for (key, value) in values.iteritems(): |
392 | - image_file_ref[key] = value |
393 | + image_file_ref.update(values) |
394 | image_file_ref.save() |
395 | return image_file_ref |
396 | |
397 | @@ -122,8 +138,7 @@ |
398 | |
399 | |
400 | def image_property_create(_context, values): |
401 | - image_properties_ref = models.ImageProperty() |
402 | - for (key, value) in values.iteritems(): |
403 | - image_properties_ref[key] = value |
404 | - image_properties_ref.save() |
405 | - return image_properties_ref |
406 | + image_property_ref = models.ImageProperty() |
407 | + image_property_ref.update(values) |
408 | + image_property_ref.save() |
409 | + return image_property_ref |
410 | |
411 | === modified file 'glance/server.py' |
412 | --- glance/server.py 2010-12-16 22:04:58 +0000 |
413 | +++ glance/server.py 2010-12-21 15:27:59 +0000 |
414 | @@ -23,6 +23,10 @@ |
415 | Configuration Options |
416 | --------------------- |
417 | |
418 | + `default_store`: When no x-image-meta-store header is sent for a |
419 | + `POST /images` request, this store will be used |
420 | + for storing the image data. Default: 'file' |
421 | + |
422 | """ |
423 | |
424 | import json |
425 | @@ -39,7 +43,9 @@ |
426 | from glance.common import wsgi |
427 | from glance.store import (get_from_backend, |
428 | delete_from_backend, |
429 | - get_store_from_location) |
430 | + get_store_from_location, |
431 | + get_backend_class, |
432 | + UnsupportedBackend) |
433 | from glance import registry |
434 | from glance import util |
435 | |
436 | @@ -174,9 +180,6 @@ |
437 | |
438 | :param request: The WSGI/Webob Request object |
439 | |
440 | - :see The `id_type` configuration option (default: uuid) determines |
441 | - the type of identifier that Glance generates for an image |
442 | - |
443 | :raises HTTPBadRequest if no x-image-meta-location is missing |
444 | and the request body is not application/octet-stream |
445 | image data. |
446 | @@ -195,7 +198,7 @@ |
447 | "mime-encoded as application/" |
448 | "octet-stream.", request=req) |
449 | else: |
450 | - if 'x-image-meta-store' in headers_keys: |
451 | + if 'x-image-meta-store' in header_keys: |
452 | image_store = req.headers['x-image-meta-store'] |
453 | image_status = 'pending' # set to available when stored... |
454 | image_in_body = True |
455 | @@ -204,23 +207,34 @@ |
456 | image_store = get_store_from_location(image_location) |
457 | image_status = 'available' |
458 | |
459 | + # If image is the request body, validate that the requested |
460 | + # or default store is capable of storing the image data... |
461 | + if not image_store: |
462 | + image_store = FLAGS.default_store |
463 | + if image_in_body: |
464 | + store = self.get_store_or_400(req, image_store) |
465 | + |
466 | image_meta = util.get_image_meta_from_headers(req) |
467 | + |
468 | image_meta['status'] = image_status |
469 | image_meta['store'] = image_store |
470 | - |
471 | try: |
472 | image_meta = registry.add_image_metadata(image_meta) |
473 | |
474 | if image_in_body: |
475 | - #store = stores.get_store() |
476 | - #store.add_image(req.body) |
477 | + try: |
478 | + location = store.add(image_meta['id'], req.body) |
479 | + except exception.Duplicate, e: |
480 | + return HTTPConflict(str(e), request=req) |
481 | image_meta['status'] = 'available' |
482 | - registries.update_image(image_meta) |
483 | + image_meta['location'] = location |
484 | + registry.update_image_metadata(image_meta['id'], image_meta) |
485 | |
486 | return dict(image=image_meta) |
487 | |
488 | except exception.Duplicate: |
489 | - return HTTPConflict() |
490 | + return HTTPConflict("An image with identifier %s already exists" |
491 | + % image_meta['id'], request=req) |
492 | except exception.Invalid: |
493 | return HTTPBadRequest() |
494 | |
495 | @@ -275,6 +289,25 @@ |
496 | request=request, |
497 | content_type='text/plain') |
498 | |
499 | + def get_store_or_400(self, request, store_name): |
500 | + """ |
501 | + Grabs the storage backend for the supplied store name |
502 | + or raises an HTTPBadRequest (400) response |
503 | + |
504 | + :param request: The WSGI/Webob Request object |
505 | + :param id: The opaque image identifier |
506 | + |
507 | + :raises HTTPNotFound if image does not exist |
508 | + """ |
509 | + try: |
510 | + return get_backend_class(store_name) |
511 | + except UnsupportedBackend: |
512 | + raise HTTPBadRequest(body='Requested store %s not available ' |
513 | + 'for storage on this Glance node' |
514 | + % store_name, |
515 | + request=request, |
516 | + content_type='text/plain') |
517 | + |
518 | |
519 | class API(wsgi.Router): |
520 | |
521 | |
522 | === modified file 'glance/store/__init__.py' |
523 | --- glance/store/__init__.py 2010-12-14 19:34:04 +0000 |
524 | +++ glance/store/__init__.py 2010-12-21 15:27:59 +0000 |
525 | @@ -66,7 +66,7 @@ |
526 | try: |
527 | return BACKENDS[backend] |
528 | except KeyError: |
529 | - raise UnsupportedBackend("No backend found for '%s'" % scheme) |
530 | + raise UnsupportedBackend("No backend found for '%s'" % backend) |
531 | |
532 | |
533 | def get_from_backend(uri, **kwargs): |
534 | |
535 | === modified file 'glance/store/filesystem.py' |
536 | --- glance/store/filesystem.py 2010-12-16 22:04:58 +0000 |
537 | +++ glance/store/filesystem.py 2010-12-21 15:27:59 +0000 |
538 | @@ -26,6 +26,11 @@ |
539 | from glance.common import flags |
540 | import glance.store |
541 | |
542 | + |
543 | +flags.DEFINE_string('filesystem_store_datadir', '/var/lib/glance/images/', |
544 | + 'Location to write image data. ' |
545 | + 'Default: /var/lib/glance/images/') |
546 | + |
547 | FLAGS = flags.FLAGS |
548 | |
549 | |
550 | @@ -94,4 +99,35 @@ |
551 | except OSError: |
552 | raise exception.NotAuthorized("You cannot delete file %s" % fn) |
553 | else: |
554 | - raise exception.NotFound("Image file %s does not exist" % fn) |
555 | + raise exception.NotFound("Image file %s does not exist" % fn) |
556 | + |
557 | + @classmethod |
558 | + def add(cls, id, data): |
559 | + """ |
560 | + Stores image data to disk and returns a location that the image was |
561 | + written to. By default, the backend writes the image data to a file |
562 | + `/<DATADIR>/<ID>`, where <DATADIR> is the value of |
563 | + FLAGS.filesystem_store_datadir and <ID> is the supplied image ID. |
564 | + |
565 | + :param id: The opaque image identifier |
566 | + :param data: The image data to write |
567 | + |
568 | + :retval The location that was written, with file:// scheme prepended |
569 | + """ |
570 | + |
571 | + datadir = FLAGS.filesystem_store_datadir |
572 | + |
573 | + if not os.path.exists(datadir): |
574 | + os.makedirs(datadir) |
575 | + |
576 | + filepath = os.path.join(datadir, str(id)) |
577 | + |
578 | + if os.path.exists(filepath): |
579 | + raise exception.Duplicate("Image file %s already exists!" |
580 | + % filepath) |
581 | + |
582 | + f = open(filepath, 'wb') |
583 | + f.write(data) |
584 | + f.close() |
585 | + |
586 | + return 'file://%s' % filepath |
587 | |
588 | === modified file 'tests/stubs.py' |
589 | --- tests/stubs.py 2010-12-18 18:00:21 +0000 |
590 | +++ tests/stubs.py 2010-12-21 15:27:59 +0000 |
591 | @@ -37,7 +37,7 @@ |
592 | import glance.registry.db.sqlalchemy.api |
593 | |
594 | |
595 | -FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests') |
596 | +FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests') |
597 | |
598 | |
599 | def stub_out_http_backend(stubs): |
600 | @@ -356,6 +356,22 @@ |
601 | return values |
602 | |
603 | def image_update(self, _context, image_id, values): |
604 | + |
605 | + props = [] |
606 | + |
607 | + if 'properties' in values.keys(): |
608 | + for k,v in values['properties'].iteritems(): |
609 | + p = {} |
610 | + p['key'] = k |
611 | + p['value'] = v |
612 | + p['deleted'] = False |
613 | + p['created_at'] = datetime.datetime.utcnow() |
614 | + p['updated_at'] = datetime.datetime.utcnow() |
615 | + p['deleted_at'] = None |
616 | + props.append(p) |
617 | + |
618 | + values['properties'] = props |
619 | + |
620 | image = self.image_get(_context, image_id) |
621 | image.update(values) |
622 | return image |
623 | |
624 | === modified file 'tests/unit/test_api.py' |
625 | --- tests/unit/test_api.py 2010-12-18 18:00:21 +0000 |
626 | +++ tests/unit/test_api.py 2010-12-21 15:27:59 +0000 |
627 | @@ -22,11 +22,14 @@ |
628 | import webob |
629 | |
630 | from glance import server |
631 | +from glance.common import flags |
632 | from glance.registry import server as rserver |
633 | from tests import stubs |
634 | |
635 | - |
636 | -class TestImageController(unittest.TestCase): |
637 | +FLAGS = flags.FLAGS |
638 | + |
639 | + |
640 | +class TestRegistryAPI(unittest.TestCase): |
641 | def setUp(self): |
642 | """Establish a clean test environment""" |
643 | self.stubs = stubout.StubOutForTesting() |
644 | @@ -230,7 +233,71 @@ |
645 | self.assertEquals(res.status_int, |
646 | webob.exc.HTTPNotFound.code) |
647 | |
648 | + |
649 | +class TestGlanceAPI(unittest.TestCase): |
650 | + def setUp(self): |
651 | + """Establish a clean test environment""" |
652 | + self.stubs = stubout.StubOutForTesting() |
653 | + stubs.stub_out_registry_and_store_server(self.stubs) |
654 | + stubs.stub_out_registry_db_image_api(self.stubs) |
655 | + stubs.stub_out_filesystem_backend() |
656 | + self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir |
657 | + FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR |
658 | + |
659 | + def tearDown(self): |
660 | + """Clear the test environment""" |
661 | + FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir |
662 | + stubs.clean_out_fake_filesystem_backend() |
663 | + self.stubs.UnsetAll() |
664 | + |
665 | + def test_add_image_no_location_no_image_as_body(self): |
666 | + """Tests raises BadRequest for no body and no loc header""" |
667 | + fixture_headers = {'x-image-meta-store': 'file', |
668 | + 'x-image-meta-name': 'fake image #3'} |
669 | + |
670 | + req = webob.Request.blank("/images") |
671 | + req.method = 'POST' |
672 | + for k,v in fixture_headers.iteritems(): |
673 | + req.headers[k] = v |
674 | + res = req.get_response(server.API()) |
675 | + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) |
676 | + |
677 | + def test_add_image_bad_store(self): |
678 | + """Tests raises BadRequest for invalid store header""" |
679 | + fixture_headers = {'x-image-meta-store': 'bad', |
680 | + 'x-image-meta-name': 'fake image #3'} |
681 | + |
682 | + req = webob.Request.blank("/images") |
683 | + req.method = 'POST' |
684 | + for k,v in fixture_headers.iteritems(): |
685 | + req.headers[k] = v |
686 | + |
687 | + req.headers['Content-Type'] = 'application/octet-stream' |
688 | + req.body = "chunk00000remainder" |
689 | + res = req.get_response(server.API()) |
690 | + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) |
691 | + |
692 | + def test_add_image_basic_file_store(self): |
693 | + """Tests raises BadRequest for invalid store header""" |
694 | + fixture_headers = {'x-image-meta-store': 'file', |
695 | + 'x-image-meta-name': 'fake image #3'} |
696 | + |
697 | + req = webob.Request.blank("/images") |
698 | + req.method = 'POST' |
699 | + for k,v in fixture_headers.iteritems(): |
700 | + req.headers[k] = v |
701 | + |
702 | + req.headers['Content-Type'] = 'application/octet-stream' |
703 | + req.body = "chunk00000remainder" |
704 | + res = req.get_response(server.API()) |
705 | + self.assertEquals(res.status_int, 200) |
706 | + |
707 | + res_body = json.loads(res.body)['image'] |
708 | + self.assertEquals(res_body['location'], |
709 | + 'file:///tmp/glance-tests/3') |
710 | + |
711 | def test_image_meta(self): |
712 | + """Test for HEAD /images/<ID>""" |
713 | expected_headers = {'x-image-meta-id': 2, |
714 | 'x-image-meta-name': 'fake image #2'} |
715 | req = webob.Request.blank("/images/2") |
716 | |
717 | === modified file 'tests/unit/test_clients.py' |
718 | --- tests/unit/test_clients.py 2010-12-16 22:04:58 +0000 |
719 | +++ tests/unit/test_clients.py 2010-12-21 15:27:59 +0000 |
720 | @@ -16,6 +16,7 @@ |
721 | # under the License. |
722 | |
723 | import json |
724 | +import os |
725 | import stubout |
726 | import StringIO |
727 | import unittest |
728 | @@ -24,9 +25,12 @@ |
729 | |
730 | from glance import client |
731 | from glance.registry import client as rclient |
732 | +from glance.common import flags |
733 | from glance.common import exception |
734 | from tests import stubs |
735 | |
736 | +FLAGS = flags.FLAGS |
737 | + |
738 | |
739 | class TestBadClients(unittest.TestCase): |
740 | |
741 | @@ -40,7 +44,7 @@ |
742 | 1) |
743 | |
744 | def test_bad_address(self): |
745 | - """Test unsupported protocol raised""" |
746 | + """Test ClientConnectionError raised""" |
747 | c = client.Client(address="http://127.999.1.1/") |
748 | self.assertRaises(client.ClientConnectionError, |
749 | c.get_image, |
750 | @@ -212,7 +216,7 @@ |
751 | 'location': "file:///tmp/glance-tests/2", |
752 | } |
753 | |
754 | - self.assertRaises(client.BadInputError, |
755 | + self.assertRaises(exception.BadInputError, |
756 | self.client.add_image, |
757 | fixture) |
758 | |
759 | @@ -279,10 +283,13 @@ |
760 | stubs.stub_out_registry_db_image_api(self.stubs) |
761 | stubs.stub_out_registry_and_store_server(self.stubs) |
762 | stubs.stub_out_filesystem_backend() |
763 | + self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir |
764 | + FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR |
765 | self.client = client.Client() |
766 | |
767 | def tearDown(self): |
768 | """Clear the test environment""" |
769 | + FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir |
770 | stubs.clean_out_fake_filesystem_backend() |
771 | self.stubs.UnsetAll() |
772 | |
773 | @@ -480,6 +487,85 @@ |
774 | |
775 | self.assertEquals(data['status'], 'available') |
776 | |
777 | + def test_add_image_with_image_data_as_string(self): |
778 | + """Tests can add image by passing image data as string""" |
779 | + fixture = {'name': 'fake public image', |
780 | + 'is_public': True, |
781 | + 'type': 'kernel', |
782 | + 'size': 19, |
783 | + 'properties': {'distro': 'Ubuntu 10.04 LTS'} |
784 | + } |
785 | + |
786 | + image_data_fixture = r"chunk0000remainder" |
787 | + |
788 | + new_id = self.client.add_image(fixture, image_data_fixture) |
789 | + |
790 | + self.assertEquals(3, new_id) |
791 | + |
792 | + new_meta, new_image_chunks = self.client.get_image(3) |
793 | + |
794 | + new_image_data = "" |
795 | + for image_chunk in new_image_chunks: |
796 | + new_image_data += image_chunk |
797 | + |
798 | + self.assertEquals(image_data_fixture, new_image_data) |
799 | + for k,v in fixture.iteritems(): |
800 | + self.assertEquals(v, new_meta[k]) |
801 | + |
802 | + def test_add_image_with_image_data_as_file(self): |
803 | + """Tests can add image by passing image data as file""" |
804 | + fixture = {'name': 'fake public image', |
805 | + 'is_public': True, |
806 | + 'type': 'kernel', |
807 | + 'size': 19, |
808 | + 'properties': {'distro': 'Ubuntu 10.04 LTS'} |
809 | + } |
810 | + |
811 | + image_data_fixture = r"chunk0000remainder" |
812 | + |
813 | + tmp_image_filepath = '/tmp/rubbish-image' |
814 | + |
815 | + if os.path.exists(tmp_image_filepath): |
816 | + os.unlink(tmp_image_filepath) |
817 | + |
818 | + tmp_file = open(tmp_image_filepath, 'wb') |
819 | + tmp_file.write(image_data_fixture) |
820 | + tmp_file.close() |
821 | + |
822 | + new_id = self.client.add_image(fixture, open(tmp_image_filepath)) |
823 | + |
824 | + self.assertEquals(3, new_id) |
825 | + |
826 | + if os.path.exists(tmp_image_filepath): |
827 | + os.unlink(tmp_image_filepath) |
828 | + |
829 | + new_meta, new_image_chunks = self.client.get_image(3) |
830 | + |
831 | + new_image_data = "" |
832 | + for image_chunk in new_image_chunks: |
833 | + new_image_data += image_chunk |
834 | + |
835 | + self.assertEquals(image_data_fixture, new_image_data) |
836 | + for k,v in fixture.iteritems(): |
837 | + self.assertEquals(v, new_meta[k]) |
838 | + |
839 | + def test_add_image_with_bad_store(self): |
840 | + """Tests BadRequest raised when supplying bad store name in meta""" |
841 | + fixture = {'name': 'fake public image', |
842 | + 'is_public': True, |
843 | + 'type': 'kernel', |
844 | + 'size': 19, |
845 | + 'store': 'bad', |
846 | + 'properties': {'distro': 'Ubuntu 10.04 LTS'} |
847 | + } |
848 | + |
849 | + image_data_fixture = r"chunk0000remainder" |
850 | + |
851 | + self.assertRaises(exception.BadInputError, |
852 | + self.client.add_image, |
853 | + fixture, |
854 | + image_data_fixture) |
855 | + |
856 | def test_update_image(self): |
857 | """Tests that the /images PUT registry API updates the image""" |
858 | fixture = {'name': 'fake public image #2', |
lgtm