Merge lp:~johannes.erdfelt/nova/lp844905 into lp:~hudson-openstack/nova/trunk

Proposed by Johannes Erdfelt
Status: Rejected
Rejected by: Brian Waldon
Proposed branch: lp:~johannes.erdfelt/nova/lp844905
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 827 lines (+522/-81)
7 files modified
etc/nova/api-paste.ini (+1/-1)
nova/api/openstack/urlmap.py (+297/-0)
nova/api/openstack/versions.py (+6/-27)
nova/api/openstack/wsgi.py (+24/-28)
nova/tests/api/openstack/fakes.py (+1/-1)
nova/tests/api/openstack/test_urlmap.py (+111/-0)
nova/tests/api/openstack/test_versions.py (+82/-24)
To merge this branch: bzr merge lp:~johannes.erdfelt/nova/lp844905
Reviewer Review Type Date Requested Status
Brian Waldon (community) Approve
Paul Voccio (community) Abstain
Review via email: mp+76439@code.launchpad.net

Commit message

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

This branch adds support for looking at the Content-Type and Accept headers for the API version to use.

Description of the change

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

This branch adds support for looking at the Content-Type and Accept headers for the API version to use.

This merge proposal is more of a code review than a claim than the branch is ready. I'd like to hear opinions on the direction of the branch.

In particular:
* The approach I took using a custom URLMap class to integrate into paste deploy. It has the benefit of still allowing the paste config to enable/disable API versions. It subclasses a paste class, so it may end up being fragile in the long term.
* To support this correctly, the accepted content type needs to be determined much earlier than before (so we get the right version specified by the MIME type in the Accept header). Unfortunately, there are a couple of resources that define an additional supported content type of application/atom+xml. Since we determine the accepted content type before we call the resource, this leaves us in a difficult position. The branch semi-hard codes it. If anyone has a better idea, I'm open for discussion.
* Much of the current code tests like to skip the paste deploy part of the WSGI stack. Also the direct API doesn't use paste deploy. As a result, I left in code to determine the accepted content type at a later point. I originally went down the path of changing the rest of the code, but that resulted in a set of huge changes, but also some corner cases where only part of the WSGI stack is intentionally tested.

To post a comment you must log in.
lp:~johannes.erdfelt/nova/lp844905 updated
1588. By Johannes Erdfelt

Remove hardcoded content type now that the code handles direct API again

1589. By Johannes Erdfelt

Remove FIXME now that it has a workaround

1590. By Johannes Erdfelt

Cleanup docstrings
Remove is_filename option, it's not necessary for us and is a hack anyway

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Finding it hard to see that the new code is being properly covered by the tests.

Ideally I'd like to see explicit unit tests on each of the new methods added (and ensuring they hit each of the conditional branches).

At the very least, some description in the unit tests as to what they are actually testing and which methods are being touched. But ideally, simple single-method unit tests.

Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

I'm not sure I understand what needs to be changed.

The unit tests check the two different methods to determine the best content type and the three different methods to determine the API version.

Each unit test checks a single piece of functionality, so I'm not sure I understand what you mean by "single-method unit tests".

Are you saying some code isn't being tested? The existing code being tested isn't tested well enough?

lp:~johannes.erdfelt/nova/lp844905 updated
1591. By Johannes Erdfelt

Add docstrings to tests to better document the intent

Revision history for this message
Paul Voccio (pvo) wrote :

Wanted to echo removing comments and some of the formatting the others mentioned here.

I got some 3 failures with the S3APITestCase. Not sure if this is related or not yet.

Please also add yourself to the Authors file. This failed on the unit tests for me.

======================================================================
FAIL: test_authors_up_to_date (nova.tests.test_misc.ProjectTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/bzr/TrustedComputingPools/nova/tests/test_misc.py", line 93, in test_authors_up_to_date
    '%r not listed in Authors' % missing)
AssertionError: set([u'<email address hidden>']) not listed in Authors

review: Needs Fixing
Revision history for this message
Paul Voccio (pvo) wrote :

meh, too many windows open.

review: Abstain
Revision history for this message
Brian Waldon (bcwaldon) wrote :

I'm very curious why nova/api/openstack/urlmap.py is necessary. I feel like we can get most of this functionality out of webob, is there something I'm missing?

Since we have to do content-type checking up front now, can we guarantee it *always* gets done up front? It seems like best_match_content_type is now designed to allow late checking of that header.

review: Needs Information
Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

paste deploy determines which versions of the API are available by using the configuration in etc/nova/api-paste.ini. This all happens before webob gets into the picture.

My original stab at this task modified nova.api.openstack.versions (which is the handler for / and thusly the default for any URL paths not prefixed with a version) instead. However, I had to duplicate the configuration of API versions, which I wanted to avoid.

The reason for best_match_content_type is covered in the description for the merge prop. As a summary, it's because the direct API uses some of the same WSGI stack but not paste deploy, and there are tests that deliberately operate on only part of the WSGI stack (for instance nova/tests/api/openstack/test_faults.py) and partially test the same functionality of urlmap (suffix content type tests).

I can't say I'm pleased with having two different methods of determining content type either. It might be possible to fix the direct API to use a different request object or maybe even use paste deploy. It might also be possible to mock up a different request object for the tests. I think there might be other problems too, I ran into a lot when I tried developing a patch that left best_match_content_type out.

It's guaranteed to get done up front if the paste configuration is correct. I'm not a fan of requiring configuration to get correct behavior, but there's already precedent for that (see the auth middleware) and I didn't want the scope to grow beyond the original bug.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Okay, I can definitely understand many of your concerns here. Maybe one day we will get away from paste and our problems will be solved ;)

As for the accept header parsing: I feel like the code in urlmap.py is duplicating functionality provided in webob.acceptparse (http://docs.webob.org/en/latest/modules/webob.html). Can you take a look at that and see if you agree with me?

Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

It *does* duplicate the code in webob, but only because webob ignores all parameters except for 'q'. We need the 'version' parameter as well (which is kind of the purpose of the branch).

I should also mention that the Accept parsing code in this branch is based off of the code in Werkzeug. It's BSD licensed and appears to be compatible with the Apache license Openstack uses. I'm thinking I might need to move that code to a separate file to keep the license separate.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Okay, I didn't look close enough at acceptparse to notice it didn't return values other than quality. I'm going to approve with the understanding that some of this will get simplified in the future.

review: Approve
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Moved to gerrit.

Unmerged revisions

1591. By Johannes Erdfelt

Add docstrings to tests to better document the intent

1590. By Johannes Erdfelt

Cleanup docstrings
Remove is_filename option, it's not necessary for us and is a hack anyway

1589. By Johannes Erdfelt

Remove FIXME now that it has a workaround

1588. By Johannes Erdfelt

Remove hardcoded content type now that the code handles direct API again

1587. By Johannes Erdfelt

Accept application/atom+xml for a few exceptions as well

1586. By Johannes Erdfelt

Add content type tests

1585. By Johannes Erdfelt

PEP8 cleanup

1584. By Johannes Erdfelt

Merge with trunk

1583. By Johannes Erdfelt

Start adding some unit tests

1582. By Johannes Erdfelt

Rename to urlmap to better reflect the purpose

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/nova/api-paste.ini'
2--- etc/nova/api-paste.ini 2011-09-20 22:17:03 +0000
3+++ etc/nova/api-paste.ini 2011-09-21 20:54:24 +0000
4@@ -71,7 +71,7 @@
5 #############
6
7 [composite:osapi]
8-use = egg:Paste#urlmap
9+use = call:nova.api.openstack.urlmap:urlmap_factory
10 /: osversions
11 /v1.0: openstackapi10
12 /v1.1: openstackapi11
13
14=== added file 'nova/api/openstack/urlmap.py'
15--- nova/api/openstack/urlmap.py 1970-01-01 00:00:00 +0000
16+++ nova/api/openstack/urlmap.py 2011-09-21 20:54:24 +0000
17@@ -0,0 +1,297 @@
18+# vim: tabstop=4 shiftwidth=4 softtabstop=4
19+
20+# Copyright 2011 OpenStack LLC.
21+# All Rights Reserved.
22+#
23+# Licensed under the Apache License, Version 2.0 (the "License"); you may
24+# not use this file except in compliance with the License. You may obtain
25+# a copy of the License at
26+#
27+# http://www.apache.org/licenses/LICENSE-2.0
28+#
29+# Unless required by applicable law or agreed to in writing, software
30+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
31+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
32+# License for the specific language governing permissions and limitations
33+# under the License.
34+
35+import paste.urlmap
36+import re
37+import urllib2
38+
39+from nova import log as logging
40+from nova.api.openstack import wsgi
41+
42+
43+_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
44+_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*'
45+ r'(?:=\s*([^;]+|%s))?\s*' %
46+ (_quoted_string_re, _quoted_string_re))
47+
48+LOG = logging.getLogger('nova.api.openstack.map')
49+
50+
51+def unquote_header_value(value):
52+ """Unquotes a header value.
53+ This does not use the real unquoting but what browsers are actually
54+ using for quoting.
55+
56+ :param value: the header value to unquote.
57+ """
58+ if value and value[0] == value[-1] == '"':
59+ # this is not the real unquoting, but fixing this so that the
60+ # RFC is met will result in bugs with internet explorer and
61+ # probably some other browsers as well. IE for example is
62+ # uploading files with "C:\foo\bar.txt" as filename
63+ value = value[1:-1]
64+ return value
65+
66+
67+def parse_list_header(value):
68+ """Parse lists as described by RFC 2068 Section 2.
69+
70+ In particular, parse comma-separated lists where the elements of
71+ the list may include quoted-strings. A quoted-string could
72+ contain a comma. A non-quoted string could have quotes in the
73+ middle. Quotes are removed automatically after parsing.
74+
75+ The return value is a standard :class:`list`:
76+
77+ >>> parse_list_header('token, "quoted value"')
78+ ['token', 'quoted value']
79+
80+ :param value: a string with a list header.
81+ :return: :class:`list`
82+ """
83+ result = []
84+ for item in urllib2.parse_http_list(value):
85+ if item[:1] == item[-1:] == '"':
86+ item = unquote_header_value(item[1:-1])
87+ result.append(item)
88+ return result
89+
90+
91+def parse_options_header(value):
92+ """Parse a ``Content-Type`` like header into a tuple with the content
93+ type and the options:
94+
95+ >>> parse_options_header('Content-Type: text/html; mimetype=text/html')
96+ ('Content-Type:', {'mimetype': 'text/html'})
97+
98+ :param value: the header to parse.
99+ :return: (str, options)
100+ """
101+ def _tokenize(string):
102+ for match in _option_header_piece_re.finditer(string):
103+ key, value = match.groups()
104+ key = unquote_header_value(key)
105+ if value is not None:
106+ value = unquote_header_value(value)
107+ yield key, value
108+
109+ if not value:
110+ return '', {}
111+
112+ parts = _tokenize(';' + value)
113+ name = parts.next()[0]
114+ extra = dict(parts)
115+ return name, extra
116+
117+
118+class Accept(object):
119+ def __init__(self, value):
120+ self._content_types = [parse_options_header(v) for v in
121+ parse_list_header(value)]
122+
123+ def best_match(self, supported_content_types):
124+ # FIXME: Should we have a more sophisticated matching algorithm that
125+ # takes into account the version as well?
126+ best_quality = -1
127+ best_content_type = None
128+ best_params = {}
129+ best_match = '*/*'
130+
131+ for content_type in supported_content_types:
132+ for content_mask, params in self._content_types:
133+ try:
134+ quality = float(params.get('q', 1))
135+ except ValueError:
136+ continue
137+
138+ if quality < best_quality:
139+ continue
140+ elif best_quality == quality:
141+ if best_match.count('*') <= content_mask.count('*'):
142+ continue
143+
144+ if self._match_mask(content_mask, content_type):
145+ best_quality = quality
146+ best_content_type = content_type
147+ best_params = params
148+ best_match = content_mask
149+
150+ return best_content_type, best_params
151+
152+ def content_type_params(self, best_content_type):
153+ """Find parameters in Accept header for given content type."""
154+ for content_type, params in self._content_types:
155+ if best_content_type == content_type:
156+ return params
157+
158+ return {}
159+
160+ def _match_mask(self, mask, content_type):
161+ if '*' not in mask:
162+ return content_type == mask
163+ if mask == '*/*':
164+ return True
165+ mask_major = mask[:-2]
166+ content_type_major = content_type.split('/', 1)[0]
167+ return content_type_major == mask_major
168+
169+
170+def urlmap_factory(loader, global_conf, **local_conf):
171+ if 'not_found_app' in local_conf:
172+ not_found_app = local_conf.pop('not_found_app')
173+ else:
174+ not_found_app = global_conf.get('not_found_app')
175+ if not_found_app:
176+ not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
177+ urlmap = URLMap(not_found_app=not_found_app)
178+ for path, app_name in local_conf.items():
179+ path = paste.urlmap.parse_path_expression(path)
180+ app = loader.get_app(app_name, global_conf=global_conf)
181+ urlmap[path] = app
182+ return urlmap
183+
184+
185+class URLMap(paste.urlmap.URLMap):
186+ def _match(self, host, port, path_info):
187+ """Find longest match for a given URL path."""
188+ for (domain, app_url), app in self.applications:
189+ if domain and domain != host and domain != host + ':' + port:
190+ continue
191+ if (path_info == app_url
192+ or path_info.startswith(app_url + '/')):
193+ return app, app_url
194+
195+ return None, None
196+
197+ def _set_script_name(self, app, app_url):
198+ def wrap(environ, start_response):
199+ environ['SCRIPT_NAME'] += app_url
200+ return app(environ, start_response)
201+
202+ return wrap
203+
204+ def _munge_path(self, app, path_info, app_url):
205+ def wrap(environ, start_response):
206+ environ['SCRIPT_NAME'] += app_url
207+ environ['PATH_INFO'] = path_info[len(app_url):]
208+ return app(environ, start_response)
209+
210+ return wrap
211+
212+ def _path_strategy(self, host, port, path_info):
213+ """Check path suffix for MIME type and path prefix for API version."""
214+ mime_type = app = app_url = None
215+
216+ parts = path_info.rsplit('.', 1)
217+ if len(parts) > 1:
218+ possible_type = 'application/' + parts[1]
219+ if possible_type in wsgi.SUPPORTED_CONTENT_TYPES:
220+ mime_type = possible_type
221+
222+ parts = path_info.split('/')
223+ if len(parts) > 1:
224+ possible_app, possible_app_url = self._match(host, port, path_info)
225+ # Don't use prefix if it ends up matching default
226+ if possible_app and possible_app_url:
227+ app_url = possible_app_url
228+ app = self._munge_path(possible_app, path_info, app_url)
229+
230+ return mime_type, app, app_url
231+
232+ def _content_type_strategy(self, host, port, environ):
233+ """Check Content-Type header for API version."""
234+ app = None
235+ params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1]
236+ if 'version' in params:
237+ app, app_url = self._match(host, port, '/v' + params['version'])
238+ if app:
239+ app = self._set_script_name(app, app_url)
240+
241+ return app
242+
243+ def _accept_strategy(self, host, port, environ, supported_content_types):
244+ """Check Accept header for best matching MIME type and API version."""
245+ accept = Accept(environ.get('HTTP_ACCEPT', ''))
246+
247+ app = None
248+
249+ # Find the best match in the Accept header
250+ mime_type, params = accept.best_match(supported_content_types)
251+ if 'version' in params:
252+ app, app_url = self._match(host, port, '/v' + params['version'])
253+ if app:
254+ app = self._set_script_name(app, app_url)
255+
256+ return mime_type, app
257+
258+ def __call__(self, environ, start_response):
259+ host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
260+ if ':' in host:
261+ host, port = host.split(':', 1)
262+ else:
263+ if environ['wsgi.url_scheme'] == 'http':
264+ port = '80'
265+ else:
266+ port = '443'
267+
268+ path_info = environ['PATH_INFO']
269+ path_info = self.normalize_url(path_info, False)[1]
270+
271+ # The MIME type for the response is determined in one of two ways:
272+ # 1) URL path suffix (eg /servers/detail.json)
273+ # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2)
274+
275+ # The API version is determined in one of three ways:
276+ # 1) URL path prefix (eg /v1.1/tenant/servers/detail)
277+ # 2) Content-Type header (eg application/json;version=1.1)
278+ # 3) Accept header (eg application/json;q=0.8;version=1.1)
279+
280+ supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES)
281+
282+ mime_type, app, app_url = self._path_strategy(host, port, path_info)
283+
284+ # Accept application/atom+xml for the index query of each API
285+ # version mount point as well as the root index
286+ if (app_url and app_url + '/' == path_info) or path_info == '/':
287+ supported_content_types.append('application/atom+xml')
288+
289+ if not app:
290+ app = self._content_type_strategy(host, port, environ)
291+
292+ if not mime_type or not app:
293+ possible_mime_type, possible_app = self._accept_strategy(
294+ host, port, environ, supported_content_types)
295+ if possible_mime_type and not mime_type:
296+ mime_type = possible_mime_type
297+ if possible_app and not app:
298+ app = possible_app
299+
300+ if not mime_type:
301+ mime_type = 'application/json'
302+
303+ if not app:
304+ # Didn't match a particular version, probably matches default
305+ app, app_url = self._match(host, port, path_info)
306+ if app:
307+ app = self._munge_path(app, path_info, app_url)
308+
309+ if app:
310+ environ['nova.best_content_type'] = mime_type
311+ return app(environ, start_response)
312+
313+ environ['paste.urlmap_object'] = self
314+ return self.not_found_application(environ, start_response)
315
316=== modified file 'nova/api/openstack/versions.py'
317--- nova/api/openstack/versions.py 2011-09-14 17:32:33 +0000
318+++ nova/api/openstack/versions.py 2011-09-21 20:54:24 +0000
319@@ -47,11 +47,11 @@
320 "media-types": [
321 {
322 "base": "application/xml",
323- "type": "application/vnd.openstack.compute-v1.0+xml",
324+ "type": "application/vnd.openstack.compute+xml;version=1.0",
325 },
326 {
327 "base": "application/json",
328- "type": "application/vnd.openstack.compute-v1.0+json",
329+ "type": "application/vnd.openstack.compute+json;version=1.0",
330 }
331 ],
332 },
333@@ -76,11 +76,11 @@
334 "media-types": [
335 {
336 "base": "application/xml",
337- "type": "application/vnd.openstack.compute-v1.1+xml",
338+ "type": "application/vnd.openstack.compute+xml;version=1.1",
339 },
340 {
341 "base": "application/json",
342- "type": "application/vnd.openstack.compute-v1.1+json",
343+ "type": "application/vnd.openstack.compute+json;version=1.1",
344 }
345 ],
346 },
347@@ -106,13 +106,7 @@
348 body_serializers=body_serializers,
349 headers_serializer=headers_serializer)
350
351- supported_content_types = ('application/json',
352- 'application/vnd.openstack.compute+json',
353- 'application/xml',
354- 'application/vnd.openstack.compute+xml',
355- 'application/atom+xml')
356- deserializer = VersionsRequestDeserializer(
357- supported_content_types=supported_content_types)
358+ deserializer = VersionsRequestDeserializer()
359
360 wsgi.Resource.__init__(self, None, serializer=serializer,
361 deserializer=deserializer)
362@@ -141,15 +135,6 @@
363
364
365 class VersionsRequestDeserializer(wsgi.RequestDeserializer):
366- def get_expected_content_type(self, request):
367- supported_content_types = list(self.supported_content_types)
368- if request.path != '/':
369- # Remove atom+xml accept type for 300 responses
370- if 'application/atom+xml' in supported_content_types:
371- supported_content_types.remove('application/atom+xml')
372-
373- return request.best_match_content_type(supported_content_types)
374-
375 def get_action_args(self, request_environment):
376 """Parse dictionary created by routes library."""
377 args = {}
378@@ -309,13 +294,7 @@
379 }
380 serializer = wsgi.ResponseSerializer(body_serializers)
381
382- supported_content_types = ('application/json',
383- 'application/vnd.openstack.compute+json',
384- 'application/xml',
385- 'application/vnd.openstack.compute+xml',
386- 'application/atom+xml')
387- deserializer = wsgi.RequestDeserializer(
388- supported_content_types=supported_content_types)
389+ deserializer = wsgi.RequestDeserializer()
390
391 return wsgi.Resource(controller, serializer=serializer,
392 deserializer=deserializer)
393
394=== modified file 'nova/api/openstack/wsgi.py'
395--- nova/api/openstack/wsgi.py 2011-09-15 18:32:10 +0000
396+++ nova/api/openstack/wsgi.py 2011-09-21 20:54:24 +0000
397@@ -43,7 +43,7 @@
398 'application/vnd.openstack.compute+xml': 'application/xml',
399 }
400
401-_SUPPORTED_CONTENT_TYPES = (
402+SUPPORTED_CONTENT_TYPES = (
403 'application/json',
404 'application/vnd.openstack.compute+json',
405 'application/xml',
406@@ -54,25 +54,26 @@
407 class Request(webob.Request):
408 """Add some Openstack API-specific logic to the base webob.Request."""
409
410- def best_match_content_type(self, supported_content_types=None):
411- """Determine the requested response content-type.
412-
413- Based on the query extension then the Accept header.
414-
415- """
416- supported_content_types = supported_content_types or \
417- _SUPPORTED_CONTENT_TYPES
418-
419- parts = self.path.rsplit('.', 1)
420- if len(parts) > 1:
421- ctype = 'application/{0}'.format(parts[1])
422- if ctype in supported_content_types:
423- return ctype
424-
425- bm = self.accept.best_match(supported_content_types)
426-
427- # default to application/json if we don't find a preference
428- return bm or 'application/json'
429+ def best_match_content_type(self):
430+ """Determine the requested response content-type."""
431+ if 'nova.best_content_type' not in self.environ:
432+ # Calculate the best MIME type
433+ content_type = None
434+
435+ # Check URL path suffix
436+ parts = self.path.rsplit('.', 1)
437+ if len(parts) > 1:
438+ possible_type = 'application/' + parts[1]
439+ if possible_type in SUPPORTED_CONTENT_TYPES:
440+ content_type = possible_type
441+
442+ if not content_type:
443+ content_type = self.accept.best_match(SUPPORTED_CONTENT_TYPES)
444+
445+ self.environ['nova.best_content_type'] = content_type or \
446+ 'application/json'
447+
448+ return self.environ['nova.best_content_type']
449
450 def get_content_type(self):
451 """Determine content type of the request body.
452@@ -83,7 +84,7 @@
453 if not "Content-Type" in self.headers:
454 return None
455
456- allowed_types = _SUPPORTED_CONTENT_TYPES
457+ allowed_types = SUPPORTED_CONTENT_TYPES
458 content_type = self.content_type
459
460 if content_type not in allowed_types:
461@@ -219,12 +220,7 @@
462 class RequestDeserializer(object):
463 """Break up a Request object into more useful pieces."""
464
465- def __init__(self, body_deserializers=None, headers_deserializer=None,
466- supported_content_types=None):
467-
468- self.supported_content_types = supported_content_types or \
469- _SUPPORTED_CONTENT_TYPES
470-
471+ def __init__(self, body_deserializers=None, headers_deserializer=None):
472 self.body_deserializers = {
473 'application/xml': XMLDeserializer(),
474 'application/json': JSONDeserializer(),
475@@ -287,7 +283,7 @@
476 raise exception.InvalidContentType(content_type=content_type)
477
478 def get_expected_content_type(self, request):
479- return request.best_match_content_type(self.supported_content_types)
480+ return request.best_match_content_type()
481
482 def get_action_args(self, request_environment):
483 """Parse dictionary created by routes library."""
484
485=== modified file 'nova/tests/api/openstack/fakes.py'
486--- nova/tests/api/openstack/fakes.py 2011-09-12 21:52:29 +0000
487+++ nova/tests/api/openstack/fakes.py 2011-09-21 20:54:24 +0000
488@@ -21,7 +21,6 @@
489
490 import webob
491 import webob.dec
492-from paste import urlmap
493
494 from glance import client as glance_client
495 from glance.common import exception as glance_exc
496@@ -37,6 +36,7 @@
497 from nova.api.openstack import extensions
498 from nova.api.openstack import versions
499 from nova.api.openstack import limits
500+from nova.api.openstack import urlmap
501 from nova.auth.manager import User, Project
502 import nova.image.fake
503 from nova.image import glance
504
505=== added file 'nova/tests/api/openstack/test_urlmap.py'
506--- nova/tests/api/openstack/test_urlmap.py 1970-01-01 00:00:00 +0000
507+++ nova/tests/api/openstack/test_urlmap.py 2011-09-21 20:54:24 +0000
508@@ -0,0 +1,111 @@
509+# Copyright 2011 OpenStack LLC.
510+# All Rights Reserved.
511+#
512+# Licensed under the Apache License, Version 2.0 (the "License"); you may
513+# not use this file except in compliance with the License. You may obtain
514+# a copy of the License at
515+#
516+# http://www.apache.org/licenses/LICENSE-2.0
517+#
518+# Unless required by applicable law or agreed to in writing, software
519+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
520+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
521+# License for the specific language governing permissions and limitations
522+# under the License.
523+
524+import json
525+import webob
526+
527+from nova import test
528+from nova import log as logging
529+from nova.tests.api.openstack import fakes
530+
531+LOG = logging.getLogger('nova.tests.api.openstack.test_urlmap')
532+
533+
534+class UrlmapTest(test.TestCase):
535+ def setUp(self):
536+ super(UrlmapTest, self).setUp()
537+ fakes.stub_out_rate_limiting(self.stubs)
538+
539+ def test_path_version_v1_0(self):
540+ """Test URL path specifying v1.0 returns v1.0 content."""
541+ req = webob.Request.blank('/v1.0/')
542+ req.accept = "application/json"
543+ res = req.get_response(fakes.wsgi_app())
544+ self.assertEqual(res.status_int, 200)
545+ self.assertEqual(res.content_type, "application/json")
546+ body = json.loads(res.body)
547+ self.assertEqual(body['version']['id'], 'v1.0')
548+
549+ def test_path_version_v1_1(self):
550+ """Test URL path specifying v1.1 returns v1.1 content."""
551+ req = webob.Request.blank('/v1.1/')
552+ req.accept = "application/json"
553+ res = req.get_response(fakes.wsgi_app())
554+ self.assertEqual(res.status_int, 200)
555+ self.assertEqual(res.content_type, "application/json")
556+ body = json.loads(res.body)
557+ self.assertEqual(body['version']['id'], 'v1.1')
558+
559+ def test_content_type_version_v1_0(self):
560+ """Test Content-Type specifying v1.0 returns v1.0 content."""
561+ req = webob.Request.blank('/')
562+ req.content_type = "application/json;version=1.0"
563+ req.accept = "application/json"
564+ res = req.get_response(fakes.wsgi_app())
565+ self.assertEqual(res.status_int, 200)
566+ self.assertEqual(res.content_type, "application/json")
567+ body = json.loads(res.body)
568+ self.assertEqual(body['version']['id'], 'v1.0')
569+
570+ def test_content_type_version_v1_1(self):
571+ """Test Content-Type specifying v1.1 returns v1.1 content."""
572+ req = webob.Request.blank('/')
573+ req.content_type = "application/json;version=1.1"
574+ req.accept = "application/json"
575+ res = req.get_response(fakes.wsgi_app())
576+ self.assertEqual(res.status_int, 200)
577+ self.assertEqual(res.content_type, "application/json")
578+ body = json.loads(res.body)
579+ self.assertEqual(body['version']['id'], 'v1.1')
580+
581+ def test_accept_version_v1_0(self):
582+ """Test Accept header specifying v1.0 returns v1.0 content."""
583+ req = webob.Request.blank('/')
584+ req.accept = "application/json;version=1.0"
585+ res = req.get_response(fakes.wsgi_app())
586+ self.assertEqual(res.status_int, 200)
587+ self.assertEqual(res.content_type, "application/json")
588+ body = json.loads(res.body)
589+ self.assertEqual(body['version']['id'], 'v1.0')
590+
591+ def test_accept_version_v1_1(self):
592+ """Test Accept header specifying v1.1 returns v1.1 content."""
593+ req = webob.Request.blank('/')
594+ req.accept = "application/json;version=1.1"
595+ res = req.get_response(fakes.wsgi_app())
596+ self.assertEqual(res.status_int, 200)
597+ self.assertEqual(res.content_type, "application/json")
598+ body = json.loads(res.body)
599+ self.assertEqual(body['version']['id'], 'v1.1')
600+
601+ def test_path_content_type(self):
602+ """Test URL path specifying JSON returns JSON content."""
603+ req = webob.Request.blank('/v1.1/foobar/images/1.json')
604+ req.accept = "application/xml"
605+ res = req.get_response(fakes.wsgi_app())
606+ self.assertEqual(res.status_int, 200)
607+ self.assertEqual(res.content_type, "application/json")
608+ body = json.loads(res.body)
609+ self.assertEqual(body['image']['id'], '1')
610+
611+ def test_accept_content_type(self):
612+ """Test Accept header specifying JSON returns JSON content."""
613+ req = webob.Request.blank('/v1.1/foobar/images/1')
614+ req.accept = "application/xml;q=0.8, application/json"
615+ res = req.get_response(fakes.wsgi_app())
616+ self.assertEqual(res.status_int, 200)
617+ self.assertEqual(res.content_type, "application/json")
618+ body = json.loads(res.body)
619+ self.assertEqual(body['image']['id'], '1')
620
621=== modified file 'nova/tests/api/openstack/test_versions.py'
622--- nova/tests/api/openstack/test_versions.py 2011-09-13 21:09:25 +0000
623+++ nova/tests/api/openstack/test_versions.py 2011-09-21 20:54:24 +0000
624@@ -55,11 +55,11 @@
625 "media-types": [
626 {
627 "base": "application/xml",
628- "type": "application/vnd.openstack.compute-v1.0+xml",
629+ "type": "application/vnd.openstack.compute+xml;version=1.0",
630 },
631 {
632 "base": "application/json",
633- "type": "application/vnd.openstack.compute-v1.0+json",
634+ "type": "application/vnd.openstack.compute+json;version=1.0",
635 },
636 ],
637 },
638@@ -84,11 +84,11 @@
639 "media-types": [
640 {
641 "base": "application/xml",
642- "type": "application/vnd.openstack.compute-v1.1+xml",
643+ "type": "application/vnd.openstack.compute+xml;version=1.1",
644 },
645 {
646 "base": "application/json",
647- "type": "application/vnd.openstack.compute-v1.1+json",
648+ "type": "application/vnd.openstack.compute+json;version=1.1",
649 },
650 ],
651 },
652@@ -174,12 +174,12 @@
653 {
654 "base": "application/xml",
655 "type": "application/"
656- "vnd.openstack.compute-v1.0+xml",
657+ "vnd.openstack.compute+xml;version=1.0",
658 },
659 {
660 "base": "application/json",
661 "type": "application/"
662- "vnd.openstack.compute-v1.0+json",
663+ "vnd.openstack.compute+json;version=1.0",
664 },
665 ],
666 },
667@@ -220,12 +220,58 @@
668 {
669 "base": "application/xml",
670 "type": "application/"
671- "vnd.openstack.compute-v1.1+xml",
672- },
673- {
674- "base": "application/json",
675- "type": "application/"
676- "vnd.openstack.compute-v1.1+json",
677+ "vnd.openstack.compute+xml;version=1.1",
678+ },
679+ {
680+ "base": "application/json",
681+ "type": "application/"
682+ "vnd.openstack.compute+json;version=1.1",
683+ },
684+ ],
685+ },
686+ }
687+ self.assertEqual(expected, version)
688+
689+ def test_get_version_1_1_detail_content_type(self):
690+ req = webob.Request.blank('/')
691+ req.accept = "application/json;version=1.1"
692+ res = req.get_response(fakes.wsgi_app())
693+ self.assertEqual(res.status_int, 200)
694+ self.assertEqual(res.content_type, "application/json")
695+ version = json.loads(res.body)
696+ expected = {
697+ "version": {
698+ "id": "v1.1",
699+ "status": "CURRENT",
700+ "updated": "2011-01-21T11:33:21Z",
701+ "links": [
702+ {
703+ "rel": "self",
704+ "href": "http://localhost/v1.1/",
705+ },
706+ {
707+ "rel": "describedby",
708+ "type": "application/pdf",
709+ "href": "http://docs.rackspacecloud.com/"
710+ "servers/api/v1.1/cs-devguide-20110125.pdf",
711+ },
712+ {
713+ "rel": "describedby",
714+ "type": "application/vnd.sun.wadl+xml",
715+ "href": "http://docs.rackspacecloud.com/"
716+ "servers/api/v1.1/application.wadl",
717+ },
718+ ],
719+ "media-types": [
720+ {
721+ "base": "application/xml",
722+ "type": "application/"
723+ "vnd.openstack.compute+xml;version=1.1",
724+ },
725+ {
726+ "base": "application/json",
727+ "type": "application/"
728+ "vnd.openstack.compute+json;version=1.1",
729 },
730 ],
731 },
732@@ -433,11 +479,13 @@
733 "media-types": [
734 {
735 "base": "application/xml",
736- "type": "application/vnd.openstack.compute-v1.1+xml"
737+ "type": "application/vnd.openstack.compute+xml"
738+ ";version=1.1"
739 },
740 {
741 "base": "application/json",
742- "type": "application/vnd.openstack.compute-v1.1+json"
743+ "type": "application/vnd.openstack.compute+json"
744+ ";version=1.1"
745 },
746 ],
747 },
748@@ -453,11 +501,13 @@
749 "media-types": [
750 {
751 "base": "application/xml",
752- "type": "application/vnd.openstack.compute-v1.0+xml"
753+ "type": "application/vnd.openstack.compute+xml"
754+ ";version=1.0"
755 },
756 {
757 "base": "application/json",
758- "type": "application/vnd.openstack.compute-v1.0+json"
759+ "type": "application/vnd.openstack.compute+json"
760+ ";version=1.0"
761 },
762 ],
763 },
764@@ -531,11 +581,13 @@
765 "media-types": [
766 {
767 "base": "application/xml",
768- "type": "application/vnd.openstack.compute-v1.1+xml"
769+ "type": "application/vnd.openstack.compute+xml"
770+ ";version=1.1"
771 },
772 {
773 "base": "application/json",
774- "type": "application/vnd.openstack.compute-v1.1+json"
775+ "type": "application/vnd.openstack.compute+json"
776+ ";version=1.1"
777 },
778 ],
779 },
780@@ -551,11 +603,13 @@
781 "media-types": [
782 {
783 "base": "application/xml",
784- "type": "application/vnd.openstack.compute-v1.0+xml"
785+ "type": "application/vnd.openstack.compute+xml"
786+ ";version=1.0"
787 },
788 {
789 "base": "application/json",
790- "type": "application/vnd.openstack.compute-v1.0+json"
791+ "type": "application/vnd.openstack.compute+json"
792+ ";version=1.0"
793 },
794 ],
795 },
796@@ -713,11 +767,13 @@
797 "media-types": [
798 {
799 "base": "application/xml",
800- "type": "application/vnd.openstack.compute-v1.0+xml",
801+ "type": "application/vnd.openstack.compute+xml"
802+ ";version=1.0",
803 },
804 {
805 "base": "application/json",
806- "type": "application/vnd.openstack.compute-v1.0+json",
807+ "type": "application/vnd.openstack.compute+json"
808+ ";version=1.0",
809 },
810 ],
811 },
812@@ -817,11 +873,13 @@
813 "media-types": [
814 {
815 "base": "application/xml",
816- "type": "application/vnd.openstack.compute-v1.1+xml",
817+ "type": "application/vnd.openstack.compute+xml"
818+ ";version=1.1",
819 },
820 {
821 "base": "application/json",
822- "type": "application/vnd.openstack.compute-v1.1+json",
823+ "type": "application/vnd.openstack.compute+json"
824+ ";version=1.1",
825 }
826 ],
827 },