diff options
| author | Jenkins <jenkins@review.openstack.org> | 2013-06-28 17:53:26 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2013-06-28 17:53:26 +0000 |
| commit | 8f880e7d3cac3fb0df83101e3981d14fd65e8225 (patch) | |
| tree | c882034406b37c69f1385cb2e286027a775b5f9d | |
| parent | 758cf7cc81eb7911e20da0c2898a603d16830733 (diff) | |
| parent | ea38833697b527db0631d185167dffdf23ca3b4a (diff) | |
| download | nova-8f880e7d3cac3fb0df83101e3981d14fd65e8225.tar.gz nova-8f880e7d3cac3fb0df83101e3981d14fd65e8225.tar.xz nova-8f880e7d3cac3fb0df83101e3981d14fd65e8225.zip | |
Merge "Port images functionality to v3 API Part 1"
| -rw-r--r-- | nova/api/openstack/compute/plugins/v3/images.py | 214 | ||||
| -rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/test_images.py | 1336 |
2 files changed, 1550 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/images.py b/nova/api/openstack/compute/plugins/v3/images.py new file mode 100644 index 000000000..e0c4f7465 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/images.py @@ -0,0 +1,214 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +import webob.exc + +from nova.api.openstack import common +from nova.api.openstack.compute.views import images as views_images +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +import nova.image.glance +import nova.utils + + +SUPPORTED_FILTERS = { + 'name': 'name', + 'status': 'status', + 'changes-since': 'changes-since', + 'server': 'property-instance_uuid', + 'type': 'property-image_type', + 'minRam': 'min_ram', + 'minDisk': 'min_disk', +} + + +def make_image(elem, detailed=False): + elem.set('name') + elem.set('id') + + if detailed: + elem.set('updated') + elem.set('created') + elem.set('status') + elem.set('progress') + elem.set('minRam') + elem.set('minDisk') + + server = xmlutil.SubTemplateElement(elem, 'server', selector='server') + server.set('id') + xmlutil.make_links(server, 'links') + + elem.append(common.MetadataTemplate()) + + xmlutil.make_links(elem, 'links') + + +image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ImageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image', selector='image') + make_image(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class MinimalImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem) + xmlutil.make_links(root, 'images_links') + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class ImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class Controller(wsgi.Controller): + """Base controller for retrieving/displaying images.""" + + _view_builder_class = views_images.ViewBuilder + + def __init__(self, image_service=None, **kwargs): + """Initialize new `ImageController`. + + :param image_service: `nova.image.glance:GlanceImageService` + + """ + super(Controller, self).__init__(**kwargs) + self._image_service = (image_service or + nova.image.glance.get_default_image_service()) + + def _get_filters(self, req): + """ + Return a dictionary of query param filters from the request + + :param req: the Request object coming from the wsgi layer + :retval a dict of key/value filters + """ + filters = {} + for param in req.params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): + # map filter name or carry through if property-* + filter_name = SUPPORTED_FILTERS.get(param, param) + filters[filter_name] = req.params.get(param) + + # ensure server filter is the instance uuid + filter_name = 'property-instance_uuid' + try: + filters[filter_name] = filters[filter_name].rsplit('/', 1)[1] + except (AttributeError, IndexError, KeyError): + pass + + filter_name = 'status' + if filter_name in filters: + # The Image API expects us to use lowercase strings for status + filters[filter_name] = filters[filter_name].lower() + + return filters + + @wsgi.serializers(xml=ImageTemplate) + def show(self, req, id): + """Return detailed information about a specific image. + + :param req: `wsgi.Request` object + :param id: Image identifier + """ + context = req.environ['nova.context'] + + try: + image = self._image_service.show(context, id) + except (exception.NotFound, exception.InvalidImageRef): + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + req.cache_db_items('images', [image], 'id') + return self._view_builder.show(req, image) + + def delete(self, req, id): + """Delete an image, if allowed. + + :param req: `wsgi.Request` object + :param id: Image identifier (integer) + """ + context = req.environ['nova.context'] + try: + self._image_service.delete(context, id) + except exception.ImageNotFound: + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + except exception.ImageNotAuthorized: + # The image service raises this exception on delete if glanceclient + # raises HTTPForbidden. + explanation = _("You are not allowed to delete the image.") + raise webob.exc.HTTPForbidden(explanation=explanation) + return webob.exc.HTTPNoContent() + + @wsgi.serializers(xml=MinimalImagesTemplate) + def index(self, req): + """Return an index listing of images available to the request. + + :param req: `wsgi.Request` object + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + + try: + images = self._image_service.detail(context, filters=filters, + **page_params) + except exception.Invalid as e: + raise webob.exc.HTTPBadRequest(explanation=e.format_message()) + return self._view_builder.index(req, images) + + @wsgi.serializers(xml=ImagesTemplate) + def detail(self, req): + """Return a detailed index listing of images available to the request. + + :param req: `wsgi.Request` object. + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + try: + images = self._image_service.detail(context, filters=filters, + **page_params) + except exception.Invalid as e: + raise webob.exc.HTTPBadRequest(explanation=e.format_message()) + + req.cache_db_items('images', images, 'id') + return self._view_builder.detail(req, images) + + def create(self, *args, **kwargs): + raise webob.exc.HTTPMethodNotAllowed() + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_images.py b/nova/tests/api/openstack/compute/plugins/v3/test_images.py new file mode 100644 index 000000000..a35dc6e51 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_images.py @@ -0,0 +1,1336 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Tests of the new image services, both as a service layer, +and as a WSGI layer +""" + +import urlparse + +from lxml import etree +import webob + +from nova.api.openstack.compute import images +from nova.api.openstack.compute.views import images as images_view +from nova.api.openstack import xmlutil +from nova import exception +from nova.image import glance +from nova import test +from nova.tests.api.openstack import fakes +from nova.tests import matchers + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +NOW_API_FORMAT = "2010-10-11T10:30:22Z" + + +class ImagesControllerTest(test.TestCase): + """ + Test of the OpenStack API /images application controller w/Glance. + """ + + def setUp(self): + """Run before each test.""" + super(ImagesControllerTest, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) + fakes.stub_out_compute_api_backup(self.stubs) + fakes.stub_out_glance(self.stubs) + + self.controller = images.Controller() + + def test_get_image(self): + fake_req = fakes.HTTPRequest.blank('/v2/fake/images/123') + actual_image = self.controller.show(fake_req, '124') + + href = "http://localhost/v2/fake/images/124" + bookmark = "http://localhost/fake/images/124" + alternate = "%s/fake/images/124" % glance.generate_glance_url() + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/fake/servers/" + server_uuid + server_bookmark = "http://localhost/fake/servers/" + server_uuid + + expected_image = { + "image": { + "id": "124", + "name": "queued snapshot", + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, + "status": "SAVING", + "progress": 25, + "minDisk": 0, + "minRam": 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "metadata": { + "instance_uuid": server_uuid, + "user_id": "fake", + }, + "links": [{ + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate + }], + }, + } + + self.assertThat(actual_image, matchers.DictMatches(expected_image)) + + def test_get_image_with_custom_prefix(self): + self.flags(osapi_compute_link_prefix='https://zoo.com:42', + osapi_glance_link_prefix='http://circus.com:34') + fake_req = fakes.HTTPRequest.blank('/v2/fake/images/123') + actual_image = self.controller.show(fake_req, '124') + href = "https://zoo.com:42/v2/fake/images/124" + bookmark = "https://zoo.com:42/fake/images/124" + alternate = "http://circus.com:34/fake/images/124" + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "https://zoo.com:42/v2/fake/servers/" + server_uuid + server_bookmark = "https://zoo.com:42/fake/servers/" + server_uuid + + expected_image = { + "image": { + "id": "124", + "name": "queued snapshot", + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, + "status": "SAVING", + "progress": 25, + "minDisk": 0, + "minRam": 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "metadata": { + "instance_uuid": server_uuid, + "user_id": "fake", + }, + "links": [{ + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate + }], + }, + } + self.assertThat(actual_image, matchers.DictMatches(expected_image)) + + def test_get_image_404(self): + fake_req = fakes.HTTPRequest.blank('/v2/fake/images/unknown') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, fake_req, 'unknown') + + def test_get_image_details(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/detail') + response = self.controller.detail(request) + response_list = response["images"] + + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/fake/servers/" + server_uuid + server_bookmark = "http://localhost/fake/servers/" + server_uuid + alternate = "%s/fake/images/%s" + + expected = [{ + 'id': '123', + 'name': 'public image', + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 10, + 'minRam': 128, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (glance.generate_glance_url(), 123), + }], + }, + { + 'id': '124', + 'name': 'queued snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 25, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (glance.generate_glance_url(), 124), + }], + }, + { + 'id': '125', + 'name': 'saving snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 50, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % glance.generate_glance_url() + }], + }, + { + 'id': '126', + 'name': 'active snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % glance.generate_glance_url() + }], + }, + { + 'id': '127', + 'name': 'killed snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % glance.generate_glance_url() + }], + }, + { + 'id': '128', + 'name': 'deleted snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % glance.generate_glance_url() + }], + }, + { + 'id': '129', + 'name': 'pending_delete snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % glance.generate_glance_url() + }], + }, + { + 'id': '130', + 'name': None, + 'metadata': {}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 0, + 'minRam': 0, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/130", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/130", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % glance.generate_glance_url() + }], + }, + ] + + self.assertThat(expected, matchers.DictListMatches(response_list)) + + def test_get_image_details_with_limit(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?limit=2') + response = self.controller.detail(request) + response_list = response["images"] + response_links = response["images_links"] + + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/fake/servers/" + server_uuid + server_bookmark = "http://localhost/fake/servers/" + server_uuid + alternate = "%s/fake/images/%s" + + expected = [{ + 'id': '123', + 'name': 'public image', + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'minDisk': 10, + 'progress': 100, + 'minRam': 128, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (glance.generate_glance_url(), 123), + }], + }, + { + 'id': '124', + 'name': 'queued snapshot', + 'metadata': { + u'instance_uuid': server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'minDisk': 0, + 'progress': 25, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (glance.generate_glance_url(), 124), + }], + }] + + self.assertThat(expected, matchers.DictListMatches(response_list)) + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/v2/fake/images', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + + self.assertThat({'limit': ['2'], 'marker': ['124']}, + matchers.DictMatches(params)) + + def test_image_detail_filter_with_name(self): + image_service = self.mox.CreateMockAnything() + filters = {'name': 'testname'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?name=testname') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_with_status(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'active'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?status=ACTIVE') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_with_property(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-test': '3'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?property-test=3') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_server_href(self): + image_service = self.mox.CreateMockAnything() + uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' + ref = 'http://localhost:8774/servers/' + uuid + url = '/v2/fake/images/detail?server=' + ref + filters = {'property-instance_uuid': uuid} + request = fakes.HTTPRequest.blank(url) + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_server_uuid(self): + image_service = self.mox.CreateMockAnything() + uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' + url = '/v2/fake/images/detail?server=' + uuid + filters = {'property-instance_uuid': uuid} + request = fakes.HTTPRequest.blank(url) + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_changes_since(self): + image_service = self.mox.CreateMockAnything() + filters = {'changes-since': '2011-01-24T17:08Z'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?changes-since=2011-01-24T17:08Z') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_with_type(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-image_type': 'BASE'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?type=BASE') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_filter_not_supported(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'active'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?status=' + 'ACTIVE&UNSUPPORTEDFILTER=testname') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_no_filters(self): + image_service = self.mox.CreateMockAnything() + filters = {} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + + def test_image_detail_invalid_marker(self): + class InvalidImageService(object): + + def detail(self, *args, **kwargs): + raise exception.Invalid('meow') + + request = fakes.HTTPRequest.blank('/v2/images?marker=invalid') + controller = images.Controller(image_service=InvalidImageService()) + self.assertRaises(webob.exc.HTTPBadRequest, controller.detail, request) + + def test_generate_alternate_link(self): + view = images_view.ViewBuilder() + request = fakes.HTTPRequest.blank('/v2/fake/images/1') + generated_url = view._get_alternate_link(request, 1) + actual_url = "%s/fake/images/1" % glance.generate_glance_url() + self.assertEqual(generated_url, actual_url) + + def test_delete_image(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/124') + request.method = 'DELETE' + response = self.controller.delete(request, '124') + self.assertEqual(response.status_int, 204) + + def test_delete_deleted_image(self): + """If you try to delete a deleted image, you get back 403 Forbidden.""" + + deleted_image_id = 128 + # see nova.tests.api.openstack.fakes:_make_image_fixtures + + request = fakes.HTTPRequest.blank( + '/v2/fake/images/%s' % deleted_image_id) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, + request, '%s' % deleted_image_id) + + def test_delete_image_not_found(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/300') + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, '300') + + +class ImageXMLSerializationTest(test.TestCase): + + TIMESTAMP = "2010-10-11T10:30:22Z" + SERVER_UUID = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74' + SERVER_HREF = 'http://localhost/v2/fake/servers/' + SERVER_UUID + SERVER_BOOKMARK = 'http://localhost/fake/servers/' + SERVER_UUID + IMAGE_HREF = 'http://localhost/v2/fake/images/%s' + IMAGE_NEXT = 'http://localhost/v2/fake/images?limit=%s&marker=%s' + IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' + + def test_xml_declaration(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_show(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minRam': 10, + 'minDisk': 100, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_zero_metadata(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': {}, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_image_no_metadata_key(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_no_server(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root, None) + + def test_show_with_min_ram(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minRam': 256, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minRam']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_with_min_disk(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minDisk': 5, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minDisk']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index(self): + serializer = images.MinimalImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images_index') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index_with_links(self): + serializer = images.MinimalImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ], + 'images_links': [ + { + 'rel': 'next', + 'href': self.IMAGE_NEXT % (2, 2), + } + ], + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images_index') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + # Check images_links + images_links = root.findall('{0}link'.format(ATOMNS)) + for i, link in enumerate(fixture['images_links']): + for key, value in link.items(): + self.assertEqual(images_links[i].get(key), value) + + def test_index_zero_images(self): + serializer = images.MinimalImagesTemplate() + + fixtures = { + 'images': [], + } + + output = serializer.serialize(fixtures) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images_index') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 0) + + def test_detail(self): + serializer = images.ImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': '2', + 'name': 'Image2', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'SAVING', + 'progress': 80, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) |
