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:
Chris Yeoh 2013-12-09 17:08:30 +10:30
parent 2e5a5a81c3
commit 76f926fc10
5 changed files with 184 additions and 6 deletions

View File

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

View 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')

View File

@ -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
View 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')

View File

@ -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)