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