Merge lp:~jaypipes/glance/api-add-image into lp:~hudson-openstack/glance/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
Reviewer Review Type Date Requested Status
Christopher MacGown (community) Approve
Rick Harris Pending
Review via email: mp+44355@code.launchpad.net

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.
Revision history for this message
Christopher MacGown (0x44) wrote :

lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'doc/source/index.rst'
--- doc/source/index.rst 2010-12-16 22:04:58 +0000
+++ doc/source/index.rst 2010-12-21 15:27:59 +0000
@@ -198,6 +198,122 @@
198.. toctree::198.. toctree::
199 :maxdepth: 1199 :maxdepth: 1
200200
201
202Adding a New Virtual Machine Image
203----------------------------------
204
205We have created a new virtual machine image in some way (created a
206"golden image" or snapshotted/backed up an existing image) and we
207wish to do two things:
208
209 * Store the disk image data in Glance
210 * Store metadata about this image in Glance
211
212We can do the above two activities in a single call to the Glance API.
213Assuming, like in the examples above, that a Glance API server is running
214at `glance.openstack.org`, we issue a `POST` request to add an image to
215Glance::
216
217 POST http://glance.openstack.org/images/
218
219The metadata about the image is sent to Glance in HTTP headers. The body
220of the HTTP request to the Glance API will be the MIME-encoded disk
221image data.
222
223
224Adding Image Metadata in HTTP Headers
225*************************************
226
227Glance will view as image metadata any HTTP header that it receives in a
228`POST` request where the header key is prefixed with the strings
229`x-image-meta-` and `x-image-meta-property-`.
230
231The list of metadata headers that Glance accepts are listed below.
232
233 * `x-image-meta-name`
234
235 This header is required. Its value should be the name of the image.
236
237 Note that the name of an image *is not unique to a Glance node*. It
238 would be an unrealistic expectation of users to know all the unique
239 names of all other user's images.
240
241 * `x-image-meta-id`
242
243 This header is optional.
244
245 When present, Glance will use the supplied identifier for the image.
246 If the identifier already exists in that Glance node, then a
247 `409 Conflict` will be returned by Glance.
248
249 When this header is *not* present, Glance will generate an identifier
250 for the image and return this identifier in the response (see below)
251
252 * `x-image-meta-store`
253
254 This header is optional. Valid values are one of `file` or `swift`
255
256 When present, Glance will attempt to store the disk image data in the
257 backing store indicated by the value of the header. If the Glance node
258 does not support the backing store, Glance will return a `400 Bad Request`.
259
260 When not present, Glance will store the disk image data in the backing
261 store that is marked default. See the configuration option `default_store`
262 for more information.
263
264 * `x-image-meta-type`
265
266 This header is required. Valid values are one of `kernel`, `machine`, `raw`,
267 or `ramdisk`.
268
269 * `x-image-meta-size`
270
271 This header is optional.
272
273 When present, Glance assumes that the expected size of the request body
274 will be the value of this header. If the length in bytes of the request
275 body *does not match* the value of this header, Glance will return a
276 `400 Bad Request`.
277
278 When not present, Glance will calculate the image's size based on the size
279 of the request body.
280
281 * `x-image-meta-is_public`
282
283 This header is optional.
284
285 When present, Glance converts the value of the header to a boolean value,
286 so "on, 1, true" are all true values. When true, the image is marked as
287 a public image, meaning that any user may view its metadata and may read
288 the disk image from Glance.
289
290 When not present, the image is assumed to be *not public* and specific to
291 a user.
292
293 * `x-image-meta-property-*`
294
295 When Glance receives any HTTP header whose key begins with the string prefix
296 `x-image-meta-property-`, Glance adds the key and value to a set of custom,
297 free-form image properties stored with the image. The key is the
298 lower-cased string following the prefix `x-image-meta-property-` with dashes
299 and punctuation replaced with underscores.
300
301 For example, if the following HTTP header were sent::
302
303 x-image-meta-property-distro Ubuntu 10.10
304
305 Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the
306 image in Glance.
307
308 There is no limit on the number of free-form key/value attributes that can
309 be attached to the image. However, keep in mind that the 8K limit on the
310 size of all HTTP headers sent in a request will effectively limit the number
311 of image properties.
312
313
314.. toctree::
315 :maxdepth: 1
316
201Image Identifiers317Image Identifiers
202=================318=================
203319
@@ -325,6 +441,129 @@
325.. toctree::441.. toctree::
326 :maxdepth: 1442 :maxdepth: 1
327443
444
445Adding a New Virtual Machine Image
446----------------------------------
447
448We have created a new virtual machine image in some way (created a
449"golden image" or snapshotted/backed up an existing image) and we
450wish to do two things:
451
452 * Store the disk image data in Glance
453 * Store metadata about this image in Glance
454
455We can do the above two activities in a single call to the Glance client.
456Assuming, like in the examples above, that a Glance API server is running
457at `glance.openstack.org`, we issue a call to `glance.client.Client.add_image`.
458
459The method signature is as follows::
460
461 glance.client.Client.add_image(image_meta, image_data=None)
462
463The `image_meta` argument is a mapping containing various image metadata. The
464`image_data` argument is the disk image data.
465
466The list of metadata that `image_meta` can contain are listed below.
467
468 * `name`
469
470 This key/value is required. Its value should be the name of the image.
471
472 Note that the name of an image *is not unique to a Glance node*. It
473 would be an unrealistic expectation of users to know all the unique
474 names of all other user's images.
475
476 * `id`
477
478 This key/value is optional.
479
480 When present, Glance will use the supplied identifier for the image.
481 If the identifier already exists in that Glance node, then a
482 `glance.common.exception.Duplicate` will be raised.
483
484 When this key/value is *not* present, Glance will generate an identifier
485 for the image and return this identifier in the response (see below)
486
487 * `store`
488
489 This key/value is optional. Valid values are one of `file` or `swift`
490
491 When present, Glance will attempt to store the disk image data in the
492 backing store indicated by the value. If the Glance node does not support
493 the backing store, Glance will raise a `glance.common.exception.BadRequest`
494
495 When not present, Glance will store the disk image data in the backing
496 store that is marked default. See the configuration option `default_store`
497 for more information.
498
499 * `type`
500
501 This key/values is required. Valid values are one of `kernel`, `machine`,
502 `raw`, or `ramdisk`.
503
504 * `size`
505
506 This key/value is optional.
507
508 When present, Glance assumes that the expected size of the request body
509 will be the value. If the length in bytes of the request body *does not
510 match* the value, Glance will raise a `glance.common.exception.BadRequest`
511
512 When not present, Glance will calculate the image's size based on the size
513 of the request body.
514
515 * `is_public`
516
517 This key/value is optional.
518
519 When present, Glance converts the value to a boolean value, so "on, 1, true"
520 are all true values. When true, the image is marked as a public image,
521 meaning that any user may view its metadata and may read the disk image from
522 Glance.
523
524 When not present, the image is assumed to be *not public* and specific to
525 a user.
526
527 * `properties`
528
529 This key/value is optional.
530
531 When present, the value is assumed to be a mapping of free-form key/value
532 attributes to store with the image.
533
534 For example, if the following is the value of the `properties` key in the
535 `image_meta` argument::
536
537 {'distro': 'Ubuntu 10.10'}
538
539 Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the
540 image in Glance.
541
542 There is no limit on the number of free-form key/value attributes that can
543 be attached to the image with `properties`. However, keep in mind that there
544 is a 8K limit on the size of all HTTP headers sent in a request and this
545 number will effectively limit the number of image properties.
546
547As a complete example, the following code would add a new machine image to
548Glance::
549
550 from glance.client import Client
551
552 c = Client("glance.openstack.org", 9292)
553
554 meta = {'name': 'Ubuntu 10.10 5G',
555 'type': 'machine',
556 'is_public': True,
557 'properties': {'distro': 'Ubuntu 10.10'}}
558
559 new_meta = c.add_image(meta, open('/path/to/image.tar.gz'))
560
561 print 'Stored image. Got identifier: %s' % new_meta['id']
562
563
564.. toctree::
565 :maxdepth: 1
566
328Indices and tables567Indices and tables
329==================568==================
330569
331570
=== modified file 'glance/client.py'
--- glance/client.py 2010-12-16 23:19:23 +0000
+++ glance/client.py 2010-12-21 15:27:59 +0000
@@ -46,11 +46,6 @@
46 pass46 pass
4747
4848
49class BadInputError(Exception):
50 """Error resulting from a client sending bad input to a server"""
51 pass
52
53
54class ImageBodyIterator(object):49class ImageBodyIterator(object):
5550
56 """51 """
@@ -150,7 +145,7 @@
150 elif status_code == httplib.CONFLICT:145 elif status_code == httplib.CONFLICT:
151 raise exception.Duplicate146 raise exception.Duplicate
152 elif status_code == httplib.BAD_REQUEST:147 elif status_code == httplib.BAD_REQUEST:
153 raise BadInputError148 raise exception.BadInputError
154 else:149 else:
155 raise Exception("Unknown error occurred! %d" % status_code)150 raise Exception("Unknown error occurred! %d" % status_code)
156151
@@ -235,17 +230,33 @@
235 """230 """
236 Tells Glance about an image's metadata as well231 Tells Glance about an image's metadata as well
237 as optionally the image_data itself232 as optionally the image_data itself
233
234 :param image_meta: Mapping of information about the
235 image
236 :param image_data: Optional string of raw image data
237 or file-like object that can be
238 used to read the image data
239
240 :retval The newly-stored image's metadata.
238 """241 """
239 if not image_data and 'location' not in image_meta.keys():242 if not image_data and 'location' not in image_meta.keys():
240 raise exception.Invalid("You must either specify a location "243 raise exception.Invalid("You must either specify a location "
241 "for the image or supply the actual "244 "for the image or supply the actual "
242 "image data when adding an image to "245 "image data when adding an image to "
243 "Glance")246 "Glance")
244 body = image_data
245 headers = util.image_meta_to_http_headers(image_meta)247 headers = util.image_meta_to_http_headers(image_meta)
248 if image_data:
249 if hasattr(image_data, 'read'):
250 # TODO(jaypipes): This is far from efficient. Implement
251 # chunked transfer encoding if size is not in image_meta
252 body = image_data.read()
253 else:
254 body = image_data
255 headers['content-type'] = 'application/octet-stream'
256 else:
257 body = None
246 258
247 res = self.do_request("POST", "/images", body, headers)259 res = self.do_request("POST", "/images", body, headers)
248 # Registry returns a JSONified dict(image=image_info)
249 data = json.loads(res.read())260 data = json.loads(res.read())
250 return data['image']['id']261 return data['image']['id']
251262
252263
=== modified file 'glance/common/exception.py'
--- glance/common/exception.py 2010-11-23 18:12:11 +0000
+++ glance/common/exception.py 2010-12-21 15:27:59 +0000
@@ -70,6 +70,11 @@
70 pass70 pass
7171
7272
73class BadInputError(Exception):
74 """Error resulting from a client sending bad input to a server"""
75 pass
76
77
73def wrap_exception(f):78def wrap_exception(f):
74 def _wrap(*args, **kw):79 def _wrap(*args, **kw):
75 try:80 try:
7681
=== modified file 'glance/common/flags.py'
--- glance/common/flags.py 2010-09-29 00:20:11 +0000
+++ glance/common/flags.py 2010-12-21 15:27:59 +0000
@@ -173,3 +173,5 @@
173 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"),173 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"),
174 'connection string for sql database')174 'connection string for sql database')
175DEFINE_bool('verbose', False, 'show debug output')175DEFINE_bool('verbose', False, 'show debug output')
176DEFINE_string('default_store', 'file',
177 'Default storage backend. Default: "file"')
176178
=== modified file 'glance/registry/db/sqlalchemy/api.py'
--- glance/registry/db/sqlalchemy/api.py 2010-12-18 18:00:21 +0000
+++ glance/registry/db/sqlalchemy/api.py 2010-12-21 15:27:59 +0000
@@ -52,10 +52,19 @@
5252
5353
54def image_create(_context, values):54def image_create(_context, values):
55 values['size'] = int(values['size'])
56 values['is_public'] = bool(values.get('is_public', False))
57 properties = values.pop('properties', {})
58
55 image_ref = models.Image()59 image_ref = models.Image()
56 image_ref.update(values)60 image_ref.update(values)
57 image_ref.save()61 image_ref.save()
58 return image_ref62
63 for key, value in properties.iteritems():
64 prop_values = {'image_id': image_ref.id, 'key': key, 'value': value}
65 image_property_create(_context, prop_values)
66
67 return image_get(_context, image_ref.id)
5968
6069
61def image_destroy(_context, image_id):70def image_destroy(_context, image_id):
@@ -102,18 +111,25 @@
102def image_update(_context, image_id, values):111def image_update(_context, image_id, values):
103 session = get_session()112 session = get_session()
104 with session.begin():113 with session.begin():
114 values['size'] = int(values['size'])
115 values['is_public'] = bool(values.get('is_public', False))
116 properties = values.pop('properties', {})
117
105 image_ref = models.Image.find(image_id, session=session)118 image_ref = models.Image.find(image_id, session=session)
106 image_ref.update(values)119 image_ref.update(values)
107 image_ref.save(session=session)120 image_ref.save(session=session)
108121
122 for key, value in properties.iteritems():
123 prop_values = {'image_id': image_ref.id, 'key': key, 'value': value}
124 image_property_create(_context, prop_values)
125
109126
110###################127###################
111128
112129
113def image_file_create(_context, values):130def image_file_create(_context, values):
114 image_file_ref = models.ImageFile()131 image_file_ref = models.ImageFile()
115 for (key, value) in values.iteritems():132 image_file_ref.update(values)
116 image_file_ref[key] = value
117 image_file_ref.save()133 image_file_ref.save()
118 return image_file_ref134 return image_file_ref
119135
@@ -122,8 +138,7 @@
122138
123139
124def image_property_create(_context, values):140def image_property_create(_context, values):
125 image_properties_ref = models.ImageProperty()141 image_property_ref = models.ImageProperty()
126 for (key, value) in values.iteritems():142 image_property_ref.update(values)
127 image_properties_ref[key] = value143 image_property_ref.save()
128 image_properties_ref.save()144 return image_property_ref
129 return image_properties_ref
130145
=== modified file 'glance/server.py'
--- glance/server.py 2010-12-16 22:04:58 +0000
+++ glance/server.py 2010-12-21 15:27:59 +0000
@@ -23,6 +23,10 @@
23Configuration Options23Configuration Options
24---------------------24---------------------
2525
26 `default_store`: When no x-image-meta-store header is sent for a
27 `POST /images` request, this store will be used
28 for storing the image data. Default: 'file'
29
26"""30"""
2731
28import json32import json
@@ -39,7 +43,9 @@
39from glance.common import wsgi43from glance.common import wsgi
40from glance.store import (get_from_backend,44from glance.store import (get_from_backend,
41 delete_from_backend,45 delete_from_backend,
42 get_store_from_location)46 get_store_from_location,
47 get_backend_class,
48 UnsupportedBackend)
43from glance import registry49from glance import registry
44from glance import util50from glance import util
4551
@@ -174,9 +180,6 @@
174180
175 :param request: The WSGI/Webob Request object181 :param request: The WSGI/Webob Request object
176182
177 :see The `id_type` configuration option (default: uuid) determines
178 the type of identifier that Glance generates for an image
179
180 :raises HTTPBadRequest if no x-image-meta-location is missing183 :raises HTTPBadRequest if no x-image-meta-location is missing
181 and the request body is not application/octet-stream184 and the request body is not application/octet-stream
182 image data.185 image data.
@@ -195,7 +198,7 @@
195 "mime-encoded as application/"198 "mime-encoded as application/"
196 "octet-stream.", request=req)199 "octet-stream.", request=req)
197 else:200 else:
198 if 'x-image-meta-store' in headers_keys:201 if 'x-image-meta-store' in header_keys:
199 image_store = req.headers['x-image-meta-store']202 image_store = req.headers['x-image-meta-store']
200 image_status = 'pending' # set to available when stored...203 image_status = 'pending' # set to available when stored...
201 image_in_body = True204 image_in_body = True
@@ -204,23 +207,34 @@
204 image_store = get_store_from_location(image_location)207 image_store = get_store_from_location(image_location)
205 image_status = 'available'208 image_status = 'available'
206209
210 # If image is the request body, validate that the requested
211 # or default store is capable of storing the image data...
212 if not image_store:
213 image_store = FLAGS.default_store
214 if image_in_body:
215 store = self.get_store_or_400(req, image_store)
216
207 image_meta = util.get_image_meta_from_headers(req)217 image_meta = util.get_image_meta_from_headers(req)
218
208 image_meta['status'] = image_status219 image_meta['status'] = image_status
209 image_meta['store'] = image_store220 image_meta['store'] = image_store
210
211 try:221 try:
212 image_meta = registry.add_image_metadata(image_meta)222 image_meta = registry.add_image_metadata(image_meta)
213223
214 if image_in_body:224 if image_in_body:
215 #store = stores.get_store()225 try:
216 #store.add_image(req.body)226 location = store.add(image_meta['id'], req.body)
227 except exception.Duplicate, e:
228 return HTTPConflict(str(e), request=req)
217 image_meta['status'] = 'available'229 image_meta['status'] = 'available'
218 registries.update_image(image_meta)230 image_meta['location'] = location
231 registry.update_image_metadata(image_meta['id'], image_meta)
219232
220 return dict(image=image_meta)233 return dict(image=image_meta)
221234
222 except exception.Duplicate:235 except exception.Duplicate:
223 return HTTPConflict()236 return HTTPConflict("An image with identifier %s already exists"
237 % image_meta['id'], request=req)
224 except exception.Invalid:238 except exception.Invalid:
225 return HTTPBadRequest()239 return HTTPBadRequest()
226240
@@ -275,6 +289,25 @@
275 request=request,289 request=request,
276 content_type='text/plain')290 content_type='text/plain')
277291
292 def get_store_or_400(self, request, store_name):
293 """
294 Grabs the storage backend for the supplied store name
295 or raises an HTTPBadRequest (400) response
296
297 :param request: The WSGI/Webob Request object
298 :param id: The opaque image identifier
299
300 :raises HTTPNotFound if image does not exist
301 """
302 try:
303 return get_backend_class(store_name)
304 except UnsupportedBackend:
305 raise HTTPBadRequest(body='Requested store %s not available '
306 'for storage on this Glance node'
307 % store_name,
308 request=request,
309 content_type='text/plain')
310
278311
279class API(wsgi.Router):312class API(wsgi.Router):
280313
281314
=== modified file 'glance/store/__init__.py'
--- glance/store/__init__.py 2010-12-14 19:34:04 +0000
+++ glance/store/__init__.py 2010-12-21 15:27:59 +0000
@@ -66,7 +66,7 @@
66 try:66 try:
67 return BACKENDS[backend]67 return BACKENDS[backend]
68 except KeyError:68 except KeyError:
69 raise UnsupportedBackend("No backend found for '%s'" % scheme)69 raise UnsupportedBackend("No backend found for '%s'" % backend)
7070
7171
72def get_from_backend(uri, **kwargs):72def get_from_backend(uri, **kwargs):
7373
=== modified file 'glance/store/filesystem.py'
--- glance/store/filesystem.py 2010-12-16 22:04:58 +0000
+++ glance/store/filesystem.py 2010-12-21 15:27:59 +0000
@@ -26,6 +26,11 @@
26from glance.common import flags26from glance.common import flags
27import glance.store27import glance.store
2828
29
30flags.DEFINE_string('filesystem_store_datadir', '/var/lib/glance/images/',
31 'Location to write image data. '
32 'Default: /var/lib/glance/images/')
33
29FLAGS = flags.FLAGS34FLAGS = flags.FLAGS
3035
3136
@@ -94,4 +99,35 @@
94 except OSError:99 except OSError:
95 raise exception.NotAuthorized("You cannot delete file %s" % fn)100 raise exception.NotAuthorized("You cannot delete file %s" % fn)
96 else:101 else:
97 raise exception.NotFound("Image file %s does not exist" % fn) 102 raise exception.NotFound("Image file %s does not exist" % fn)
103
104 @classmethod
105 def add(cls, id, data):
106 """
107 Stores image data to disk and returns a location that the image was
108 written to. By default, the backend writes the image data to a file
109 `/<DATADIR>/<ID>`, where <DATADIR> is the value of
110 FLAGS.filesystem_store_datadir and <ID> is the supplied image ID.
111
112 :param id: The opaque image identifier
113 :param data: The image data to write
114
115 :retval The location that was written, with file:// scheme prepended
116 """
117
118 datadir = FLAGS.filesystem_store_datadir
119
120 if not os.path.exists(datadir):
121 os.makedirs(datadir)
122
123 filepath = os.path.join(datadir, str(id))
124
125 if os.path.exists(filepath):
126 raise exception.Duplicate("Image file %s already exists!"
127 % filepath)
128
129 f = open(filepath, 'wb')
130 f.write(data)
131 f.close()
132
133 return 'file://%s' % filepath
98134
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2010-12-18 18:00:21 +0000
+++ tests/stubs.py 2010-12-21 15:27:59 +0000
@@ -37,7 +37,7 @@
37import glance.registry.db.sqlalchemy.api37import glance.registry.db.sqlalchemy.api
3838
3939
40FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests')40FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
4141
4242
43def stub_out_http_backend(stubs):43def stub_out_http_backend(stubs):
@@ -356,6 +356,22 @@
356 return values356 return values
357357
358 def image_update(self, _context, image_id, values):358 def image_update(self, _context, image_id, values):
359
360 props = []
361
362 if 'properties' in values.keys():
363 for k,v in values['properties'].iteritems():
364 p = {}
365 p['key'] = k
366 p['value'] = v
367 p['deleted'] = False
368 p['created_at'] = datetime.datetime.utcnow()
369 p['updated_at'] = datetime.datetime.utcnow()
370 p['deleted_at'] = None
371 props.append(p)
372
373 values['properties'] = props
374
359 image = self.image_get(_context, image_id)375 image = self.image_get(_context, image_id)
360 image.update(values)376 image.update(values)
361 return image377 return image
362378
=== modified file 'tests/unit/test_api.py'
--- tests/unit/test_api.py 2010-12-18 18:00:21 +0000
+++ tests/unit/test_api.py 2010-12-21 15:27:59 +0000
@@ -22,11 +22,14 @@
22import webob22import webob
2323
24from glance import server24from glance import server
25from glance.common import flags
25from glance.registry import server as rserver26from glance.registry import server as rserver
26from tests import stubs27from tests import stubs
2728
2829FLAGS = flags.FLAGS
29class TestImageController(unittest.TestCase):30
31
32class TestRegistryAPI(unittest.TestCase):
30 def setUp(self):33 def setUp(self):
31 """Establish a clean test environment"""34 """Establish a clean test environment"""
32 self.stubs = stubout.StubOutForTesting()35 self.stubs = stubout.StubOutForTesting()
@@ -230,7 +233,71 @@
230 self.assertEquals(res.status_int,233 self.assertEquals(res.status_int,
231 webob.exc.HTTPNotFound.code)234 webob.exc.HTTPNotFound.code)
232235
236
237class TestGlanceAPI(unittest.TestCase):
238 def setUp(self):
239 """Establish a clean test environment"""
240 self.stubs = stubout.StubOutForTesting()
241 stubs.stub_out_registry_and_store_server(self.stubs)
242 stubs.stub_out_registry_db_image_api(self.stubs)
243 stubs.stub_out_filesystem_backend()
244 self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
245 FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
246
247 def tearDown(self):
248 """Clear the test environment"""
249 FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
250 stubs.clean_out_fake_filesystem_backend()
251 self.stubs.UnsetAll()
252
253 def test_add_image_no_location_no_image_as_body(self):
254 """Tests raises BadRequest for no body and no loc header"""
255 fixture_headers = {'x-image-meta-store': 'file',
256 'x-image-meta-name': 'fake image #3'}
257
258 req = webob.Request.blank("/images")
259 req.method = 'POST'
260 for k,v in fixture_headers.iteritems():
261 req.headers[k] = v
262 res = req.get_response(server.API())
263 self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
264
265 def test_add_image_bad_store(self):
266 """Tests raises BadRequest for invalid store header"""
267 fixture_headers = {'x-image-meta-store': 'bad',
268 'x-image-meta-name': 'fake image #3'}
269
270 req = webob.Request.blank("/images")
271 req.method = 'POST'
272 for k,v in fixture_headers.iteritems():
273 req.headers[k] = v
274
275 req.headers['Content-Type'] = 'application/octet-stream'
276 req.body = "chunk00000remainder"
277 res = req.get_response(server.API())
278 self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
279
280 def test_add_image_basic_file_store(self):
281 """Tests raises BadRequest for invalid store header"""
282 fixture_headers = {'x-image-meta-store': 'file',
283 'x-image-meta-name': 'fake image #3'}
284
285 req = webob.Request.blank("/images")
286 req.method = 'POST'
287 for k,v in fixture_headers.iteritems():
288 req.headers[k] = v
289
290 req.headers['Content-Type'] = 'application/octet-stream'
291 req.body = "chunk00000remainder"
292 res = req.get_response(server.API())
293 self.assertEquals(res.status_int, 200)
294
295 res_body = json.loads(res.body)['image']
296 self.assertEquals(res_body['location'],
297 'file:///tmp/glance-tests/3')
298
233 def test_image_meta(self):299 def test_image_meta(self):
300 """Test for HEAD /images/<ID>"""
234 expected_headers = {'x-image-meta-id': 2,301 expected_headers = {'x-image-meta-id': 2,
235 'x-image-meta-name': 'fake image #2'}302 'x-image-meta-name': 'fake image #2'}
236 req = webob.Request.blank("/images/2")303 req = webob.Request.blank("/images/2")
237304
=== modified file 'tests/unit/test_clients.py'
--- tests/unit/test_clients.py 2010-12-16 22:04:58 +0000
+++ tests/unit/test_clients.py 2010-12-21 15:27:59 +0000
@@ -16,6 +16,7 @@
16# under the License.16# under the License.
1717
18import json18import json
19import os
19import stubout20import stubout
20import StringIO21import StringIO
21import unittest22import unittest
@@ -24,9 +25,12 @@
2425
25from glance import client26from glance import client
26from glance.registry import client as rclient27from glance.registry import client as rclient
28from glance.common import flags
27from glance.common import exception29from glance.common import exception
28from tests import stubs30from tests import stubs
2931
32FLAGS = flags.FLAGS
33
3034
31class TestBadClients(unittest.TestCase):35class TestBadClients(unittest.TestCase):
32 36
@@ -40,7 +44,7 @@
40 1)44 1)
4145
42 def test_bad_address(self):46 def test_bad_address(self):
43 """Test unsupported protocol raised"""47 """Test ClientConnectionError raised"""
44 c = client.Client(address="http://127.999.1.1/")48 c = client.Client(address="http://127.999.1.1/")
45 self.assertRaises(client.ClientConnectionError,49 self.assertRaises(client.ClientConnectionError,
46 c.get_image,50 c.get_image,
@@ -212,7 +216,7 @@
212 'location': "file:///tmp/glance-tests/2",216 'location': "file:///tmp/glance-tests/2",
213 }217 }
214218
215 self.assertRaises(client.BadInputError,219 self.assertRaises(exception.BadInputError,
216 self.client.add_image,220 self.client.add_image,
217 fixture)221 fixture)
218222
@@ -279,10 +283,13 @@
279 stubs.stub_out_registry_db_image_api(self.stubs)283 stubs.stub_out_registry_db_image_api(self.stubs)
280 stubs.stub_out_registry_and_store_server(self.stubs)284 stubs.stub_out_registry_and_store_server(self.stubs)
281 stubs.stub_out_filesystem_backend()285 stubs.stub_out_filesystem_backend()
286 self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
287 FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
282 self.client = client.Client()288 self.client = client.Client()
283289
284 def tearDown(self):290 def tearDown(self):
285 """Clear the test environment"""291 """Clear the test environment"""
292 FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
286 stubs.clean_out_fake_filesystem_backend()293 stubs.clean_out_fake_filesystem_backend()
287 self.stubs.UnsetAll()294 self.stubs.UnsetAll()
288295
@@ -480,6 +487,85 @@
480487
481 self.assertEquals(data['status'], 'available')488 self.assertEquals(data['status'], 'available')
482489
490 def test_add_image_with_image_data_as_string(self):
491 """Tests can add image by passing image data as string"""
492 fixture = {'name': 'fake public image',
493 'is_public': True,
494 'type': 'kernel',
495 'size': 19,
496 'properties': {'distro': 'Ubuntu 10.04 LTS'}
497 }
498
499 image_data_fixture = r"chunk0000remainder"
500
501 new_id = self.client.add_image(fixture, image_data_fixture)
502
503 self.assertEquals(3, new_id)
504
505 new_meta, new_image_chunks = self.client.get_image(3)
506
507 new_image_data = ""
508 for image_chunk in new_image_chunks:
509 new_image_data += image_chunk
510
511 self.assertEquals(image_data_fixture, new_image_data)
512 for k,v in fixture.iteritems():
513 self.assertEquals(v, new_meta[k])
514
515 def test_add_image_with_image_data_as_file(self):
516 """Tests can add image by passing image data as file"""
517 fixture = {'name': 'fake public image',
518 'is_public': True,
519 'type': 'kernel',
520 'size': 19,
521 'properties': {'distro': 'Ubuntu 10.04 LTS'}
522 }
523
524 image_data_fixture = r"chunk0000remainder"
525
526 tmp_image_filepath = '/tmp/rubbish-image'
527
528 if os.path.exists(tmp_image_filepath):
529 os.unlink(tmp_image_filepath)
530
531 tmp_file = open(tmp_image_filepath, 'wb')
532 tmp_file.write(image_data_fixture)
533 tmp_file.close()
534
535 new_id = self.client.add_image(fixture, open(tmp_image_filepath))
536
537 self.assertEquals(3, new_id)
538
539 if os.path.exists(tmp_image_filepath):
540 os.unlink(tmp_image_filepath)
541
542 new_meta, new_image_chunks = self.client.get_image(3)
543
544 new_image_data = ""
545 for image_chunk in new_image_chunks:
546 new_image_data += image_chunk
547
548 self.assertEquals(image_data_fixture, new_image_data)
549 for k,v in fixture.iteritems():
550 self.assertEquals(v, new_meta[k])
551
552 def test_add_image_with_bad_store(self):
553 """Tests BadRequest raised when supplying bad store name in meta"""
554 fixture = {'name': 'fake public image',
555 'is_public': True,
556 'type': 'kernel',
557 'size': 19,
558 'store': 'bad',
559 'properties': {'distro': 'Ubuntu 10.04 LTS'}
560 }
561
562 image_data_fixture = r"chunk0000remainder"
563
564 self.assertRaises(exception.BadInputError,
565 self.client.add_image,
566 fixture,
567 image_data_fixture)
568
483 def test_update_image(self):569 def test_update_image(self):
484 """Tests that the /images PUT registry API updates the image"""570 """Tests that the /images PUT registry API updates the image"""
485 fixture = {'name': 'fake public image #2',571 fixture = {'name': 'fake public image #2',

Subscribers

People subscribed via source and target branches