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

Proposed by Johannes Erdfelt
Status: Merged
Approved by: Dan Prince
Approved revision: 1561
Merged at revision: 1572
Proposed branch: lp:~johannes.erdfelt/nova/lp844910
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 267 lines (+101/-28)
4 files modified
nova/api/openstack/versions.py (+4/-0)
nova/api/openstack/wsgi.py (+38/-5)
nova/tests/api/openstack/test_api.py (+25/-0)
nova/tests/api/openstack/test_wsgi.py (+34/-23)
To merge this branch: bzr merge lp:~johannes.erdfelt/nova/lp844910
Reviewer Review Type Date Requested Status
Dan Prince (community) Approve
Brian Waldon (community) Approve
Jay Pipes (community) Approve
Review via email: mp+75255@code.launchpad.net

Description of the change

The 1.1 API specifies that two vendor content types are allowed in addition to the standard JSON and XML content types.

This branch adds support for application/vnd.openstack.compute+json and application/vnd.openstack.compute+xml.

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Personally, I feel like these new content types are just aliases to their simpler counterparts. I don't really think it is necessary to map them individually for (de)serialization. Did you think about writing a simple middleware that just simplifies the content types?

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

I had thought about that, but if you alias the values, then the content-type returned won't necessarily match what is requested in the accept header.

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

Being middleware, you could remember that you aliased the value in the request and set it back in the response.

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

True, but the code would be relatively complicated. Much of the code in nova/api/openstack/wsgi.py deals with determining the best match for the accept header. This isn't trivial since content types can be weighted.

To handle the case of "application/vnd.openstack.compute+xml; q=0.2, application/xml; q=0.8" would require some fairly sophisticated code to parse out the accept, choose the best match, possibly replace/remove it from the accept header and then possibly replace the content-type on the response path.

It would duplicate much of the existing code.

The proposed branch is extremely straight forward and has no tricky code in it. I still think it's the better option.

Revision history for this message
Jay Pipes (jaypipes) wrote :

> True, but the code would be relatively complicated. Much of the code in
> nova/api/openstack/wsgi.py deals with determining the best match for the
> accept header. This isn't trivial since content types can be weighted.
>
> To handle the case of "application/vnd.openstack.compute+xml; q=0.2,
> application/xml; q=0.8" would require some fairly sophisticated code to parse
> out the accept, choose the best match, possibly replace/remove it from the
> accept header and then possibly replace the content-type on the response path.

Sophisticated code that's actually already written :)

http://docs.webob.org/en/latest/reference.html#accept-headers

-jay

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

The sophisticated code I meant was the code to wrap up the webob best match code with code to modify the Accept header.

The webob code makes matching against the Accept header easy, but not modifying it. That's the sophisticated part.

Revision history for this message
Jay Pipes (jaypipes) wrote :

OK, I don't really see why you would need to modify the Accept header at all (is that even advisable/legal?)

Revision history for this message
Jay Pipes (jaypipes) wrote :

In any case, my remark is not consequential to this patch being good, which it is ;)

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

I've made changes to simplify the code a little bit and fix a bug that snuck during a merge. The vendor types will be mapped to the standard types when the code goes to serialize or deserialize. This simplifies the code while minimizing the changes to all of the other code that handles XML serialization.

I also added a couple of more tests and a missing copyright.

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

This is great, Johannes.

review: Approve
Revision history for this message
Dan Prince (dan-prince) wrote :

This looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/openstack/versions.py'
2--- nova/api/openstack/versions.py 2011-09-02 05:07:30 +0000
3+++ nova/api/openstack/versions.py 2011-09-14 17:58:40 +0000
4@@ -107,7 +107,9 @@
5 headers_serializer=headers_serializer)
6
7 supported_content_types = ('application/json',
8+ 'application/vnd.openstack.compute+json',
9 'application/xml',
10+ 'application/vnd.openstack.compute+xml',
11 'application/atom+xml')
12 deserializer = VersionsRequestDeserializer(
13 supported_content_types=supported_content_types)
14@@ -308,7 +310,9 @@
15 serializer = wsgi.ResponseSerializer(body_serializers)
16
17 supported_content_types = ('application/json',
18+ 'application/vnd.openstack.compute+json',
19 'application/xml',
20+ 'application/vnd.openstack.compute+xml',
21 'application/atom+xml')
22 deserializer = wsgi.RequestDeserializer(
23 supported_content_types=supported_content_types)
24
25=== modified file 'nova/api/openstack/wsgi.py'
26--- nova/api/openstack/wsgi.py 2011-08-25 20:35:04 +0000
27+++ nova/api/openstack/wsgi.py 2011-09-14 17:58:40 +0000
28@@ -1,3 +1,19 @@
29+# vim: tabstop=4 shiftwidth=4 softtabstop=4
30+
31+# Copyright 2011 OpenStack LLC.
32+# All Rights Reserved.
33+#
34+# Licensed under the Apache License, Version 2.0 (the "License"); you may
35+# not use this file except in compliance with the License. You may obtain
36+# a copy of the License at
37+#
38+# http://www.apache.org/licenses/LICENSE-2.0
39+#
40+# Unless required by applicable law or agreed to in writing, software
41+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
42+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
43+# License for the specific language governing permissions and limitations
44+# under the License.
45
46 import json
47 from lxml import etree
48@@ -19,6 +35,21 @@
49
50 LOG = logging.getLogger('nova.api.openstack.wsgi')
51
52+# The vendor content types should serialize identically to the non-vendor
53+# content types. So to avoid littering the code with both options, we
54+# map the vendor to the other when looking up the type
55+_CONTENT_TYPE_MAP = {
56+ 'application/vnd.openstack.compute+json': 'application/json',
57+ 'application/vnd.openstack.compute+xml': 'application/xml',
58+}
59+
60+_SUPPORTED_CONTENT_TYPES = (
61+ 'application/json',
62+ 'application/vnd.openstack.compute+json',
63+ 'application/xml',
64+ 'application/vnd.openstack.compute+xml',
65+)
66+
67
68 class Request(webob.Request):
69 """Add some Openstack API-specific logic to the base webob.Request."""
70@@ -30,7 +61,7 @@
71
72 """
73 supported_content_types = supported_content_types or \
74- ('application/json', 'application/xml')
75+ _SUPPORTED_CONTENT_TYPES
76
77 parts = self.path.rsplit('.', 1)
78 if len(parts) > 1:
79@@ -52,7 +83,7 @@
80 if not "Content-Type" in self.headers:
81 return None
82
83- allowed_types = ("application/xml", "application/json")
84+ allowed_types = _SUPPORTED_CONTENT_TYPES
85 content_type = self.content_type
86
87 if content_type not in allowed_types:
88@@ -192,7 +223,7 @@
89 supported_content_types=None):
90
91 self.supported_content_types = supported_content_types or \
92- ('application/json', 'application/xml')
93+ _SUPPORTED_CONTENT_TYPES
94
95 self.body_deserializers = {
96 'application/xml': XMLDeserializer(),
97@@ -250,7 +281,8 @@
98
99 def get_body_deserializer(self, content_type):
100 try:
101- return self.body_deserializers[content_type]
102+ ctype = _CONTENT_TYPE_MAP.get(content_type, content_type)
103+ return self.body_deserializers[ctype]
104 except (KeyError, TypeError):
105 raise exception.InvalidContentType(content_type=content_type)
106
107@@ -444,7 +476,8 @@
108
109 def get_body_serializer(self, content_type):
110 try:
111- return self.body_serializers[content_type]
112+ ctype = _CONTENT_TYPE_MAP.get(content_type, content_type)
113+ return self.body_serializers[ctype]
114 except (KeyError, TypeError):
115 raise exception.InvalidContentType(content_type=content_type)
116
117
118=== modified file 'nova/tests/api/openstack/test_api.py'
119--- nova/tests/api/openstack/test_api.py 2011-06-14 14:16:51 +0000
120+++ nova/tests/api/openstack/test_api.py 2011-09-14 17:58:40 +0000
121@@ -20,6 +20,7 @@
122 import webob.exc
123 import webob.dec
124
125+from lxml import etree
126 from webob import Request
127
128 from nova import test
129@@ -52,6 +53,30 @@
130 res = req.get_response(fakes.wsgi_app())
131 self.assertEqual(res.status_int, 400)
132
133+ def test_vendor_content_type_json(self):
134+ ctype = 'application/vnd.openstack.compute+json'
135+
136+ req = webob.Request.blank('/')
137+ req.headers['Accept'] = ctype
138+
139+ res = req.get_response(fakes.wsgi_app())
140+ self.assertEqual(res.status_int, 200)
141+ self.assertEqual(res.content_type, ctype)
142+
143+ body = json.loads(res.body)
144+
145+ def test_vendor_content_type_xml(self):
146+ ctype = 'application/vnd.openstack.compute+xml'
147+
148+ req = webob.Request.blank('/')
149+ req.headers['Accept'] = ctype
150+
151+ res = req.get_response(fakes.wsgi_app())
152+ self.assertEqual(res.status_int, 200)
153+ self.assertEqual(res.content_type, ctype)
154+
155+ body = etree.XML(res.body)
156+
157 def test_exceptions_are_converted_to_faults(self):
158
159 @webob.dec.wsgify
160
161=== modified file 'nova/tests/api/openstack/test_wsgi.py'
162--- nova/tests/api/openstack/test_wsgi.py 2011-07-25 19:56:23 +0000
163+++ nova/tests/api/openstack/test_wsgi.py 2011-09-14 17:58:40 +0000
164@@ -27,17 +27,17 @@
165 result = request.get_content_type()
166 self.assertEqual(result, "application/json")
167
168- def test_content_type_from_accept_xml(self):
169- request = wsgi.Request.blank('/tests/123')
170- request.headers["Accept"] = "application/xml"
171- result = request.best_match_content_type()
172- self.assertEqual(result, "application/xml")
173-
174- request = wsgi.Request.blank('/tests/123')
175- request.headers["Accept"] = "application/json"
176- result = request.best_match_content_type()
177- self.assertEqual(result, "application/json")
178-
179+ def test_content_type_from_accept(self):
180+ for content_type in ('application/xml',
181+ 'application/vnd.openstack.compute+xml',
182+ 'application/json',
183+ 'application/vnd.openstack.compute+json'):
184+ request = wsgi.Request.blank('/tests/123')
185+ request.headers["Accept"] = content_type
186+ result = request.best_match_content_type()
187+ self.assertEqual(result, content_type)
188+
189+ def test_content_type_from_accept_best(self):
190 request = wsgi.Request.blank('/tests/123')
191 request.headers["Accept"] = "application/xml, application/json"
192 result = request.best_match_content_type()
193@@ -231,7 +231,7 @@
194
195 self.body_serializers = {
196 'application/json': JSONSerializer(),
197- 'application/XML': XMLSerializer(),
198+ 'application/xml': XMLSerializer(),
199 }
200
201 self.serializer = wsgi.ResponseSerializer(self.body_serializers,
202@@ -250,15 +250,24 @@
203 self.serializer.get_body_serializer,
204 'application/unknown')
205
206- def test_serialize_response(self):
207- response = self.serializer.serialize({}, 'application/json')
208- self.assertEqual(response.headers['Content-Type'], 'application/json')
209- self.assertEqual(response.body, 'pew_json')
210- self.assertEqual(response.status_int, 404)
211+ def test_serialize_response_json(self):
212+ for content_type in ('application/json',
213+ 'application/vnd.openstack.compute+json'):
214+ response = self.serializer.serialize({}, content_type)
215+ self.assertEqual(response.headers['Content-Type'], content_type)
216+ self.assertEqual(response.body, 'pew_json')
217+ self.assertEqual(response.status_int, 404)
218+
219+ def test_serialize_response_xml(self):
220+ for content_type in ('application/xml',
221+ 'application/vnd.openstack.compute+xml'):
222+ response = self.serializer.serialize({}, content_type)
223+ self.assertEqual(response.headers['Content-Type'], content_type)
224+ self.assertEqual(response.body, 'pew_xml')
225+ self.assertEqual(response.status_int, 404)
226
227 def test_serialize_response_None(self):
228 response = self.serializer.serialize(None, 'application/json')
229- print response
230 self.assertEqual(response.headers['Content-Type'], 'application/json')
231 self.assertEqual(response.body, '')
232 self.assertEqual(response.status_int, 404)
233@@ -281,7 +290,7 @@
234
235 self.body_deserializers = {
236 'application/json': JSONDeserializer(),
237- 'application/XML': XMLDeserializer(),
238+ 'application/xml': XMLDeserializer(),
239 }
240
241 self.deserializer = wsgi.RequestDeserializer(self.body_deserializers)
242@@ -290,8 +299,9 @@
243 pass
244
245 def test_get_deserializer(self):
246- expected = self.deserializer.get_body_deserializer('application/json')
247- self.assertEqual(expected, self.body_deserializers['application/json'])
248+ ctype = 'application/json'
249+ expected = self.deserializer.get_body_deserializer(ctype)
250+ self.assertEqual(expected, self.body_deserializers[ctype])
251
252 def test_get_deserializer_unknown_content_type(self):
253 self.assertRaises(exception.InvalidContentType,
254@@ -299,10 +309,11 @@
255 'application/unknown')
256
257 def test_get_expected_content_type(self):
258+ ctype = 'application/json'
259 request = wsgi.Request.blank('/')
260- request.headers['Accept'] = 'application/json'
261+ request.headers['Accept'] = ctype
262 self.assertEqual(self.deserializer.get_expected_content_type(request),
263- 'application/json')
264+ ctype)
265
266 def test_get_action_args(self):
267 env = {