Adds images support for Nova V3 API
Adds support for basic image querying from the image server rather than the Nova API. The Nova V3 API no longer supports image querying directly, but in order to support convenience functions such as specifying images by name rather than ID, it is necessary to have some basic image query support. image delete and image meta manipulation is no longer supported by the client as these features can be accessed directly through the glance client Partially implements blueprint v3-api Change-Id: I9050845d631e9dfc2e110327221d154b8924cd65
This commit is contained in:
parent
2e5a5a81c3
commit
76f926fc10
@ -111,3 +111,19 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient):
|
||||
#
|
||||
get_flavors_2_flavor_access = (
|
||||
fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access)
|
||||
|
||||
#
|
||||
# Images
|
||||
#
|
||||
get_v1_images_detail = fakes_v1_1.FakeHTTPClient.get_images_detail
|
||||
get_v1_images = fakes_v1_1.FakeHTTPClient.get_images
|
||||
|
||||
def head_v1_images_1(self, **kw):
|
||||
headers = {
|
||||
'x-image-meta-id': '1',
|
||||
'x-image-meta-name': 'CentOS 5.2',
|
||||
'x-image-meta-updated': '2010-10-10T12:00:00Z',
|
||||
'x-image-meta-created': '2010-10-10T12:00:00Z',
|
||||
'x-image-meta-status': 'ACTIVE',
|
||||
'x-image-meta-property-test_key': 'test_value'}
|
||||
return 200, headers, ''
|
||||
|
57
novaclient/tests/v3/test_images.py
Normal file
57
novaclient/tests/v3/test_images.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from novaclient.tests import utils
|
||||
from novaclient.tests.v3 import fakes
|
||||
from novaclient.v3 import images
|
||||
|
||||
|
||||
cs = fakes.FakeClient()
|
||||
|
||||
|
||||
class ImagesTest(utils.TestCase):
|
||||
|
||||
def test_list_images(self):
|
||||
il = cs.images.list()
|
||||
cs.assert_called('GET', '/v1/images/detail')
|
||||
for i in il:
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
|
||||
def test_list_images_undetailed(self):
|
||||
il = cs.images.list(detailed=False)
|
||||
cs.assert_called('GET', '/v1/images')
|
||||
for i in il:
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
|
||||
def test_list_images_with_limit(self):
|
||||
il = cs.images.list(limit=4)
|
||||
cs.assert_called('GET', '/v1/images/detail?limit=4')
|
||||
|
||||
def test_get_image_details(self):
|
||||
i = cs.images.get(1)
|
||||
cs.assert_called('HEAD', '/v1/images/1')
|
||||
self.assertTrue(isinstance(i, images.Image))
|
||||
self.assertEqual(i.id, '1')
|
||||
self.assertEqual(i.name, 'CentOS 5.2')
|
||||
|
||||
def test_find(self):
|
||||
i = cs.images.find(name="CentOS 5.2")
|
||||
self.assertEqual(i.id, '1')
|
||||
cs.assert_called('GET', '/v1/images', pos=-2)
|
||||
cs.assert_called('HEAD', '/v1/images/1', pos=-1)
|
||||
|
||||
iml = cs.images.findall(status='SAVING')
|
||||
self.assertEqual(len(iml), 1)
|
||||
self.assertEqual(iml[0].name, 'My Server Backup')
|
@ -19,6 +19,7 @@ from novaclient.v3 import agents
|
||||
from novaclient.v3 import flavor_access
|
||||
from novaclient.v3 import flavors
|
||||
from novaclient.v3 import hosts
|
||||
from novaclient.v3 import images
|
||||
|
||||
|
||||
class Client(object):
|
||||
@ -57,6 +58,7 @@ class Client(object):
|
||||
self.hosts = hosts.HostManager(self)
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.flavor_access = flavor_access.FlavorAccessManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
|
104
novaclient/v3/images.py
Normal file
104
novaclient/v3/images.py
Normal file
@ -0,0 +1,104 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Image interface.
|
||||
"""
|
||||
from novaclient import base
|
||||
from novaclient.openstack.common.py3kcompat import urlutils
|
||||
from novaclient.openstack.common import strutils
|
||||
|
||||
|
||||
class Image(base.Resource):
|
||||
"""
|
||||
An image is a collection of files used to create or rebuild a server.
|
||||
"""
|
||||
HUMAN_ID = True
|
||||
|
||||
def __repr__(self):
|
||||
return "<Image: %s>" % self.name
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this image.
|
||||
"""
|
||||
self.manager.delete(self)
|
||||
|
||||
|
||||
class ImageManager(base.ManagerWithFind):
|
||||
"""
|
||||
Manage :class:`Image` resources.
|
||||
"""
|
||||
resource_class = Image
|
||||
# NOTE(cyeoh): Eventually we'll want novaclient to be smart
|
||||
# enough to do version discovery, but for now we just request
|
||||
# the v1 image API
|
||||
image_api_prefix = '/v1'
|
||||
|
||||
def _image_meta_from_headers(self, headers):
|
||||
meta = {'properties': {}}
|
||||
safe_decode = strutils.safe_decode
|
||||
for key, value in headers.items():
|
||||
value = safe_decode(value, incoming='utf-8')
|
||||
if key.startswith('x-image-meta-property-'):
|
||||
_key = safe_decode(key[22:], incoming='utf-8')
|
||||
meta['properties'][_key] = value
|
||||
elif key.startswith('x-image-meta-'):
|
||||
_key = safe_decode(key[13:], incoming='utf-8')
|
||||
meta[_key] = value
|
||||
|
||||
for key in ['is_public', 'protected', 'deleted']:
|
||||
if key in meta:
|
||||
meta[key] = strutils.bool_from_string(meta[key])
|
||||
|
||||
return self._format_image_meta_for_user(meta)
|
||||
|
||||
@staticmethod
|
||||
def _format_image_meta_for_user(meta):
|
||||
for key in ['size', 'min_ram', 'min_disk']:
|
||||
if key in meta:
|
||||
try:
|
||||
meta[key] = int(meta[key])
|
||||
except ValueError:
|
||||
pass
|
||||
return meta
|
||||
|
||||
def get(self, image):
|
||||
"""
|
||||
Get an image.
|
||||
|
||||
:param image: The ID of the image to get.
|
||||
:rtype: :class:`Image`
|
||||
"""
|
||||
url = "%s/images/%s" % (self.image_api_prefix, base.getid(image))
|
||||
resp, _ = self.api.client._cs_request(url, 'HEAD')
|
||||
foo = self._image_meta_from_headers(resp.headers)
|
||||
return Image(self, foo)
|
||||
|
||||
def list(self, detailed=True, limit=None):
|
||||
"""
|
||||
Get a list of all images.
|
||||
|
||||
:rtype: list of :class:`Image`
|
||||
:param limit: maximum number of images to return.
|
||||
"""
|
||||
params = {}
|
||||
detail = ''
|
||||
if detailed:
|
||||
detail = '/detail'
|
||||
if limit:
|
||||
params['limit'] = int(limit)
|
||||
query = '?%s' % urlutils.urlencode(params) if params else ''
|
||||
return self._list('/v1/images%s%s' % (detail, query), 'images')
|
@ -773,6 +773,7 @@ def do_network_create(cs, args):
|
||||
dest="limit",
|
||||
metavar="<limit>",
|
||||
help='number of images to return per request')
|
||||
@utils.service_type('image')
|
||||
def do_image_list(cs, _args):
|
||||
"""Print a list of available images to boot from."""
|
||||
limit = _args.limit
|
||||
@ -831,9 +832,6 @@ def _extract_metadata(args):
|
||||
def _print_image(image):
|
||||
info = image._info.copy()
|
||||
|
||||
# ignore links, we don't need to present those
|
||||
info.pop('links')
|
||||
|
||||
# try to replace a server entity to just an id
|
||||
server = info.pop('server', None)
|
||||
try:
|
||||
@ -842,10 +840,10 @@ def _print_image(image):
|
||||
pass
|
||||
|
||||
# break up metadata and display each on its own row
|
||||
metadata = info.pop('metadata', {})
|
||||
properties = info.pop('properties', {})
|
||||
try:
|
||||
for key, value in metadata.items():
|
||||
_key = 'metadata %s' % key
|
||||
for key, value in properties.items():
|
||||
_key = 'Property %s' % key
|
||||
info[_key] = value
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -864,6 +862,7 @@ def _print_flavor(flavor):
|
||||
@utils.arg('image',
|
||||
metavar='<image>',
|
||||
help="Name or ID of image")
|
||||
@utils.service_type('image')
|
||||
def do_image_show(cs, args):
|
||||
"""Show details about the given image."""
|
||||
image = _find_image(cs, args.image)
|
||||
|
Loading…
x
Reference in New Issue
Block a user