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
This commit is contained in:
parent
3837f09ee0
commit
6e5f2d88e6
@ -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
|
||||
|
297
nova/api/openstack/urlmap.py
Normal file
297
nova/api/openstack/urlmap.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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
|
||||
if not content_type:
|
||||
content_type = self.accept.best_match(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
|
||||
self.environ['nova.best_content_type'] = content_type or \
|
||||
'application/json'
|
||||
|
||||
bm = self.accept.best_match(supported_content_types)
|
||||
|
||||
# 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."""
|
||||
|
@ -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
|
||||
|
111
nova/tests/api/openstack/test_urlmap.py
Normal file
111
nova/tests/api/openstack/test_urlmap.py
Normal file
@ -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')
|
@ -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",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user