diff options
author | Brian Lamar <brian.lamar@rackspace.com> | 2011-03-29 16:51:44 +0000 |
---|---|---|
committer | Tarmac <> | 2011-03-29 16:51:44 +0000 |
commit | 11944d59a8c04ba48832a8bb4dc1f63c3a0ef27f (patch) | |
tree | 6d14df6095b1dfc903830fc65c13d14cf060631c | |
parent | 6719b9cb8794f69b719976a8b30af189a2273d02 (diff) | |
parent | 2f89d5541aa11b8654b197ffe24d3fd13e945da6 (diff) | |
download | nova-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__.py | 11 | ||||
-rw-r--r-- | nova/api/openstack/images.py | 307 | ||||
-rw-r--r-- | nova/api/openstack/views/images.py | 98 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_images.py | 425 |
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') |