diff --git a/doc/api_samples/versions-get-resp.json b/doc/api_samples/versions-get-resp.json index 8bcc7f4f2075..40c7c4add158 100644 --- a/doc/api_samples/versions-get-resp.json +++ b/doc/api_samples/versions-get-resp.json @@ -10,6 +10,17 @@ ], "status": "CURRENT", "updated": "2011-01-21T11:33:21Z" + }, + { + "id": "v3.0", + "links": [ + { + "href": "http://openstack.example.com/v3/", + "rel": "self" + } + ], + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z" } ] -} \ No newline at end of file +} diff --git a/doc/api_samples/versions-get-resp.xml b/doc/api_samples/versions-get-resp.xml index d0cea0cd55de..cf53b0dd2c07 100644 --- a/doc/api_samples/versions-get-resp.xml +++ b/doc/api_samples/versions-get-resp.xml @@ -3,4 +3,7 @@ - \ No newline at end of file + + + + diff --git a/nova/api/openstack/compute/plugins/v3/versions.py b/nova/api/openstack/compute/plugins/v3/versions.py new file mode 100644 index 000000000000..8e8f5d71b63e --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/versions.py @@ -0,0 +1,57 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 IBM Corp. +# 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. + +from nova.api.openstack.compute import versions +from nova.api.openstack.compute.views import versions as views_versions +from nova.api.openstack import extensions +from nova.api.openstack import wsgi + + +ALIAS = "versions" + + +class VersionsController(object): + @extensions.expected_errors(()) + @wsgi.serializers(xml=versions.VersionTemplate, + atom=versions.VersionAtomSerializer) + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(versions.VERSIONS['v3.0']) + + +class Versions(extensions.V3APIExtensionBase): + """API Version information.""" + + name = "Versions" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/core/versions/v3" + version = 1 + + def get_resources(self): + resources = [ + extensions.ResourceExtension(ALIAS, VersionsController(), + custom_routes_fn=self.version_map)] + return resources + + def get_controller_extensions(self): + return [] + + def version_map(self, mapper, wsgi_resource): + mapper.connect("versions", "/", + controller=wsgi_resource, + action='show', conditions={"method": ['GET']}) + mapper.redirect("", "/") diff --git a/nova/api/openstack/compute/versions.py b/nova/api/openstack/compute/versions.py index 0d2117053834..b93287bb6878 100644 --- a/nova/api/openstack/compute/versions.py +++ b/nova/api/openstack/compute/versions.py @@ -16,6 +16,7 @@ # under the License. from lxml import etree +from oslo.config import cfg from nova.api.openstack.compute.views import versions as views_versions from nova.api.openstack import wsgi @@ -23,6 +24,9 @@ from nova.api.openstack import xmlutil from nova.openstack.common import timeutils +CONF = cfg.CONF +CONF.import_opt('enabled', 'nova.api.openstack', group='osapi_v3') + LINKS = { 'v2.0': { 'pdf': 'http://docs.openstack.org/' @@ -30,6 +34,12 @@ LINKS = { 'wadl': 'http://docs.openstack.org/' 'api/openstack-compute/2/wadl/os-compute-2.wadl' }, + 'v3.0': { + 'pdf': 'http://docs.openstack.org/' + 'api/openstack-compute/3/os-compute-devguide-3.pdf', + 'wadl': 'http://docs.openstack.org/' + 'api/openstack-compute/3/wadl/os-compute-3.wadl' + }, } @@ -60,6 +70,33 @@ VERSIONS = { "type": "application/vnd.openstack.compute+json;version=2", } ], + }, + "v3.0": { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": LINKS['v3.0']['pdf'], + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": LINKS['v3.0']['wadl'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=3", + } + ], } } @@ -205,6 +242,8 @@ class VersionAtomSerializer(AtomSerializer): class Versions(wsgi.Resource): def __init__(self): super(Versions, self).__init__(None) + if not CONF.osapi_v3.enabled: + del VERSIONS["v3.0"] @wsgi.serializers(xml=VersionsTemplate, atom=VersionsAtomSerializer) diff --git a/nova/api/openstack/compute/views/versions.py b/nova/api/openstack/compute/views/versions.py index 7e8d15f0bbd0..336008c7ac56 100644 --- a/nova/api/openstack/compute/views/versions.py +++ b/nova/api/openstack/compute/views/versions.py @@ -44,7 +44,7 @@ class ViewBuilder(common.ViewBuilder): "links": [ { "rel": "self", - "href": self.generate_href(req.path), + "href": self.generate_href(version['id'], req.path), }, ], "media-types": version['media-types'], @@ -75,7 +75,7 @@ class ViewBuilder(common.ViewBuilder): def _build_links(self, version_data): """Generate a container of links that refer to the provided version.""" - href = self.generate_href() + href = self.generate_href(version_data['id']) links = [ { @@ -86,10 +86,14 @@ class ViewBuilder(common.ViewBuilder): return links - def generate_href(self, path=None): + def generate_href(self, version, path=None): """Create an url that refers to a specific version_number.""" prefix = self._update_compute_link_prefix(self.base_url) - version_number = 'v2' + if version.find('v3.') == 0: + version_number = 'v3' + else: + version_number = 'v2' + if path: path = path.strip('/') return os.path.join(prefix, version_number, path) diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_versions.py b/nova/tests/api/openstack/compute/plugins/v3/test_versions.py new file mode 100644 index 000000000000..c0ec034b2931 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_versions.py @@ -0,0 +1,246 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 IBM Corp. +# Copyright 2010-2011 OpenStack Foundation +# +# 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 feedparser +from lxml import etree +import webob + +from nova.api.openstack import xmlutil +from nova.openstack.common import jsonutils +from nova import test +from nova.tests.api.openstack import common +from nova.tests.api.openstack import fakes + + +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/common/api/v1.0' +} + + +EXP_LINKS = { + 'v3.0': { + 'pdf': 'http://docs.openstack.org/' + 'api/openstack-compute/3/os-compute-devguide-3.pdf', + 'wadl': 'http://docs.openstack.org/' + 'api/openstack-compute/3/wadl/os-compute-3.wadl' + }, +} + + +EXP_VERSIONS = { + "v3.0": { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": EXP_LINKS['v3.0']['pdf'], + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": EXP_LINKS['v3.0']['wadl'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=3", + } + ], + } +} + + +class VersionsTest(test.TestCase): + + def test_get_version_list_302(self): + req = webob.Request.blank('/v3') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v3()) + self.assertEqual(res.status_int, 302) + redirect_req = webob.Request.blank('/v3/') + self.assertEqual(res.location, redirect_req.url) + + def test_get_version_3_detail(self): + req = webob.Request.blank('/v3/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v3()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = { + "version": { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/", + }, + ], + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/", + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": EXP_LINKS['v3.0']['pdf'], + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": EXP_LINKS['v3.0']['wadl'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/" + "vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": "application/" + "vnd.openstack.compute+json;version=3", + }, + ], + }, + } + self.assertEqual(expected, version) + + def test_get_version_3_detail_content_type(self): + req = webob.Request.blank('/') + req.accept = "application/json;version=3" + res = req.get_response(fakes.wsgi_app_v3()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = { + "version": { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/", + }, + ], + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/", + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": EXP_LINKS['v3.0']['pdf'], + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": EXP_LINKS['v3.0']['wadl'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/" + "vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": "application/" + "vnd.openstack.compute+json;version=3", + }, + ], + }, + } + self.assertEqual(expected, version) + + def test_get_version_3_detail_xml(self): + req = webob.Request.blank('/v3/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app_v3()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + + version = etree.XML(res.body) + xmlutil.validate_schema(version, 'version') + + expected = EXP_VERSIONS['v3.0'] + self.assertTrue(version.xpath('/ns:version', namespaces=NS)) + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + expected['media-types'])) + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v3/'}] + + expected['links'])) + + def test_get_version_3_detail_atom(self): + req = webob.Request.blank('/v3/') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app_v3()) + self.assertEqual(res.status_int, 200) + self.assertEqual("application/atom+xml", res.content_type) + + xmlutil.validate_schema(etree.XML(res.body), 'atom') + + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2013-07-23T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v3/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v3/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v3/') + self.assertEqual(entry.title, 'Version v3.0') + self.assertEqual(entry.updated, '2013-07-23T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v3.0 EXPERIMENTAL (2013-07-23T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v3/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'href': EXP_LINKS['v3.0']['pdf'], + 'type': 'application/pdf', + 'rel': 'describedby'}) + self.assertEqual(entry.links[2], { + 'href': EXP_LINKS['v3.0']['wadl'], + 'type': 'application/vnd.sun.wadl+xml', + 'rel': 'describedby'}) diff --git a/nova/tests/api/openstack/compute/test_versions.py b/nova/tests/api/openstack/compute/test_versions.py index 973fd0105f22..66699aa8e118 100644 --- a/nova/tests/api/openstack/compute/test_versions.py +++ b/nova/tests/api/openstack/compute/test_versions.py @@ -75,6 +75,21 @@ EXP_VERSIONS = { }, ], }, + "v3.0": { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=3", + } + ], + } } @@ -98,6 +113,16 @@ class VersionsTest(test.TestCase): "href": "http://localhost/v2/", }], }, + { + "id": "v3.0", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/", + }], + }, ] self.assertEqual(versions, expected) @@ -232,9 +257,9 @@ class VersionsTest(test.TestCase): self.assertTrue(root.xpath('/ns:versions', namespaces=NS)) versions = root.xpath('ns:version', namespaces=NS) - self.assertEqual(len(versions), 1) + self.assertEqual(len(versions), 2) - for i, v in enumerate(['v2.0']): + for i, v in enumerate(['v2.0', 'v3.0']): version = versions[i] expected = EXP_VERSIONS[v] for key in ['id', 'status', 'updated']: @@ -291,7 +316,7 @@ class VersionsTest(test.TestCase): f = feedparser.parse(res.body) self.assertEqual(f.feed.title, 'Available API Versions') - self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.updated, '2013-07-23T11:33:21Z') self.assertEqual(f.feed.id, 'http://localhost/') self.assertEqual(f.feed.author, 'Rackspace') self.assertEqual(f.feed.author_detail.href, @@ -299,7 +324,7 @@ class VersionsTest(test.TestCase): self.assertEqual(f.feed.links[0]['href'], 'http://localhost/') self.assertEqual(f.feed.links[0]['rel'], 'self') - self.assertEqual(len(f.entries), 1) + self.assertEqual(len(f.entries), 2) entry = f.entries[0] self.assertEqual(entry.id, 'http://localhost/v2/') self.assertEqual(entry.title, 'Version v2.0') @@ -311,6 +336,17 @@ class VersionsTest(test.TestCase): self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') self.assertEqual(entry.links[0]['rel'], 'self') + entry = f.entries[1] + self.assertEqual(entry.id, 'http://localhost/v3/') + self.assertEqual(entry.title, 'Version v3.0') + self.assertEqual(entry.updated, '2013-07-23T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v3.0 EXPERIMENTAL (2013-07-23T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v3/') + self.assertEqual(entry.links[0]['rel'], 'self') + def test_multi_choice_image(self): req = webob.Request.blank('/images/1') req.accept = "application/json" @@ -320,6 +356,28 @@ class VersionsTest(test.TestCase): expected = { "choices": [ + { + "id": "v3.0", + "status": "EXPERIMENTAL", + "links": [ + { + "href": "http://localhost/v3/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": + "application/vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": + "application/vnd.openstack.compute+json;version=3", + } + ], + }, { "id": "v2.0", "status": "CURRENT", @@ -357,9 +415,9 @@ class VersionsTest(test.TestCase): root = etree.XML(res.body) self.assertTrue(root.xpath('/ns:choices', namespaces=NS)) versions = root.xpath('ns:version', namespaces=NS) - self.assertEqual(len(versions), 1) + self.assertEqual(len(versions), 2) - version = versions[0] + version = versions[1] self.assertEqual(version.get('id'), 'v2.0') self.assertEqual(version.get('status'), 'CURRENT') media_types = version.xpath('ns:media-types/ns:media-type', @@ -373,6 +431,20 @@ class VersionsTest(test.TestCase): self.assertTrue(common.compare_links(links, [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}])) + version = versions[0] + self.assertEqual(version.get('id'), 'v3.0') + self.assertEqual(version.get('status'), 'EXPERIMENTAL') + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common. + compare_media_types(media_types, + EXP_VERSIONS['v3.0']['media-types'] + )) + + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v3/images/1'}])) + def test_multi_choice_server_atom(self): """ Make sure multi choice responses do not have content-type @@ -394,6 +466,28 @@ class VersionsTest(test.TestCase): expected = { "choices": [ + { + "id": "v3.0", + "status": "EXPERIMENTAL", + "links": [ + { + "href": "http://localhost/v3/servers/" + uuid, + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": + "application/vnd.openstack.compute+xml;version=3", + }, + { + "base": "application/json", + "type": + "application/vnd.openstack.compute+json;version=3", + } + ], + }, { "id": "v2.0", "status": "CURRENT", @@ -461,7 +555,27 @@ class VersionsViewBuilderTests(test.TestCase): expected = "http://example.org/app/v2/" builder = views.versions.ViewBuilder(base_url) - actual = builder.generate_href() + actual = builder.generate_href('v2') + + self.assertEqual(actual, expected) + + def test_generate_href_v3(self): + base_url = "http://example.org/app/" + + expected = "http://example.org/app/v3/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href('v3.0') + + self.assertEqual(actual, expected) + + def test_generate_href_unknown(self): + base_url = "http://example.org/app/" + + expected = "http://example.org/app/v2/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href('foo') self.assertEqual(actual, expected) diff --git a/nova/tests/integrated/api_samples/versions-get-resp.json.tpl b/nova/tests/integrated/api_samples/versions-get-resp.json.tpl index b5b20d6b9d28..40c7c4add158 100644 --- a/nova/tests/integrated/api_samples/versions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/versions-get-resp.json.tpl @@ -4,12 +4,23 @@ "id": "v2.0", "links": [ { - "href": "%(host)s/v2/", + "href": "http://openstack.example.com/v2/", "rel": "self" } ], "status": "CURRENT", "updated": "2011-01-21T11:33:21Z" + }, + { + "id": "v3.0", + "links": [ + { + "href": "http://openstack.example.com/v3/", + "rel": "self" + } + ], + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z" } ] } diff --git a/nova/tests/integrated/api_samples/versions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/versions-get-resp.xml.tpl index b426f9521521..cf53b0dd2c07 100644 --- a/nova/tests/integrated/api_samples/versions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/versions-get-resp.xml.tpl @@ -3,4 +3,7 @@ + + + diff --git a/setup.cfg b/setup.cfg index df7d33995869..3ebfa9988f32 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,6 +106,7 @@ nova.api.v3.extensions = services = nova.api.openstack.compute.plugins.v3.services:Services simple_tenant_usage = nova.api.openstack.compute.plugins.v3.simple_tenant_usage:SimpleTenantUsage used_limits = nova.api.openstack.compute.plugins.v3.used_limits:UsedLimits + versions = nova.api.openstack.compute.plugins.v3.versions:Versions nova.api.v3.extensions.server.create = availability_zone = nova.api.openstack.compute.plugins.v3.availability_zone:AvailabilityZone