diff options
-rw-r--r-- | glanceclient/v1/images.py | 52 | ||||
-rw-r--r-- | glanceclient/v1/shell.py | 64 | ||||
-rw-r--r-- | setup.py | 16 | ||||
-rw-r--r-- | tests/v1/test_images.py | 38 | ||||
-rw-r--r-- | tests/v1/utils.py | 16 |
5 files changed, 142 insertions, 44 deletions
diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py index 44b0aaf..459fde0 100644 --- a/glanceclient/v1/images.py +++ b/glanceclient/v1/images.py @@ -13,10 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import urllib from glanceclient.common import base +CREATE_PARAMS = ['id', 'name', 'disk_format', 'container_format', 'min_disk', + 'min_ram', 'owner', 'size', 'is_public', 'protected', + 'location', 'checksum', 'copy_from', 'properties'] + class Image(base.Resource): def __repr__(self): @@ -45,11 +50,11 @@ class ImageManager(base.Manager): def _image_meta_to_headers(self, fields): headers = {} - for key, value in fields.iteritems(): - if key == 'properties': - headers['x-image-meta-property-%s' % key] = value - else: - headers['x-image-meta-%s' % key] = value + fields_copy = copy.deepcopy(fields) + for key, value in fields_copy.pop('properties', {}).iteritems(): + headers['x-image-meta-property-%s' % key] = str(value) + for key, value in fields_copy.iteritems(): + headers['x-image-meta-%s' % key] = str(value) return headers def get(self, image_id): @@ -73,7 +78,7 @@ class ImageManager(base.Manager): if limit: params['limit'] = int(limit) if marker: - params['marker'] = int(marker) + params['marker'] = marker query = '?%s' % urllib.urlencode(params) if params else '' return self._list('/v1/images/detail%s' % query, "images") @@ -82,13 +87,25 @@ class ImageManager(base.Manager): self._delete("/v1/images/%s" % base.getid(image)) def create(self, **kwargs): - """Create an image""" + """Create an image + + TODO(bcwaldon): document accepted params + """ fields = {} - if 'name' in kwargs: - fields['name'] = kwargs['name'] - resp, body = self.api.post('/v1/images', body={'image': fields}) - meta = self._image_meta_from_headers(resp) - return Image(self, meta) + for field in kwargs: + if field in CREATE_PARAMS: + fields[field] = kwargs[field] + else: + msg = 'create() got an unexpected keyword argument \'%s\'' + raise TypeError(msg % field) + + copy_from = fields.pop('copy_from', None) + hdrs = self._image_meta_to_headers(fields) + if copy_from is not None: + hdrs['x-glance-api-copy-from'] = copy_from + + resp, body = self.api.post('/v1/images', headers=hdrs) + return Image(self, body['image']) def update(self, image, **kwargs): """Update an image""" @@ -100,14 +117,3 @@ class ImageManager(base.Manager): resp, body = self.api.put(url, headers=send_meta) recv_meta = self._image_meta_from_headers(resp) return Image(self, recv_meta) - - def delete_member(self, image, image_member): - """Remove a member from an image""" - image_id = base.getid(image) - try: - member_id = image_member.member_id - except AttributeError: - member_id = image_member - - url = '/v1/images/%s/members/%s' % (image_id, member_id) - resp, body = self.api.delete(url) diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py index 14488ee..be32fff 100644 --- a/glanceclient/v1/shell.py +++ b/glanceclient/v1/shell.py @@ -16,6 +16,7 @@ import copy from glanceclient.common import utils +import glanceclient.v1.images def do_image_list(gc, args): @@ -26,7 +27,7 @@ def do_image_list(gc, args): def _image_show(image): - # Flatten image properties dict + # Flatten image properties dict for display info = copy.deepcopy(image._info) for (k, v) in info.pop('properties').iteritems(): info['Property \'%s\'' % k] = v @@ -41,10 +42,61 @@ def do_image_show(gc, args): _image_show(image) -@utils.arg('--id', metavar='<IMAGE_ID>', help='ID of image to reserve.') -@utils.arg('--name', metavar='<NAME>', help='Name of image.') -@utils.arg('--disk_format', metavar='<CONTAINER_FORMAT>', help='Disk format of image.') -@utils.arg('--container_format', metavar='<DISK_FORMAT>', help='Container format of image.') +@utils.arg('--id', metavar='<IMAGE_ID>', + help='ID of image to reserve.') +@utils.arg('--name', metavar='<NAME>', + help='Name of image.') +@utils.arg('--disk_format', metavar='<CONTAINER_FORMAT>', + help='Disk format of image.') +@utils.arg('--container_format', metavar='<DISK_FORMAT>', + help='Container format of image.') +@utils.arg('--owner', metavar='<TENANT_ID>', + help='Tenant who should own image.') +@utils.arg('--size', metavar='<SIZE>', + help='Size of image data (in bytes).') +@utils.arg('--min_disk', metavar='<DISK_GB>', + help='Minimum size of disk needed to boot image (in gigabytes).') +@utils.arg('--min_ram', metavar='<DISK_RAM>', + help='Minimum amount of ram needed to boot image (in megabytes).') +@utils.arg('--location', metavar='<IMAGE_URL>', + help=('URL where the data for this image already resides.' + ' For example, if the image data is stored in the filesystem' + ' local to the glance server at \'/usr/share/image.tar.gz\',' + ' you would specify \'file:///usr/share/image.tar.gz\'.')) +@utils.arg('--checksum', metavar='<CHECKSUM>', + help='Hash of image data used Glance can use for verification.') +@utils.arg('--copy_from', metavar='<IMAGE_URL>', + help=('Similar to \'--location\' in usage, but this indicates that' + ' the Glance server should immediately copy the data and' + ' store it in its configured image store.')) +@utils.arg('--public', action='store_true', default=False, + help='Make image accessible to the public.') +@utils.arg('--protected', action='store_true', default=False, + help='Prevent image from being deleted.') +@utils.arg('--property', metavar="<key=value>", action='append', default=[], + help=("Arbitrary property to associate with image. " + "May be used multiple times.")) def do_image_create(gc, args): - image = gc.images.create(*args) + # Filter out None values + fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) + + fields['is_public'] = fields.pop('public') + + raw_properties = fields.pop('property') + fields['properties'] = {} + for datum in raw_properties: + key, value = datum.split('=', 1) + fields['properties'][key] = value + + # Filter out values we can't use + CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS + fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items())) + + image = gc.images.create(**fields) _image_show(image) + + +@utils.arg('id', metavar='<IMAGE_ID>', help='ID of image to delete.') +def do_image_delete(gc, args): + """Delete a specific image.""" + gc.images.delete(args.id) @@ -2,15 +2,15 @@ import os import setuptools -from glanceclient.openstack.common.setup import parse_requirements -from glanceclient.openstack.common.setup import parse_dependency_links -from glanceclient.openstack.common.setup import write_requirements +#from glanceclient.openstack.common.setup import parse_requirements +#from glanceclient.openstack.common.setup import parse_dependency_links +#from glanceclient.openstack.common.setup import write_requirements from glanceclient.openstack.common.setup import write_git_changelog -requires = parse_requirements() -dependency_links = parse_dependency_links() -write_requirements() +#requires = parse_requirements() +#dependency_links = parse_dependency_links() +#write_requirements() write_git_changelog() @@ -36,8 +36,8 @@ setuptools.setup( 'Operating System :: OS Independent', 'Programming Language :: Python', ], - install_requires=requires, - dependency_links=dependency_links, +# install_requires=requires, +# dependency_links=dependency_links, test_suite="nose.collector", entry_points={'console_scripts': ['glance = glanceclient.shell:main']}, ) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index 8ade8ab..1c51fab 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -43,12 +43,44 @@ class ImageManagerTest(unittest.TestCase): expect = [('DELETE', '/v1/images/1', {}, None)] self.assertEqual(self.api.calls, expect) - def test_create(self): - image = self.mgr.create(name='image-1') - expect = [('POST', '/v1/images', {}, {'image': {'name': 'image-1'}})] + def test_create_no_data(self): + params = { + 'id': '1', + 'name': 'image-1', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': 1024, + 'min_ram': 512, + 'min_disk': 10, + 'copy_from': 'http://example.com', + 'properties': {'a': 'b', 'c': 'd'}, + } + image = self.mgr.create(**params) + expect_headers = { + 'x-image-meta-id': '1', + 'x-image-meta-name': 'image-1', + 'x-image-meta-container_format': 'ovf', + 'x-image-meta-disk_format': 'vhd', + 'x-image-meta-owner': 'asdf', + 'x-image-meta-size': '1024', + 'x-image-meta-min_ram': '512', + 'x-image-meta-min_disk': '10', + 'x-glance-api-copy-from': 'http://example.com', + 'x-image-meta-property-a': 'b', + 'x-image-meta-property-c': 'd', + } + expect = [('POST', '/v1/images', expect_headers, None)] self.assertEqual(self.api.calls, expect) self.assertEqual(image.id, '1') self.assertEqual(image.name, 'image-1') + self.assertEqual(image.container_format, 'ovf') + self.assertEqual(image.disk_format, 'vhd') + self.assertEqual(image.owner, 'asdf') + self.assertEqual(image.size, '1024') + self.assertEqual(image.min_ram, '512') + self.assertEqual(image.min_disk, '10') + self.assertEqual(image.properties, {'a': 'b', 'c': 'd'}) def test_update(self): image = self.mgr.update('1', name='image-2') diff --git a/tests/v1/utils.py b/tests/v1/utils.py index c06b075..7598aa5 100644 --- a/tests/v1/utils.py +++ b/tests/v1/utils.py @@ -18,11 +18,19 @@ fixtures = { 'POST': ( { 'location': '/v1/images/1', - 'x-image-meta-id': '1', - 'x-image-meta-name': 'image-1', - 'x-image-meta-property-arch': 'x86_64', }, - None), + {'image': { + 'id': '1', + 'name': 'image-1', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': '1024', + 'min_ram': '512', + 'min_disk': '10', + 'properties': {'a': 'b', 'c': 'd'}, + + }}), }, '/v1/images/detail': { 'GET': ( |