summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Erdfelt <johannes.erdfelt@rackspace.com>2011-09-25 22:51:55 +0000
committerJohannes Erdfelt <johannes.erdfelt@rackspace.com>2011-10-04 14:44:34 +0000
commit6e5f2d88e630540b0ab121cd9949289d2fc0cd67 (patch)
tree1da9cb982a4452025e75c68a1cafb7aeed32c9b3
parent3837f09ee0b7b0da23e1caa185f58610d30bffe6 (diff)
downloadnova-6e5f2d88e630540b0ab121cd9949289d2fc0cd67.tar.gz
nova-6e5f2d88e630540b0ab121cd9949289d2fc0cd67.tar.xz
nova-6e5f2d88e630540b0ab121cd9949289d2fc0cd67.zip
Add support for header version parameter to specify API version.
bug 844905 The 1.1 API specifies that the API version can be determined by URL path (eg /v1.1/tenant/servers/detail), Content-Type header (eg application/json;version=1.1) or Accept header (eg application/json;q=0.8;version=1.1, application/xml;q=0.2;version=1.1). Change-Id: I01220cf1eebc0f759d66563ec67ef2f697c6d310
-rw-r--r--etc/nova/api-paste.ini2
-rw-r--r--nova/api/openstack/urlmap.py297
-rw-r--r--nova/api/openstack/versions.py33
-rw-r--r--nova/api/openstack/wsgi.py44
-rw-r--r--nova/tests/api/openstack/fakes.py2
-rw-r--r--nova/tests/api/openstack/test_urlmap.py111
-rw-r--r--nova/tests/api/openstack/test_versions.py98
-rw-r--r--tools/pip-requires2
8 files changed, 515 insertions, 74 deletions
diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini
index 8555f6ce5..d38ad92d9 100644
--- a/etc/nova/api-paste.ini
+++ b/etc/nova/api-paste.ini
@@ -71,7 +71,7 @@ paste.app_factory = nova.api.ec2.metadatarequesthandler:MetadataRequestHandler.f
#############
[composite:osapi]
-use = egg:Paste#urlmap
+use = call:nova.api.openstack.urlmap:urlmap_factory
/: osversions
/v1.0: openstackapi10
/v1.1: openstackapi11
diff --git a/nova/api/openstack/urlmap.py b/nova/api/openstack/urlmap.py
new file mode 100644
index 000000000..199fa6130
--- /dev/null
+++ b/nova/api/openstack/urlmap.py
@@ -0,0 +1,297 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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 paste.urlmap
+import re
+import urllib2
+
+from nova import log as logging
+from nova.api.openstack import wsgi
+
+
+_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
+_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*'
+ r'(?:=\s*([^;]+|%s))?\s*' %
+ (_quoted_string_re, _quoted_string_re))
+
+LOG = logging.getLogger('nova.api.openstack.map')
+
+
+def unquote_header_value(value):
+ """Unquotes a header value.
+ This does not use the real unquoting but what browsers are actually
+ using for quoting.
+
+ :param value: the header value to unquote.
+ """
+ if value and value[0] == value[-1] == '"':
+ # this is not the real unquoting, but fixing this so that the
+ # RFC is met will result in bugs with internet explorer and
+ # probably some other browsers as well. IE for example is
+ # uploading files with "C:\foo\bar.txt" as filename
+ value = value[1:-1]
+ return value
+
+
+def parse_list_header(value):
+ """Parse lists as described by RFC 2068 Section 2.
+
+ In particular, parse comma-separated lists where the elements of
+ the list may include quoted-strings. A quoted-string could
+ contain a comma. A non-quoted string could have quotes in the
+ middle. Quotes are removed automatically after parsing.
+
+ The return value is a standard :class:`list`:
+
+ >>> parse_list_header('token, "quoted value"')
+ ['token', 'quoted value']
+
+ :param value: a string with a list header.
+ :return: :class:`list`
+ """
+ result = []
+ for item in urllib2.parse_http_list(value):
+ if item[:1] == item[-1:] == '"':
+ item = unquote_header_value(item[1:-1])
+ result.append(item)
+ return result
+
+
+def parse_options_header(value):
+ """Parse a ``Content-Type`` like header into a tuple with the content
+ type and the options:
+
+ >>> parse_options_header('Content-Type: text/html; mimetype=text/html')
+ ('Content-Type:', {'mimetype': 'text/html'})
+
+ :param value: the header to parse.
+ :return: (str, options)
+ """
+ def _tokenize(string):
+ for match in _option_header_piece_re.finditer(string):
+ key, value = match.groups()
+ key = unquote_header_value(key)
+ if value is not None:
+ value = unquote_header_value(value)
+ yield key, value
+
+ if not value:
+ return '', {}
+
+ parts = _tokenize(';' + value)
+ name = parts.next()[0]
+ extra = dict(parts)
+ return name, extra
+
+
+class Accept(object):
+ def __init__(self, value):
+ self._content_types = [parse_options_header(v) for v in
+ parse_list_header(value)]
+
+ def best_match(self, supported_content_types):
+ # FIXME: Should we have a more sophisticated matching algorithm that
+ # takes into account the version as well?
+ best_quality = -1
+ best_content_type = None
+ best_params = {}
+ best_match = '*/*'
+
+ for content_type in supported_content_types:
+ for content_mask, params in self._content_types:
+ try:
+ quality = float(params.get('q', 1))
+ except ValueError:
+ continue
+
+ if quality < best_quality:
+ continue
+ elif best_quality == quality:
+ if best_match.count('*') <= content_mask.count('*'):
+ continue
+
+ if self._match_mask(content_mask, content_type):
+ best_quality = quality
+ best_content_type = content_type
+ best_params = params
+ best_match = content_mask
+
+ return best_content_type, best_params
+
+ def content_type_params(self, best_content_type):
+ """Find parameters in Accept header for given content type."""
+ for content_type, params in self._content_types:
+ if best_content_type == content_type:
+ return params
+
+ return {}
+
+ def _match_mask(self, mask, content_type):
+ if '*' not in mask:
+ return content_type == mask
+ if mask == '*/*':
+ return True
+ mask_major = mask[:-2]
+ content_type_major = content_type.split('/', 1)[0]
+ return content_type_major == mask_major
+
+
+def urlmap_factory(loader, global_conf, **local_conf):
+ if 'not_found_app' in local_conf:
+ not_found_app = local_conf.pop('not_found_app')
+ else:
+ not_found_app = global_conf.get('not_found_app')
+ if not_found_app:
+ not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
+ urlmap = URLMap(not_found_app=not_found_app)
+ for path, app_name in local_conf.items():
+ path = paste.urlmap.parse_path_expression(path)
+ app = loader.get_app(app_name, global_conf=global_conf)
+ urlmap[path] = app
+ return urlmap
+
+
+class URLMap(paste.urlmap.URLMap):
+ def _match(self, host, port, path_info):
+ """Find longest match for a given URL path."""
+ for (domain, app_url), app in self.applications:
+ if domain and domain != host and domain != host + ':' + port:
+ continue
+ if (path_info == app_url
+ or path_info.startswith(app_url + '/')):
+ return app, app_url
+
+ return None, None
+
+ def _set_script_name(self, app, app_url):
+ def wrap(environ, start_response):
+ environ['SCRIPT_NAME'] += app_url
+ return app(environ, start_response)
+
+ return wrap
+
+ def _munge_path(self, app, path_info, app_url):
+ def wrap(environ, start_response):
+ environ['SCRIPT_NAME'] += app_url
+ environ['PATH_INFO'] = path_info[len(app_url):]
+ return app(environ, start_response)
+
+ return wrap
+
+ def _path_strategy(self, host, port, path_info):
+ """Check path suffix for MIME type and path prefix for API version."""
+ mime_type = app = app_url = None
+
+ parts = path_info.rsplit('.', 1)
+ if len(parts) > 1:
+ possible_type = 'application/' + parts[1]
+ if possible_type in wsgi.SUPPORTED_CONTENT_TYPES:
+ mime_type = possible_type
+
+ parts = path_info.split('/')
+ if len(parts) > 1:
+ possible_app, possible_app_url = self._match(host, port, path_info)
+ # Don't use prefix if it ends up matching default
+ if possible_app and possible_app_url:
+ app_url = possible_app_url
+ app = self._munge_path(possible_app, path_info, app_url)
+
+ return mime_type, app, app_url
+
+ def _content_type_strategy(self, host, port, environ):
+ """Check Content-Type header for API version."""
+ app = None
+ params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1]
+ if 'version' in params:
+ app, app_url = self._match(host, port, '/v' + params['version'])
+ if app:
+ app = self._set_script_name(app, app_url)
+
+ return app
+
+ def _accept_strategy(self, host, port, environ, supported_content_types):
+ """Check Accept header for best matching MIME type and API version."""
+ accept = Accept(environ.get('HTTP_ACCEPT', ''))
+
+ app = None
+
+ # Find the best match in the Accept header
+ mime_type, params = accept.best_match(supported_content_types)
+ if 'version' in params:
+ app, app_url = self._match(host, port, '/v' + params['version'])
+ if app:
+ app = self._set_script_name(app, app_url)
+
+ return mime_type, app
+
+ def __call__(self, environ, start_response):
+ host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
+ if ':' in host:
+ host, port = host.split(':', 1)
+ else:
+ if environ['wsgi.url_scheme'] == 'http':
+ port = '80'
+ else:
+ port = '443'
+
+ path_info = environ['PATH_INFO']
+ path_info = self.normalize_url(path_info, False)[1]
+
+ # The MIME type for the response is determined in one of two ways:
+ # 1) URL path suffix (eg /servers/detail.json)
+ # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2)
+
+ # The API version is determined in one of three ways:
+ # 1) URL path prefix (eg /v1.1/tenant/servers/detail)
+ # 2) Content-Type header (eg application/json;version=1.1)
+ # 3) Accept header (eg application/json;q=0.8;version=1.1)
+
+ supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES)
+
+ mime_type, app, app_url = self._path_strategy(host, port, path_info)
+
+ # Accept application/atom+xml for the index query of each API
+ # version mount point as well as the root index
+ if (app_url and app_url + '/' == path_info) or path_info == '/':
+ supported_content_types.append('application/atom+xml')
+
+ if not app:
+ app = self._content_type_strategy(host, port, environ)
+
+ if not mime_type or not app:
+ possible_mime_type, possible_app = self._accept_strategy(
+ host, port, environ, supported_content_types)
+ if possible_mime_type and not mime_type:
+ mime_type = possible_mime_type
+ if possible_app and not app:
+ app = possible_app
+
+ if not mime_type:
+ mime_type = 'application/json'
+
+ if not app:
+ # Didn't match a particular version, probably matches default
+ app, app_url = self._match(host, port, path_info)
+ if app:
+ app = self._munge_path(app, path_info, app_url)
+
+ if app:
+ environ['nova.best_content_type'] = mime_type
+ return app(environ, start_response)
+
+ environ['paste.urlmap_object'] = self
+ return self.not_found_application(environ, start_response)
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
index 75a1d0ba4..034287b10 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -47,11 +47,11 @@ VERSIONS = {
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml",
+ "type": "application/vnd.openstack.compute+xml;version=1.0",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json",
+ "type": "application/vnd.openstack.compute+json;version=1.0",
}
],
},
@@ -76,11 +76,11 @@ VERSIONS = {
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml",
+ "type": "application/vnd.openstack.compute+xml;version=1.1",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json",
+ "type": "application/vnd.openstack.compute+json;version=1.1",
}
],
},
@@ -106,13 +106,7 @@ class Versions(wsgi.Resource):
body_serializers=body_serializers,
headers_serializer=headers_serializer)
- supported_content_types = ('application/json',
- 'application/vnd.openstack.compute+json',
- 'application/xml',
- 'application/vnd.openstack.compute+xml',
- 'application/atom+xml')
- deserializer = VersionsRequestDeserializer(
- supported_content_types=supported_content_types)
+ deserializer = VersionsRequestDeserializer()
wsgi.Resource.__init__(self, None, serializer=serializer,
deserializer=deserializer)
@@ -141,15 +135,6 @@ class VersionV11(object):
class VersionsRequestDeserializer(wsgi.RequestDeserializer):
- def get_expected_content_type(self, request):
- supported_content_types = list(self.supported_content_types)
- if request.path != '/':
- # Remove atom+xml accept type for 300 responses
- if 'application/atom+xml' in supported_content_types:
- supported_content_types.remove('application/atom+xml')
-
- return request.best_match_content_type(supported_content_types)
-
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
args = {}
@@ -309,13 +294,7 @@ def create_resource(version='1.0'):
}
serializer = wsgi.ResponseSerializer(body_serializers)
- supported_content_types = ('application/json',
- 'application/vnd.openstack.compute+json',
- 'application/xml',
- 'application/vnd.openstack.compute+xml',
- 'application/atom+xml')
- deserializer = wsgi.RequestDeserializer(
- supported_content_types=supported_content_types)
+ deserializer = wsgi.RequestDeserializer()
return wsgi.Resource(controller, serializer=serializer,
deserializer=deserializer)
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 180f328b9..aa420953b 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -43,7 +43,7 @@ _CONTENT_TYPE_MAP = {
'application/vnd.openstack.compute+xml': 'application/xml',
}
-_SUPPORTED_CONTENT_TYPES = (
+SUPPORTED_CONTENT_TYPES = (
'application/json',
'application/vnd.openstack.compute+json',
'application/xml',
@@ -54,25 +54,26 @@ _SUPPORTED_CONTENT_TYPES = (
class Request(webob.Request):
"""Add some Openstack API-specific logic to the base webob.Request."""
- def best_match_content_type(self, supported_content_types=None):
- """Determine the requested response content-type.
+ def best_match_content_type(self):
+ """Determine the requested response content-type."""
+ if 'nova.best_content_type' not in self.environ:
+ # Calculate the best MIME type
+ content_type = None
- Based on the query extension then the Accept header.
+ # Check URL path suffix
+ parts = self.path.rsplit('.', 1)
+ if len(parts) > 1:
+ possible_type = 'application/' + parts[1]
+ if possible_type in SUPPORTED_CONTENT_TYPES:
+ content_type = possible_type
- """
- supported_content_types = supported_content_types or \
- _SUPPORTED_CONTENT_TYPES
-
- parts = self.path.rsplit('.', 1)
- if len(parts) > 1:
- ctype = 'application/{0}'.format(parts[1])
- if ctype in supported_content_types:
- return ctype
+ if not content_type:
+ content_type = self.accept.best_match(SUPPORTED_CONTENT_TYPES)
- bm = self.accept.best_match(supported_content_types)
+ self.environ['nova.best_content_type'] = content_type or \
+ 'application/json'
- # default to application/json if we don't find a preference
- return bm or 'application/json'
+ return self.environ['nova.best_content_type']
def get_content_type(self):
"""Determine content type of the request body.
@@ -83,7 +84,7 @@ class Request(webob.Request):
if not "Content-Type" in self.headers:
return None
- allowed_types = _SUPPORTED_CONTENT_TYPES
+ allowed_types = SUPPORTED_CONTENT_TYPES
content_type = self.content_type
if content_type not in allowed_types:
@@ -219,12 +220,7 @@ class RequestHeadersDeserializer(ActionDispatcher):
class RequestDeserializer(object):
"""Break up a Request object into more useful pieces."""
- def __init__(self, body_deserializers=None, headers_deserializer=None,
- supported_content_types=None):
-
- self.supported_content_types = supported_content_types or \
- _SUPPORTED_CONTENT_TYPES
-
+ def __init__(self, body_deserializers=None, headers_deserializer=None):
self.body_deserializers = {
'application/xml': XMLDeserializer(),
'application/json': JSONDeserializer(),
@@ -287,7 +283,7 @@ class RequestDeserializer(object):
raise exception.InvalidContentType(content_type=content_type)
def get_expected_content_type(self, request):
- return request.best_match_content_type(self.supported_content_types)
+ return request.best_match_content_type()
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index e0176c9a3..81eee216e 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -17,7 +17,6 @@
import webob
import webob.dec
-from paste import urlmap
from glance import client as glance_client
@@ -32,6 +31,7 @@ from nova.api.openstack import auth
from nova.api.openstack import extensions
from nova.api.openstack import versions
from nova.api.openstack import limits
+from nova.api.openstack import urlmap
from nova.auth.manager import User, Project
import nova.image.fake
from nova.tests.glance import stubs as glance_stubs
diff --git a/nova/tests/api/openstack/test_urlmap.py b/nova/tests/api/openstack/test_urlmap.py
new file mode 100644
index 000000000..9e7cdda75
--- /dev/null
+++ b/nova/tests/api/openstack/test_urlmap.py
@@ -0,0 +1,111 @@
+# Copyright 2011 OpenStack LLC.
+# 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 json
+import webob
+
+from nova import test
+from nova import log as logging
+from nova.tests.api.openstack import fakes
+
+LOG = logging.getLogger('nova.tests.api.openstack.test_urlmap')
+
+
+class UrlmapTest(test.TestCase):
+ def setUp(self):
+ super(UrlmapTest, self).setUp()
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ def test_path_version_v1_0(self):
+ """Test URL path specifying v1.0 returns v1.0 content."""
+ req = webob.Request.blank('/v1.0/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.0')
+
+ def test_path_version_v1_1(self):
+ """Test URL path specifying v1.1 returns v1.1 content."""
+ req = webob.Request.blank('/v1.1/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.1')
+
+ def test_content_type_version_v1_0(self):
+ """Test Content-Type specifying v1.0 returns v1.0 content."""
+ req = webob.Request.blank('/')
+ req.content_type = "application/json;version=1.0"
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.0')
+
+ def test_content_type_version_v1_1(self):
+ """Test Content-Type specifying v1.1 returns v1.1 content."""
+ req = webob.Request.blank('/')
+ req.content_type = "application/json;version=1.1"
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.1')
+
+ def test_accept_version_v1_0(self):
+ """Test Accept header specifying v1.0 returns v1.0 content."""
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=1.0"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.0')
+
+ def test_accept_version_v1_1(self):
+ """Test Accept header specifying v1.1 returns v1.1 content."""
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=1.1"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v1.1')
+
+ def test_path_content_type(self):
+ """Test URL path specifying JSON returns JSON content."""
+ req = webob.Request.blank('/v1.1/foobar/images/1.json')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['image']['id'], '1')
+
+ def test_accept_content_type(self):
+ """Test Accept header specifying JSON returns JSON content."""
+ req = webob.Request.blank('/v1.1/foobar/images/1')
+ req.accept = "application/xml;q=0.8, application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = json.loads(res.body)
+ self.assertEqual(body['image']['id'], '1')
diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py
index 1ae3789d9..375470a64 100644
--- a/nova/tests/api/openstack/test_versions.py
+++ b/nova/tests/api/openstack/test_versions.py
@@ -56,11 +56,11 @@ VERSIONS = {
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml",
+ "type": "application/vnd.openstack.compute+xml;version=1.0",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json",
+ "type": "application/vnd.openstack.compute+json;version=1.0",
},
],
},
@@ -85,11 +85,11 @@ VERSIONS = {
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml",
+ "type": "application/vnd.openstack.compute+xml;version=1.1",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json",
+ "type": "application/vnd.openstack.compute+json;version=1.1",
},
],
},
@@ -175,12 +175,12 @@ class VersionsTest(test.TestCase):
{
"base": "application/xml",
"type": "application/"
- "vnd.openstack.compute-v1.0+xml",
+ "vnd.openstack.compute+xml;version=1.0",
},
{
"base": "application/json",
"type": "application/"
- "vnd.openstack.compute-v1.0+json",
+ "vnd.openstack.compute+json;version=1.0",
},
],
},
@@ -221,12 +221,58 @@ class VersionsTest(test.TestCase):
{
"base": "application/xml",
"type": "application/"
- "vnd.openstack.compute-v1.1+xml",
+ "vnd.openstack.compute+xml;version=1.1",
},
{
"base": "application/json",
"type": "application/"
- "vnd.openstack.compute-v1.1+json",
+ "vnd.openstack.compute+json;version=1.1",
+ },
+ ],
+ },
+ }
+ self.assertEqual(expected, version)
+
+ def test_get_version_1_1_detail_content_type(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=1.1"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = json.loads(res.body)
+ expected = {
+ "version": {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v1.1/",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/"
+ "vnd.openstack.compute+xml;version=1.1",
+ },
+ {
+ "base": "application/json",
+ "type": "application/"
+ "vnd.openstack.compute+json;version=1.1",
},
],
},
@@ -445,11 +491,13 @@ class VersionsTest(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml"
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.1"
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json"
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.1"
},
],
},
@@ -465,11 +513,13 @@ class VersionsTest(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml"
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.0"
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json"
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.0"
},
],
},
@@ -543,11 +593,13 @@ class VersionsTest(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml"
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.1"
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json"
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.1"
},
],
},
@@ -563,11 +615,13 @@ class VersionsTest(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml"
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.0"
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json"
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.0"
},
],
},
@@ -727,11 +781,13 @@ class VersionsSerializerTests(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.0+xml",
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.0",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.0+json",
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.0",
},
],
},
@@ -831,11 +887,13 @@ class VersionsSerializerTests(test.TestCase):
"media-types": [
{
"base": "application/xml",
- "type": "application/vnd.openstack.compute-v1.1+xml",
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=1.1",
},
{
"base": "application/json",
- "type": "application/vnd.openstack.compute-v1.1+json",
+ "type": "application/vnd.openstack.compute+json"
+ ";version=1.1",
}
],
},
diff --git a/tools/pip-requires b/tools/pip-requires
index f1cbf802c..85ca9b6db 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -21,7 +21,7 @@ wsgiref==0.1.2
mox==0.5.3
greenlet==0.3.1
nose
-PasteDeploy
+PasteDeploy==1.5.0
paste
sqlalchemy-migrate
netaddr