Adds API version discovery support for V3

Adds version information for the V3 API which is only displayed
when the V3 API is enabled. Even if the the V3 API is enabled the
V3 API status is "EXPERIMENTAL" and the V2 one "CURRENT". This was
done so autodiscovery tools would not yet use the V3 version by
default.

Ports the relevant parts of the version extension and associated
tests to the V3 API to display V3 version information for /v3 GET
requests.

DocImpact

Partially implements blueprint nova-v3-api

Change-Id: Idd335ce0df63d91e94a4a757f1fbae94b576c37e
This commit is contained in:
Chris Yeoh
2013-07-24 12:51:22 +09:30
parent d3837ce0da
commit a1baa247a4
10 changed files with 503 additions and 14 deletions

View File

@@ -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"
}
]
}
}

View File

@@ -3,4 +3,7 @@
<version status="CURRENT" updated="2011-01-21T11:33:21Z" id="v2.0">
<atom:link href="http://openstack.example.com/v2/" rel="self"/>
</version>
</versions>
<version status="EXPERIMENTAL" updated="2013-07-23T11:33:21Z" id="v3.0">
<atom:link href="http://openstack.example.com/v3/" rel="self"/>
</version>
</versions>

View File

@@ -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("", "/")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'})

View File

@@ -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)

View File

@@ -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"
}
]
}

View File

@@ -3,4 +3,7 @@
<version status="CURRENT" updated="2011-01-21T11:33:21Z" id="v2.0">
<atom:link href="http://openstack.example.com/v2/" rel="self"/>
</version>
<version status="EXPERIMENTAL" updated="2013-07-23T11:33:21Z" id="v3.0">
<atom:link href="http://openstack.example.com/v3/" rel="self"/>
</version>
</versions>

View File

@@ -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