More complete image creation
This commit is contained in:
@@ -13,10 +13,15 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from glanceclient.common import base
|
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):
|
class Image(base.Resource):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -45,11 +50,11 @@ class ImageManager(base.Manager):
|
|||||||
|
|
||||||
def _image_meta_to_headers(self, fields):
|
def _image_meta_to_headers(self, fields):
|
||||||
headers = {}
|
headers = {}
|
||||||
for key, value in fields.iteritems():
|
fields_copy = copy.deepcopy(fields)
|
||||||
if key == 'properties':
|
for key, value in fields_copy.pop('properties', {}).iteritems():
|
||||||
headers['x-image-meta-property-%s' % key] = value
|
headers['x-image-meta-property-%s' % key] = str(value)
|
||||||
else:
|
for key, value in fields_copy.iteritems():
|
||||||
headers['x-image-meta-%s' % key] = value
|
headers['x-image-meta-%s' % key] = str(value)
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def get(self, image_id):
|
def get(self, image_id):
|
||||||
@@ -73,7 +78,7 @@ class ImageManager(base.Manager):
|
|||||||
if limit:
|
if limit:
|
||||||
params['limit'] = int(limit)
|
params['limit'] = int(limit)
|
||||||
if marker:
|
if marker:
|
||||||
params['marker'] = int(marker)
|
params['marker'] = marker
|
||||||
query = '?%s' % urllib.urlencode(params) if params else ''
|
query = '?%s' % urllib.urlencode(params) if params else ''
|
||||||
return self._list('/v1/images/detail%s' % query, "images")
|
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))
|
self._delete("/v1/images/%s" % base.getid(image))
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
"""Create an image"""
|
"""Create an image
|
||||||
|
|
||||||
|
TODO(bcwaldon): document accepted params
|
||||||
|
"""
|
||||||
fields = {}
|
fields = {}
|
||||||
if 'name' in kwargs:
|
for field in kwargs:
|
||||||
fields['name'] = kwargs['name']
|
if field in CREATE_PARAMS:
|
||||||
resp, body = self.api.post('/v1/images', body={'image': fields})
|
fields[field] = kwargs[field]
|
||||||
meta = self._image_meta_from_headers(resp)
|
else:
|
||||||
return Image(self, meta)
|
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):
|
def update(self, image, **kwargs):
|
||||||
"""Update an image"""
|
"""Update an image"""
|
||||||
@@ -100,14 +117,3 @@ class ImageManager(base.Manager):
|
|||||||
resp, body = self.api.put(url, headers=send_meta)
|
resp, body = self.api.put(url, headers=send_meta)
|
||||||
recv_meta = self._image_meta_from_headers(resp)
|
recv_meta = self._image_meta_from_headers(resp)
|
||||||
return Image(self, recv_meta)
|
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)
|
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
from glanceclient.common import utils
|
from glanceclient.common import utils
|
||||||
|
import glanceclient.v1.images
|
||||||
|
|
||||||
|
|
||||||
def do_image_list(gc, args):
|
def do_image_list(gc, args):
|
||||||
@@ -26,7 +27,7 @@ def do_image_list(gc, args):
|
|||||||
|
|
||||||
|
|
||||||
def _image_show(image):
|
def _image_show(image):
|
||||||
# Flatten image properties dict
|
# Flatten image properties dict for display
|
||||||
info = copy.deepcopy(image._info)
|
info = copy.deepcopy(image._info)
|
||||||
for (k, v) in info.pop('properties').iteritems():
|
for (k, v) in info.pop('properties').iteritems():
|
||||||
info['Property \'%s\'' % k] = v
|
info['Property \'%s\'' % k] = v
|
||||||
@@ -41,10 +42,61 @@ def do_image_show(gc, args):
|
|||||||
_image_show(image)
|
_image_show(image)
|
||||||
|
|
||||||
|
|
||||||
@utils.arg('--id', metavar='<IMAGE_ID>', help='ID of image to reserve.')
|
@utils.arg('--id', metavar='<IMAGE_ID>',
|
||||||
@utils.arg('--name', metavar='<NAME>', help='Name of image.')
|
help='ID of image to reserve.')
|
||||||
@utils.arg('--disk_format', metavar='<CONTAINER_FORMAT>', help='Disk format of image.')
|
@utils.arg('--name', metavar='<NAME>',
|
||||||
@utils.arg('--container_format', metavar='<DISK_FORMAT>', help='Container format of image.')
|
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):
|
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)
|
_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)
|
||||||
|
16
setup.py
16
setup.py
@@ -2,15 +2,15 @@ import os
|
|||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
from glanceclient.openstack.common.setup import parse_requirements
|
#from glanceclient.openstack.common.setup import parse_requirements
|
||||||
from glanceclient.openstack.common.setup import parse_dependency_links
|
#from glanceclient.openstack.common.setup import parse_dependency_links
|
||||||
from glanceclient.openstack.common.setup import write_requirements
|
#from glanceclient.openstack.common.setup import write_requirements
|
||||||
from glanceclient.openstack.common.setup import write_git_changelog
|
from glanceclient.openstack.common.setup import write_git_changelog
|
||||||
|
|
||||||
|
|
||||||
requires = parse_requirements()
|
#requires = parse_requirements()
|
||||||
dependency_links = parse_dependency_links()
|
#dependency_links = parse_dependency_links()
|
||||||
write_requirements()
|
#write_requirements()
|
||||||
write_git_changelog()
|
write_git_changelog()
|
||||||
|
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ setuptools.setup(
|
|||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
],
|
],
|
||||||
install_requires=requires,
|
# install_requires=requires,
|
||||||
dependency_links=dependency_links,
|
# dependency_links=dependency_links,
|
||||||
test_suite="nose.collector",
|
test_suite="nose.collector",
|
||||||
entry_points={'console_scripts': ['glance = glanceclient.shell:main']},
|
entry_points={'console_scripts': ['glance = glanceclient.shell:main']},
|
||||||
)
|
)
|
||||||
|
@@ -43,12 +43,44 @@ class ImageManagerTest(unittest.TestCase):
|
|||||||
expect = [('DELETE', '/v1/images/1', {}, None)]
|
expect = [('DELETE', '/v1/images/1', {}, None)]
|
||||||
self.assertEqual(self.api.calls, expect)
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
|
||||||
def test_create(self):
|
def test_create_no_data(self):
|
||||||
image = self.mgr.create(name='image-1')
|
params = {
|
||||||
expect = [('POST', '/v1/images', {}, {'image': {'name': 'image-1'}})]
|
'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(self.api.calls, expect)
|
||||||
self.assertEqual(image.id, '1')
|
self.assertEqual(image.id, '1')
|
||||||
self.assertEqual(image.name, 'image-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):
|
def test_update(self):
|
||||||
image = self.mgr.update('1', name='image-2')
|
image = self.mgr.update('1', name='image-2')
|
||||||
|
@@ -18,11 +18,19 @@ fixtures = {
|
|||||||
'POST': (
|
'POST': (
|
||||||
{
|
{
|
||||||
'location': '/v1/images/1',
|
'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': {
|
'/v1/images/detail': {
|
||||||
'GET': (
|
'GET': (
|
||||||
|
Reference in New Issue
Block a user