summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorBrian Lamar <brian.lamar@rackspace.com>2011-03-17 22:30:34 -0400
committerBrian Lamar <brian.lamar@rackspace.com>2011-03-17 22:30:34 -0400
commitaf67fba36436feeede4dcc5720e51d2b66c3094a (patch)
treee98b22c0e8f0ef82d8835e2a96e2a4aec12300eb /nova
parente138e0836922ee0608feefbff5e4e5d03ad14197 (diff)
Images now v1.1 supported...mostly.
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/__init__.py11
-rw-r--r--nova/api/openstack/images.py214
-rw-r--r--nova/api/openstack/views/images.py63
-rw-r--r--nova/tests/api/openstack/test_images.py171
4 files changed, 285 insertions, 174 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 0b50d17d0..0ac67fdba 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -113,8 +113,6 @@ class APIRouter(wsgi.Router):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("image", "images", controller=images.Controller(),
- collection={'detail': 'GET'})
mapper.resource("flavor", "flavors", controller=flavors.Controller(),
collection={'detail': 'GET'})
mapper.resource("shared_ip_group", "shared_ip_groups",
@@ -130,6 +128,10 @@ class APIRouterV10(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.Controller_v1_0(),
+ collection={'detail': 'GET'})
+
class APIRouterV11(APIRouter):
def _setup_routes(self, mapper):
@@ -139,6 +141,11 @@ class APIRouterV11(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.Controller_v1_1(),
+ collection={'detail': 'GET'})
+
+
class Versions(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 98f0dd96b..2357bfd3d 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -1,6 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 OpenStack LLC.
+# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -19,92 +17,19 @@ from webob import exc
from nova import compute
from nova import flags
+from nova import log
from nova import utils
from nova import wsgi
-import nova.api.openstack
-from nova.api.openstack import common
-from nova.api.openstack import faults
-import nova.image.service
-
-
-FLAGS = flags.FLAGS
-
-
-def _translate_keys(item):
- """
- Maps key names to Rackspace-like attributes for return
- also pares down attributes to those we want
- item is a dict
-
- Note: should be removed when the set of keys expected by the api
- and the set of keys returned by the image service are equivalent
-
- """
- # TODO(tr3buchet): this map is specific to s3 object store,
- # replace with a list of keys for _filter_keys later
- mapped_keys = {'status': 'imageState',
- 'id': 'imageId',
- 'name': 'imageLocation'}
-
- mapped_item = {}
- # TODO(tr3buchet):
- # this chunk of code works with s3 and the local image service/glance
- # when we switch to glance/local image service it can be replaced with
- # a call to _filter_keys, and mapped_keys can be changed to a list
- try:
- for k, v in mapped_keys.iteritems():
- # map s3 fields
- mapped_item[k] = item[v]
- except KeyError:
- # return only the fields api expects
- mapped_item = _filter_keys(item, mapped_keys.keys())
-
- return mapped_item
-
-
-def _translate_status(item):
- """
- Translates status of image to match current Rackspace api bindings
- item is a dict
+from nova.api.openstack.views import images as images_view
- Note: should be removed when the set of statuses expected by the api
- and the set of statuses returned by the image service are equivalent
+class Controller(wsgi.Controller):
"""
- status_mapping = {
- 'pending': 'queued',
- 'decrypting': 'preparing',
- 'untarring': 'saving',
- 'available': 'active'}
- try:
- item['status'] = status_mapping[item['status']]
- except KeyError:
- # TODO(sirp): Performing translation of status (if necessary) here for
- # now. Perhaps this should really be done in EC2 API and
- # S3ImageService
- pass
-
- return item
-
-
-def _filter_keys(item, keys):
- """
- Filters all model attributes except for keys
- item is a dict
-
+ Base `wsgi.Controller` for retrieving and displaying images in the
+ OpenStack API. Version-inspecific code goes here.
"""
- return dict((k, v) for k, v in item.iteritems() if k in keys)
-
-
-def _convert_image_id_to_hash(image):
- if 'imageId' in image:
- # Convert EC2-style ID (i-blah) to Rackspace-style (int)
- image_id = abs(hash(image['imageId']))
- image['imageId'] = image_id
- image['id'] = image_id
-
-class Controller(wsgi.Controller):
+ _builder = images_view.Builder_v1_0()
_serialization_metadata = {
'application/xml': {
@@ -112,55 +37,96 @@ class Controller(wsgi.Controller):
"image": ["id", "name", "updated", "created", "status",
"serverId", "progress"]}}}
- def __init__(self):
- self._service = utils.import_object(FLAGS.image_service)
+ def __init__(self, image_service=None, compute_service=None):
+ """
+ Initialize new `ImageController`.
+
+ @param compute_service: `nova.compute.api:API`
+ @param image_service: `nova.image.service:BaseImageService`
+ """
+ _default_service = utils.import_object(flags.FLAGS.image_service)
+
+ self.__compute = compute_service or compute.API()
+ self.__image = image_service or _default_service
+ self.__log = log.getLogger(self.__class__.__name__)
def index(self, req):
- """Return all public images in brief"""
- items = self._service.index(req.environ['nova.context'])
- items = common.limited(items, req)
- items = [_filter_keys(item, ('id', 'name')) for item in items]
- return dict(images=items)
+ """
+ Return an index listing of images available to the request.
+
+ @param req: `webob.Request` object
+ """
+ context = req.environ['nova.context']
+ images = self.__image.index(context)
+ build = self._builder.build
+ return dict(images=[build(req, image, False) for image in images])
def detail(self, req):
- """Return all public images in detail"""
- try:
- items = self._service.detail(req.environ['nova.context'])
- except NotImplementedError:
- items = self._service.index(req.environ['nova.context'])
- for image in items:
- _convert_image_id_to_hash(image)
-
- items = common.limited(items, req)
- items = [_translate_keys(item) for item in items]
- items = [_translate_status(item) for item in items]
- return dict(images=items)
-
- def show(self, req, id):
- """Return data about the given image id"""
- image_id = common.get_image_id_from_image_hash(self._service,
- req.environ['nova.context'], id)
-
- image = self._service.show(req.environ['nova.context'], image_id)
- _convert_image_id_to_hash(image)
- return dict(image=image)
-
- def delete(self, req, id):
- # Only public images are supported for now.
- raise faults.Fault(exc.HTTPNotFound())
+ """
+ Return a detailed index listing of images available to the request.
+
+ @param req: `webob.Request` object.
+ """
+ context = req.environ['nova.context']
+ images = self.__image.detail(context)
+ build = self._builder.build
+ return dict(images=[build(req, image, True) for image in images])
+
+ def show(self, req, image_id):
+ """
+ Return detailed information about a specific image.
+
+ @param req: `webob.Request` object
+ @param image_id: Image identifier (integer)
+ """
+ context = req.environ['nova.context']
+ image = self.__image.show(context, image_id)
+ return self._builder.build(req, image, True)
+
+ def delete(self, req, image_id):
+ """
+ Delete an image, if allowed.
+
+ @param req: `webob.Request` object
+ @param image_id: Image identifier (integer)
+ """
+ context = req.environ['nova.context']
+ self.__image.delete(context, image_id)
+ return exc.HTTPNoContent()
def create(self, req):
+ """
+ Snapshot a server instance and save the image.
+
+ @param req: `webob.Request` object
+ """
context = req.environ['nova.context']
- env = self._deserialize(req.body, req.get_content_type())
- instance_id = env["image"]["serverId"]
- name = env["image"]["name"]
+ body = req.body
+ content_type = req.get_content_type()
+ image = self._deserialize(body, content_type)
+
+ if not image:
+ raise exc.HTTPBadRequest()
+
+ try:
+ server_id = image["serverId"]
+ image_name = image["name"]
+ except KeyError:
+ raise exc.HTTPBadRequest()
+
+ image = self.__compute.snapshot(context, server_id, image_name)
+ return self._builder.build(req, image, True)
- image_meta = compute.API().snapshot(
- context, instance_id, name)
- return dict(image=image_meta)
+class Controller_v1_0(Controller):
+ """
+ Version 1.0 specific controller logic.
+ """
+ _builder = images_view.Builder_v1_0()
+
- def update(self, req, id):
- # Users may not modify public images, and that's all that
- # we support for now.
- raise faults.Fault(exc.HTTPNotFound())
+class Controller_v1_1(Controller):
+ """
+ Version 1.1 specific controller logic.
+ """
+ _builder = images_view.Builder_v1_1()
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index a6c6ad7d1..1631d1fe3 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -15,20 +15,61 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova.api.openstack import common
+class Builder(object):
+ """
+ Base class for generating responses to OpenStack API requests for
+ information about images.
+ """
-class ViewBuilder(object):
- def __init__(self):
- pass
+ def build(self, request, image_obj, detail=False):
+ """
+ Return a standardized image structure for display by the API.
+ """
+ image = {
+ "id": image_obj["id"],
+ "name": image_obj["name"],
+ }
- def build(self, image_obj):
- raise NotImplementedError()
+ if detail:
+ image.update({
+ "created": image_obj["created_at"],
+ "updated": image_obj["updated_at"],
+ "status": image_obj["status"],
+ })
+ return image
-class ViewBuilderV11(ViewBuilder):
- def __init__(self, base_url):
- self.base_url = base_url
- def generate_href(self, image_id):
- return "%s/images/%s" % (self.base_url, image_id)
+class Builder_v1_0(Builder):
+ pass
+
+
+class Builder_v1_1(Builder):
+ """
+ OpenStack API v1.1 Image Builder
+ """
+
+ def build(self, request, image_obj, detail=False):
+ """
+ Return a standardized image structure for display by the API.
+ """
+ image = Builder.build(self, request, image_obj, detail)
+ href = "%s/images/%s" % (request.application_url, image_obj["id"])
+
+ image["links"] = [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }]
+
+ return image
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 76f758929..c313192b7 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -42,8 +42,9 @@ FLAGS = flags.FLAGS
class BaseImageServiceTests(object):
-
- """Tasks to test for all image services"""
+ """
+ Tasks to test for all image services.
+ """
def test_create(self):
@@ -173,10 +174,9 @@ class GlanceImageServiceTest(test.TestCase,
class ImageControllerWithGlanceServiceTest(test.TestCase):
-
- """Test of the OpenStack API /images application controller"""
-
- # Registered images at start of each test.
+ """
+ Test of the OpenStack API /images application controller w/Glance.
+ """
IMAGE_FIXTURES = [
{'id': '23g2ogk23k4hhkk4k42l',
@@ -198,7 +198,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'deleted': False,
'is_public': True,
'status': 'available',
- 'image_type': 'ramdisk'}]
+ 'image_type': 'ramdisk'},
+ ]
def setUp(self):
super(ImageControllerWithGlanceServiceTest, self).setUp()
@@ -219,36 +220,132 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
super(ImageControllerWithGlanceServiceTest, self).tearDown()
def test_get_image_index(self):
- req = webob.Request.blank('/v1.0/images')
- res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
+ request = webob.Request.blank('/v1.0/images')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ for image in self.IMAGE_FIXTURES:
+ test_image = {
+ "id": image["id"],
+ "name": image["name"],
+ }
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(self.IMAGE_FIXTURES))
+
+ def test_get_image_index_v1_1(self):
+ request = webob.Request.blank('/v1.1/images')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ for image in self.IMAGE_FIXTURES:
+ href = "http://localhost/v1.1/images/%s" % image["id"]
+ test_image = {
+ "id": image["id"],
+ "name": image["name"],
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/%s" % image["id"],
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }],
+ }
+ print test_image
+ print
+ print response_list
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(self.IMAGE_FIXTURES))
- fixture_index = [dict(id=f['id'], name=f['name']) for f
- in self.IMAGE_FIXTURES]
+ def test_get_image_details(self):
+ request = webob.Request.blank('/v1.0/images/detail')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ for image in self.IMAGE_FIXTURES:
+ test_image = {
+ "id": image["id"],
+ "name": image["name"],
+ "updated": image["updated_at"],
+ "created": image["created_at"],
+ "status": image["status"],
+ }
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(self.IMAGE_FIXTURES))
+
+ def test_get_image_details_v1_1(self):
+ request = webob.Request.blank('/v1.1/images/detail')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ for image in self.IMAGE_FIXTURES:
+ href = "http://localhost/v1.1/images/%s" % image["id"]
+ test_image = {
+ "id": image["id"],
+ "name": image["name"],
+ "updated": image["updated_at"],
+ "created": image["created_at"],
+ "status": image["status"],
+ "links": [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }],
+ }
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(self.IMAGE_FIXTURES))
+
+ def test_get_image_create_empty(self):
+ request = webob.Request.blank('/v1.1/images')
+ request.method = "POST"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_get_image_create_bad_no_name(self):
+ request = webob.Request.blank('/v1.1/images')
+ request.method = "POST"
+ request.content_type = "application/json"
+ request.body = json.dumps({
+ "serverId": 1,
+ })
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_get_image_create_bad_no_id(self):
+ request = webob.Request.blank('/v1.1/images')
+ request.method = "POST"
+ request.content_type = "application/json"
+ request.body = json.dumps({
+ "name" : "Snapshot Test",
+ })
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
- for image in res_dict['images']:
- self.assertEquals(1, fixture_index.count(image),
- "image %s not in fixture index!" % str(image))
- def test_get_image_details(self):
- req = webob.Request.blank('/v1.0/images/detail')
- res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
-
- def _is_equivalent_subset(x, y):
- if set(x) <= set(y):
- for k, v in x.iteritems():
- if x[k] != y[k]:
- if x[k] == 'active' and y[k] == 'available':
- continue
- return False
- return True
- return False
-
- for image in res_dict['images']:
- for image_fixture in self.IMAGE_FIXTURES:
- if _is_equivalent_subset(image, image_fixture):
- break
- else:
- self.assertEquals(1, 2, "image %s not in fixtures!" %
- str(image))