Merge "Adds API version discovery support for V3"

This commit is contained in:
Jenkins 2013-09-02 10:34:02 +00:00 committed by Gerrit Code Review
commit 88fb94f5fa
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