summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Lamar <brian.lamar@rackspace.com>2011-03-29 16:51:44 +0000
committerTarmac <>2011-03-29 16:51:44 +0000
commit11944d59a8c04ba48832a8bb4dc1f63c3a0ef27f (patch)
tree6d14df6095b1dfc903830fc65c13d14cf060631c
parent6719b9cb8794f69b719976a8b30af189a2273d02 (diff)
parent2f89d5541aa11b8654b197ffe24d3fd13e945da6 (diff)
downloadnova-11944d59a8c04ba48832a8bb4dc1f63c3a0ef27f.tar.gz
nova-11944d59a8c04ba48832a8bb4dc1f63c3a0ef27f.tar.xz
nova-11944d59a8c04ba48832a8bb4dc1f63c3a0ef27f.zip
Adds support for versioned requests on /images through the OpenStack API.
-rw-r--r--nova/api/openstack/__init__.py11
-rw-r--r--nova/api/openstack/images.py307
-rw-r--r--nova/api/openstack/views/images.py98
-rw-r--r--nova/tests/api/openstack/test_images.py425
4 files changed, 591 insertions, 250 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 283cebb4d..4f9405075 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -111,9 +111,6 @@ class APIRouter(wsgi.Router):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("image", "images", controller=images.Controller(),
- collection={'detail': 'GET'})
-
_limits = limits.LimitsController()
mapper.resource("limit", "limits", controller=_limits)
@@ -128,6 +125,10 @@ class APIRouterV10(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.ControllerV10(),
+ collection={'detail': 'GET'})
+
mapper.resource("flavor", "flavors",
controller=flavors.ControllerV10(),
collection={'detail': 'GET'})
@@ -152,6 +153,10 @@ class APIRouterV11(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.ControllerV11(),
+ collection={'detail': 'GET'})
+
mapper.resource("image_meta", "meta",
controller=image_metadata.Controller(),
parent_resource=dict(member_name='image',
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 79852ecc6..e77100d7b 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
@@ -17,7 +15,7 @@
import datetime
-from webob import exc
+import webob.exc
from nova import compute
from nova import exception
@@ -25,238 +23,133 @@ 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
+from nova.api.openstack.views import images as images_view
LOG = log.getLogger('nova.api.openstack.images')
-
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
-
- 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
-
- """
- 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
-
-
-def _filter_keys(item, keys):
- """
- Filters all model attributes except for keys
- item is a dict
-
- """
- 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
-
-
-def _translate_s3_like_images(image_metadata):
- """Work-around for leaky S3ImageService abstraction"""
- api_metadata = image_metadata.copy()
- _convert_image_id_to_hash(api_metadata)
- api_metadata = _translate_keys(api_metadata)
- _translate_status(api_metadata)
- return api_metadata
-
-
-def _translate_from_image_service_to_api(image_metadata):
- """Translate from ImageService to OpenStack API style attribute names
-
- This involves 4 steps:
-
- 1. Filter out attributes that the OpenStack API doesn't need
-
- 2. Translate from base image attributes from names used by
- BaseImageService to names used by OpenStack API
-
- 3. Add in any image properties
-
- 4. Format values according to API spec (for example dates must
- look like "2010-08-10T12:00:00Z")
- """
- service_metadata = image_metadata.copy()
- properties = service_metadata.pop('properties', {})
-
- # 1. Filter out unecessary attributes
- api_keys = ['id', 'name', 'updated_at', 'created_at', 'status']
- api_metadata = utils.subset_dict(service_metadata, api_keys)
-
- # 2. Translate base image attributes
- api_map = {'updated_at': 'updated', 'created_at': 'created'}
- api_metadata = utils.map_dict_keys(api_metadata, api_map)
-
- # 3. Add in any image properties
- # 3a. serverId is used for backups and snapshots
- try:
- api_metadata['serverId'] = int(properties['instance_id'])
- except KeyError:
- pass # skip if it's not present
- except ValueError:
- pass # skip if it's not an integer
-
- # 3b. Progress special case
- # TODO(sirp): ImageService doesn't have a notion of progress yet, so for
- # now just fake it
- if service_metadata['status'] == 'saving':
- api_metadata['progress'] = 0
-
- # 4. Format values
- # 4a. Format Image Status (API requires uppercase)
- api_metadata['status'] = _format_status_for_api(api_metadata['status'])
-
- # 4b. Format timestamps
- for attr in ('created', 'updated'):
- if attr in api_metadata:
- api_metadata[attr] = _format_datetime_for_api(
- api_metadata[attr])
-
- return api_metadata
-
-
-def _format_status_for_api(status):
- """Return status in a format compliant with OpenStack API"""
- mapping = {'queued': 'QUEUED',
- 'preparing': 'PREPARING',
- 'saving': 'SAVING',
- 'active': 'ACTIVE',
- 'killed': 'FAILED'}
- return mapping[status]
-
-
-def _format_datetime_for_api(datetime_):
- """Stringify datetime objects in a format compliant with OpenStack API"""
- API_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
- return datetime_.strftime(API_DATETIME_FMT)
-
-
-def _safe_translate(image_metadata):
- """Translate attributes for OpenStack API, temporary workaround for
- S3ImageService attribute leakage.
- """
- # FIXME(sirp): The S3ImageService appears to be leaking implementation
- # details, including its internal attribute names, and internal
- # `status` values. Working around it for now.
- s3_like_image = ('imageId' in image_metadata)
- if s3_like_image:
- translate = _translate_s3_like_images
- else:
- translate = _translate_from_image_service_to_api
- return translate(image_metadata)
-
-
class Controller(wsgi.Controller):
+ """Base `wsgi.Controller` for retrieving/displaying images."""
_serialization_metadata = {
'application/xml': {
"attributes": {
"image": ["id", "name", "updated", "created", "status",
- "serverId", "progress"]}}}
+ "serverId", "progress"],
+ "link": ["rel", "type", "href"],
+ },
+ },
+ }
+
+ def __init__(self, image_service=None, compute_service=None):
+ """Initialize new `ImageController`.
- def __init__(self):
- self._service = utils.import_object(FLAGS.image_service)
+ :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_service = compute_service or compute.API()
+ self._image_service = image_service or _default_service
def index(self, req):
- """Return all public images in brief"""
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+ """
context = req.environ['nova.context']
- image_metas = self._service.index(context)
- image_metas = common.limited(image_metas, req)
- return dict(images=image_metas)
+ images = self._image_service.index(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=False) for image in images])
def detail(self, req):
- """Return all public images in detail"""
+ """Return a detailed index listing of images available to the request.
+
+ :param req: `wsgi.Request` object.
+ """
context = req.environ['nova.context']
- image_metas = self._service.detail(context)
- image_metas = common.limited(image_metas, req)
- api_image_metas = [_safe_translate(image_meta)
- for image_meta in image_metas]
- return dict(images=api_image_metas)
+ images = self._image_service.detail(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=True) for image in images])
def show(self, req, id):
- """Return data about the given image id"""
+ """Return detailed information about a specific image.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
context = req.environ['nova.context']
+
+ try:
+ image_id = int(id)
+ except ValueError:
+ explanation = _("Image not found.")
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
+
try:
- image_id = common.get_image_id_from_image_hash(
- self._service, context, id)
+ image = self._image_service.show(context, image_id)
except exception.NotFound:
- raise faults.Fault(exc.HTTPNotFound())
+ explanation = _("Image '%d' not found.") % (image_id)
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
- image_meta = self._service.show(context, image_id)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
+ return dict(image=self.get_builder(req).build(image, detail=True))
def delete(self, req, id):
- # Only public images are supported for now.
- raise faults.Fault(exc.HTTPNotFound())
+ """Delete an image, if allowed.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ image_id = id
+ context = req.environ['nova.context']
+ self._image_service.delete(context, image_id)
+ return webob.exc.HTTPNoContent()
def create(self, req):
+ """Snapshot a server instance and save the image.
+
+ :param req: `wsgi.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"]
- image_meta = compute.API().snapshot(
- context, instance_id, name)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
-
- 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())
+ content_type = req.get_content_type()
+ image = self._deserialize(req.body, content_type)
+
+ if not image:
+ raise webob.exc.HTTPBadRequest()
+
+ try:
+ server_id = image["image"]["serverId"]
+ image_name = image["image"]["name"]
+ except KeyError:
+ raise webob.exc.HTTPBadRequest()
+
+ image = self._compute_service.snapshot(context, server_id, image_name)
+ return self.get_builder(req).build(image, detail=True)
+
+ def get_builder(self, request):
+ """Indicates that you must use a Controller subclass."""
+ raise NotImplementedError
+
+
+class ControllerV10(Controller):
+ """Version 1.0 specific controller logic."""
+
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV10(base_url)
+
+
+class ControllerV11(Controller):
+ """Version 1.1 specific controller logic."""
+
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV11(base_url)
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index a6c6ad7d1..3807fa95f 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -15,20 +15,100 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova.api.openstack import common
+import os.path
class ViewBuilder(object):
- def __init__(self):
- pass
+ """Base class for generating responses to OpenStack API image requests."""
- def build(self, image_obj):
- raise NotImplementedError()
+ def __init__(self, base_url):
+ """Initialize new `ViewBuilder`."""
+ self._url = base_url
+ def _format_dates(self, image):
+ """Update all date fields to ensure standardized formatting."""
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
-class ViewBuilderV11(ViewBuilder):
- def __init__(self, base_url):
- self.base_url = base_url
+ def _format_status(self, image):
+ """Update the status field to standardize format."""
+ status_mapping = {
+ 'pending': 'queued',
+ 'decrypting': 'preparing',
+ 'untarring': 'saving',
+ 'available': 'active',
+ 'killed': 'failed',
+ }
+
+ try:
+ image['status'] = status_mapping[image['status']].upper()
+ except KeyError:
+ image['status'] = image['status'].upper()
def generate_href(self, image_id):
- return "%s/images/%s" % (self.base_url, image_id)
+ """Return an href string pointing to this object."""
+ return os.path.join(self._url, "images", str(image_id))
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ properties = image_obj.get("properties", {})
+
+ self._format_dates(image_obj)
+
+ if "status" in image_obj:
+ self._format_status(image_obj)
+
+ image = {
+ "id": image_obj["id"],
+ "name": image_obj["name"],
+ }
+
+ if "instance_id" in properties:
+ try:
+ image["serverId"] = int(properties["instance_id"])
+ except ValueError:
+ pass
+
+ if detail:
+ image.update({
+ "created": image_obj["created_at"],
+ "updated": image_obj["updated_at"],
+ "status": image_obj["status"],
+ })
+
+ if image["status"] == "SAVING":
+ image["progress"] = 0
+
+ return image
+
+
+class ViewBuilderV10(ViewBuilder):
+ """OpenStack API v1.0 Image Builder"""
+ pass
+
+
+class ViewBuilderV11(ViewBuilder):
+ """OpenStack API v1.1 Image Builder"""
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ image = ViewBuilder.build(self, image_obj, detail)
+ href = self.generate_href(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 738bdda19..57e447dce 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -20,11 +20,13 @@ Tests of the new image services, both as a service layer,
and as a WSGI layer
"""
+import copy
import json
import datetime
import os
import shutil
import tempfile
+import xml.dom.minidom as minidom
import stubout
import webob
@@ -214,12 +216,14 @@ class GlanceImageServiceTest(_BaseImageServiceTests):
class ImageControllerWithGlanceServiceTest(test.TestCase):
- """Test of the OpenStack API /images application controller"""
-
+ """
+ Test of the OpenStack API /images application controller w/Glance.
+ """
NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
NOW_API_FORMAT = "2010-10-11T10:30:22Z"
def setUp(self):
+ """Run before each test."""
super(ImageControllerWithGlanceServiceTest, self).setUp()
self.orig_image_service = FLAGS.image_service
FLAGS.image_service = 'nova.image.glance.GlanceImageService'
@@ -230,18 +234,30 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_auth(self.stubs)
fakes.stub_out_key_pair_funcs(self.stubs)
- fixtures = self._make_image_fixtures()
- fakes.stub_out_glance(self.stubs, initial_fixtures=fixtures)
+ self.fixtures = self._make_image_fixtures()
+ fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures)
def tearDown(self):
+ """Run after each test."""
self.stubs.UnsetAll()
FLAGS.image_service = self.orig_image_service
super(ImageControllerWithGlanceServiceTest, self).tearDown()
+ def _applicable_fixture(self, fixture, user_id):
+ """Determine if this fixture is applicable for given user id."""
+ is_public = fixture["is_public"]
+ try:
+ uid = int(fixture["properties"]["user_id"])
+ except KeyError:
+ uid = None
+ return uid == user_id or is_public
+
def test_get_image_index(self):
- req = webob.Request.blank('/v1.0/images')
- res = req.get_response(fakes.wsgi_app())
- image_metas = json.loads(res.body)['images']
+ 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"]
expected = [{'id': 123, 'name': 'public image'},
{'id': 124, 'name': 'queued backup'},
@@ -249,32 +265,379 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{'id': 126, 'name': 'active backup'},
{'id': 127, 'name': 'killed backup'}]
- self.assertDictListMatch(image_metas, expected)
+ self.assertDictListMatch(response_list, expected)
+
+ def test_get_image(self):
+ request = webob.Request.blank('/v1.0/images/123')
+ response = request.get_response(fakes.wsgi_app())
+
+ self.assertEqual(200, response.status_int)
+
+ actual_image = json.loads(response.body)
+
+ expected_image = {
+ "image": {
+ "id": 123,
+ "name": "public image",
+ "updated": self.NOW_API_FORMAT,
+ "created": self.NOW_API_FORMAT,
+ "status": "ACTIVE",
+ },
+ }
+
+ self.assertEqual(expected_image, actual_image)
+
+ def test_get_image_v1_1(self):
+ request = webob.Request.blank('/v1.1/images/123')
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = json.loads(response.body)
+
+ href = "http://localhost/v1.1/images/123"
+
+ expected_image = {
+ "image": {
+ "id": 123,
+ "name": "public image",
+ "updated": self.NOW_API_FORMAT,
+ "created": self.NOW_API_FORMAT,
+ "status": "ACTIVE",
+ "links": [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }],
+ },
+ }
+
+ self.assertEqual(expected_image, actual_image)
+
+ def test_get_image_xml(self):
+ request = webob.Request.blank('/v1.0/images/123')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_now = self.NOW_API_FORMAT
+ expected_image = minidom.parseString("""
+ <image id="123"
+ name="public image"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE" />
+ """ % (locals()))
+
+ self.assertEqual(expected_image.toxml(), actual_image.toxml())
+
+ def test_get_image_v1_1_xml(self):
+ request = webob.Request.blank('/v1.1/images/123')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_href = "http://localhost/v1.1/images/123"
+ expected_now = self.NOW_API_FORMAT
+ expected_image = minidom.parseString("""
+ <image id="123"
+ name="public image"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE">
+ <links>
+ <link href="%(expected_href)s" rel="self"/>
+ <link href="%(expected_href)s" rel="bookmark"
+ type="application/json" />
+ <link href="%(expected_href)s" rel="bookmark"
+ type="application/xml" />
+ </links>
+ </image>
+ """.replace(" ", "") % (locals()))
+
+ self.assertEqual(expected_image.toxml(), actual_image.toxml())
+
+ def test_get_image_404_json(self):
+ request = webob.Request.blank('/v1.0/images/NonExistantImage')
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = {
+ "itemNotFound": {
+ "message": "Image not found.",
+ "code": 404,
+ },
+ }
+
+ actual = json.loads(response.body)
+
+ self.assertEqual(expected, actual)
+
+ def test_get_image_404_xml(self):
+ request = webob.Request.blank('/v1.0/images/NonExistantImage')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = minidom.parseString("""
+ <itemNotFound code="404">
+ <message>
+ Image not found.
+ </message>
+ </itemNotFound>
+ """.replace(" ", ""))
+
+ actual = minidom.parseString(response.body.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_get_image_404_v1_1_json(self):
+ request = webob.Request.blank('/v1.1/images/NonExistantImage')
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = {
+ "itemNotFound": {
+ "message": "Image not found.",
+ "code": 404,
+ },
+ }
+
+ actual = json.loads(response.body)
+
+ self.assertEqual(expected, actual)
+
+ def test_get_image_404_v1_1_xml(self):
+ request = webob.Request.blank('/v1.1/images/NonExistantImage')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = minidom.parseString("""
+ <itemNotFound code="404">
+ <message>
+ Image not found.
+ </message>
+ </itemNotFound>
+ """.replace(" ", ""))
+
+ actual = minidom.parseString(response.body.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ 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"]
+
+ fixtures = copy.copy(self.fixtures)
+
+ for image in fixtures:
+ if not self._applicable_fixture(image, 1):
+ fixtures.remove(image)
+ continue
+
+ 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,
+ }],
+ }
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(fixtures))
def test_get_image_details(self):
- req = webob.Request.blank('/v1.0/images/detail')
- res = req.get_response(fakes.wsgi_app())
- image_metas = json.loads(res.body)['images']
-
- now = self.NOW_API_FORMAT
- expected = [
- {'id': 123, 'name': 'public image', 'updated': now,
- 'created': now, 'status': 'ACTIVE'},
- {'id': 124, 'name': 'queued backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'QUEUED'},
- {'id': 125, 'name': 'saving backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'SAVING', 'progress': 0},
- {'id': 126, 'name': 'active backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'ACTIVE'},
- {'id': 127, 'name': 'killed backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'FAILED'}
- ]
-
- self.assertDictListMatch(image_metas, expected)
+ 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"]
+
+ expected = [{
+ 'id': 123,
+ 'name': 'public image',
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ },
+ {
+ 'id': 124,
+ 'name': 'queued backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'QUEUED',
+ },
+ {
+ 'id': 125,
+ 'name': 'saving backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 0,
+ },
+ {
+ 'id': 126,
+ 'name': 'active backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE'
+ },
+ {
+ 'id': 127,
+ 'name': 'killed backup', 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'FAILED',
+ }]
+
+ self.assertDictListMatch(expected, response_list)
+
+ 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"]
+
+ expected = [{
+ 'id': 123,
+ 'name': 'public image',
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/123",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/123",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/123",
+ }],
+ },
+ {
+ 'id': 124,
+ 'name': 'queued backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'QUEUED',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/124",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/124",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/124",
+ }],
+ },
+ {
+ 'id': 125,
+ 'name': 'saving backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 0,
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/125",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/125",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/125",
+ }],
+ },
+ {
+ 'id': 126,
+ 'name': 'active backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/126",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/126",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/126",
+ }],
+ },
+ {
+ 'id': 127,
+ 'name': 'killed backup', 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'FAILED',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/127",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/127",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/127",
+ }],
+ }]
+
+ self.assertDictListMatch(expected, response_list)
def test_get_image_found(self):
req = webob.Request.blank('/v1.0/images/123')