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
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',

Subscribers

People subscribed via source and target branches