From b87b1b5086b0351c06865ef7b6e9e6057fead0b6 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 3 Apr 2012 17:01:48 -0700 Subject: [PATCH] Adding support for passing image data through cli --- glanceclient/common/http.py | 6 ++--- glanceclient/v1/images.py | 48 +++++++++++++++++++++++++++++++++++-- glanceclient/v1/shell.py | 15 ++++++++++-- tests/v1/test_images.py | 10 +++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py index 01463ce1..4d1d7eb2 100644 --- a/glanceclient/common/http.py +++ b/glanceclient/common/http.py @@ -71,9 +71,9 @@ class HTTPClient(httplib2.Http): _kwargs = copy.copy(kwargs) _kwargs.setdefault('headers', kwargs.get('headers', {})) _kwargs['headers']['User-Agent'] = USER_AGENT - if 'body' in kwargs: - _kwargs['headers']['Content-Type'] = 'application/json' - _kwargs['body'] = json.dumps(kwargs['body']) + if 'body' in kwargs and kwargs['body'] is not None: + _kwargs['headers']['Content-Type'] = 'application/octet-stream' + _kwargs['body'] = kwargs['body'] resp, body = super(HTTPClient, self).request(url, method, **_kwargs) self.http_log((url, method,), _kwargs, resp, body) diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py index 7af68ce3..c2800ca1 100644 --- a/glanceclient/v1/images.py +++ b/glanceclient/v1/images.py @@ -14,6 +14,8 @@ # under the License. import copy +import errno +import os import urllib from glanceclient.common import base @@ -88,12 +90,42 @@ class ImageManager(base.Manager): """Delete an image.""" self._delete("/v1/images/%s" % base.getid(image)) + def _get_file_size(self, obj): + """Analyze file-like object and attempt to determine its size. + + :param obj: file-like object, typically redirected from stdin. + :retval The file's size or None if it cannot be determined. + """ + # For large images, we need to supply the size of the + # image file. See LP Bugs #827660 and #845788. + if hasattr(obj, 'seek') and hasattr(obj, 'tell'): + try: + obj.seek(0, os.SEEK_END) + obj_size = obj.tell() + obj.seek(0) + return obj_size + except IOError, e: + if e.errno == errno.ESPIPE: + # Illegal seek. This means the user is trying + # to pipe image data to the client, e.g. + # echo testdata | bin/glance add blah..., or + # that stdin is empty + return None + else: + raise + def create(self, **kwargs): """Create an image TODO(bcwaldon): document accepted params """ image_data = kwargs.pop('data', None) + if image_data is not None: + image_size = self._get_file_size(image_data) + if image_size != 0: + kwargs.setdefault('size', image_size) + else: + image_data = None fields = {} for field in kwargs: @@ -112,7 +144,18 @@ class ImageManager(base.Manager): return Image(self, body['image']) def update(self, image, **kwargs): - """Update an image""" + """Update an image + + TODO(bcwaldon): document accepted params + """ + image_data = kwargs.pop('data', None) + if image_data is not None: + image_size = self._get_file_size(image_data) + if image_size != 0: + kwargs.setdefault('size', image_size) + else: + image_data = None + fields = {} for field in kwargs: if field in UPDATE_PARAMS: @@ -127,5 +170,6 @@ class ImageManager(base.Manager): hdrs['x-glance-api-copy-from'] = copy_from image_id = base.getid(image) - resp, body = self.api.put('/v1/images/%s' % image_id, headers=hdrs) + resp, body = self.api.put('/v1/images/%s' % image_id, headers=hdrs, + body=image_data) return Image(self, body['image']) diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py index 48302c2f..c79f83ed 100644 --- a/glanceclient/v1/shell.py +++ b/glanceclient/v1/shell.py @@ -14,6 +14,7 @@ # under the License. import copy +import sys from glanceclient.common import utils import glanceclient.v1.images @@ -53,7 +54,8 @@ def do_image_show(gc, args): @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', - help='Size of image data (in bytes).') + help=('Size of image data (in bytes). Only used with' + ' \'--location\' and \'--copy_from\'.')) @utils.arg('--min_disk', metavar='', help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min_ram', metavar='', @@ -92,10 +94,14 @@ def do_image_create(gc, args): CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items())) + if 'location' not in fields and 'copy_from' not in fields: + fields['data'] = sys.stdin + image = gc.images.create(**fields) _image_show(image) +@utils.arg('id', metavar='', help='ID of image to modify.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--disk_format', metavar='', @@ -132,6 +138,8 @@ def do_image_update(gc, args): # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) + image_id = fields.pop('id') + raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: @@ -142,7 +150,10 @@ def do_image_update(gc, args): UPDATE_PARAMS = glanceclient.v1.images.UPDATE_PARAMS fields = dict(filter(lambda x: x[0] in UPDATE_PARAMS, fields.items())) - image = gc.images.create(**fields) + if 'location' not in fields and 'copy_from' not in fields: + fields['data'] = sys.stdin + + image = gc.images.update(image_id, **fields) _image_show(image) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index ab6f57b6..dcec755f 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -86,7 +86,8 @@ class ImageManagerTest(unittest.TestCase): def test_create_with_data(self): image_data = StringIO.StringIO('XXX') self.mgr.create(data=image_data) - expect = [('POST', '/v1/images', {}, image_data)] + expect_headers = {'x-image-meta-size': '3'} + expect = [('POST', '/v1/images', expect_headers, image_data)] self.assertEqual(self.api.calls, expect) def test_update(self): @@ -119,6 +120,13 @@ class ImageManagerTest(unittest.TestCase): self.assertEqual(image.id, '1') self.assertEqual(image.name, 'image-2') + def test_update_with_data(self): + image_data = StringIO.StringIO('XXX') + self.mgr.update('1', data=image_data) + expect_headers = {'x-image-meta-size': '3'} + expect = [('PUT', '/v1/images/1', expect_headers, image_data)] + self.assertEqual(self.api.calls, expect) + class ImageTest(unittest.TestCase): def setUp(self):