Merge lp:~rackspace-titan/nova/wsgi-refactor into lp:~hudson-openstack/nova/trunk

Proposed by Brian Waldon
Status: Merged
Approved by: Brian Lamar
Approved revision: 1252
Merged at revision: 1261
Proposed branch: lp:~rackspace-titan/nova/wsgi-refactor
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1184 lines (+318/-209)
21 files modified
nova/api/openstack/accounts.py (+3/-3)
nova/api/openstack/backup_schedules.py (+7/-6)
nova/api/openstack/consoles.py (+1/-11)
nova/api/openstack/contrib/floating_ips.py (+2/-2)
nova/api/openstack/create_instance_helper.py (+1/-1)
nova/api/openstack/flavors.py (+4/-2)
nova/api/openstack/image_metadata.py (+3/-2)
nova/api/openstack/images.py (+4/-2)
nova/api/openstack/ips.py (+3/-2)
nova/api/openstack/limits.py (+4/-2)
nova/api/openstack/server_metadata.py (+4/-2)
nova/api/openstack/servers.py (+6/-4)
nova/api/openstack/shared_ip_groups.py (+6/-6)
nova/api/openstack/users.py (+4/-2)
nova/api/openstack/versions.py (+3/-2)
nova/api/openstack/wsgi.py (+132/-73)
nova/api/openstack/zones.py (+5/-4)
nova/tests/api/openstack/contrib/test_floating_ips.py (+3/-0)
nova/tests/api/openstack/test_servers.py (+23/-23)
nova/tests/api/openstack/test_wsgi.py (+97/-56)
nova/tests/integrated/api/client.py (+3/-4)
To merge this branch: bzr merge lp:~rackspace-titan/nova/wsgi-refactor
Reviewer Review Type Date Requested Status
Brian Lamar (community) Approve
Josh Kearney (community) Approve
Review via email: mp+67084@code.launchpad.net

Commit message

Expanding OSAPI wsgi module to allow handling of headers and status codes

Description of the change

- Expanding OSAPI wsgi module to allow handling of headers and status codes
- Allows us to fix many of our OSAPI-related bugs efficiently:
    https://bugs.launchpad.net/nova/+bug/796709
    https://bugs.launchpad.net/nova/+bug/802656
    https://bugs.launchpad.net/nova/+bug/804087

To post a comment you must log in.
Revision history for this message
Josh Kearney (jk0) wrote :

Changes look good and tests pass.

review: Approve
Revision history for this message
Brian Lamar (blamar) wrote :

After talking it through with Waldon I like the separation of serialization between body and headers. Serializing bodies shouldn't include HTTP codes/headers and vice versa. Passes all my tests and I'm looking forward to the other bug fixes using this branch. Thanks!

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/accounts.py'
2--- nova/api/openstack/accounts.py 2011-05-20 18:42:19 +0000
3+++ nova/api/openstack/accounts.py 2011-07-11 17:17:32 +0000
4@@ -87,8 +87,8 @@
5 },
6 }
7
8- serializers = {
9+ body_serializers = {
10 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
11 }
12-
13- return wsgi.Resource(Controller(), serializers=serializers)
14+ serializer = wsgi.ResponseSerializer(body_serializers)
15+ return wsgi.Resource(Controller(), serializer=serializer)
16
17=== modified file 'nova/api/openstack/backup_schedules.py'
18--- nova/api/openstack/backup_schedules.py 2011-05-20 18:42:19 +0000
19+++ nova/api/openstack/backup_schedules.py 2011-07-11 17:17:32 +0000
20@@ -34,20 +34,20 @@
21 def __init__(self):
22 pass
23
24- def index(self, req, server_id):
25+ def index(self, req, server_id, **kwargs):
26 """ Returns the list of backup schedules for a given instance """
27 return faults.Fault(exc.HTTPNotImplemented())
28
29- def show(self, req, server_id, id):
30+ def show(self, req, server_id, id, **kwargs):
31 """ Returns a single backup schedule for a given instance """
32 return faults.Fault(exc.HTTPNotImplemented())
33
34- def create(self, req, server_id, body):
35+ def create(self, req, server_id, **kwargs):
36 """ No actual update method required, since the existing API allows
37 both create and update through a POST """
38 return faults.Fault(exc.HTTPNotImplemented())
39
40- def delete(self, req, server_id, id):
41+ def delete(self, req, server_id, id, **kwargs):
42 """ Deletes an existing backup schedule """
43 return faults.Fault(exc.HTTPNotImplemented())
44
45@@ -59,9 +59,10 @@
46 },
47 }
48
49- serializers = {
50+ body_serializers = {
51 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
52 metadata=metadata),
53 }
54
55- return wsgi.Resource(Controller(), serializers=serializers)
56+ serializer = wsgi.ResponseSerializer(body_serializers)
57+ return wsgi.Resource(Controller(), serializer=serializer)
58
59=== modified file 'nova/api/openstack/consoles.py'
60--- nova/api/openstack/consoles.py 2011-05-20 18:42:19 +0000
61+++ nova/api/openstack/consoles.py 2011-07-11 17:17:32 +0000
62@@ -90,14 +90,4 @@
63
64
65 def create_resource():
66- metadata = {
67- 'attributes': {
68- 'console': [],
69- },
70- }
71-
72- serializers = {
73- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
74- }
75-
76- return wsgi.Resource(Controller(), serializers=serializers)
77+ return wsgi.Resource(Controller())
78
79=== modified file 'nova/api/openstack/contrib/floating_ips.py'
80--- nova/api/openstack/contrib/floating_ips.py 2011-06-29 17:58:10 +0000
81+++ nova/api/openstack/contrib/floating_ips.py 2011-07-11 17:17:32 +0000
82@@ -78,7 +78,7 @@
83
84 return _translate_floating_ips_view(floating_ips)
85
86- def create(self, req, body):
87+ def create(self, req):
88 context = req.environ['nova.context']
89
90 try:
91@@ -124,7 +124,7 @@
92 "floating_ip": floating_ip,
93 "fixed_ip": fixed_ip}}
94
95- def disassociate(self, req, id, body):
96+ def disassociate(self, req, id):
97 """ POST /floating_ips/{id}/disassociate """
98 context = req.environ['nova.context']
99 floating_ip = self.network_api.get_floating_ip(context, id)
100
101=== modified file 'nova/api/openstack/create_instance_helper.py'
102--- nova/api/openstack/create_instance_helper.py 2011-06-29 22:22:56 +0000
103+++ nova/api/openstack/create_instance_helper.py 2011-07-11 17:17:32 +0000
104@@ -289,7 +289,7 @@
105 """Deserialize an xml-formatted server create request"""
106 dom = minidom.parseString(string)
107 server = self._extract_server(dom)
108- return {'server': server}
109+ return {'body': {'server': server}}
110
111 def _extract_server(self, node):
112 """Marshal the server attribute of a parsed request"""
113
114=== modified file 'nova/api/openstack/flavors.py'
115--- nova/api/openstack/flavors.py 2011-06-30 13:28:21 +0000
116+++ nova/api/openstack/flavors.py 2011-07-11 17:17:32 +0000
117@@ -85,8 +85,10 @@
118 '1.1': wsgi.XMLNS_V11,
119 }[version]
120
121- serializers = {
122+ body_serializers = {
123 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
124 }
125
126- return wsgi.Resource(controller, serializers=serializers)
127+ serializer = wsgi.ResponseSerializer(body_serializers)
128+
129+ return wsgi.Resource(controller, serializer=serializer)
130
131=== modified file 'nova/api/openstack/image_metadata.py'
132--- nova/api/openstack/image_metadata.py 2011-06-28 15:59:46 +0000
133+++ nova/api/openstack/image_metadata.py 2011-07-11 17:17:32 +0000
134@@ -160,8 +160,9 @@
135
136
137 def create_resource():
138- serializers = {
139+ body_serializers = {
140 'application/xml': ImageMetadataXMLSerializer(),
141 }
142+ serializer = wsgi.ResponseSerializer(body_serializers)
143
144- return wsgi.Resource(Controller(), serializers=serializers)
145+ return wsgi.Resource(Controller(), serializer=serializer)
146
147=== modified file 'nova/api/openstack/images.py'
148--- nova/api/openstack/images.py 2011-07-11 13:13:22 +0000
149+++ nova/api/openstack/images.py 2011-07-11 17:17:32 +0000
150@@ -348,8 +348,10 @@
151 '1.1': ImageXMLSerializer(),
152 }[version]
153
154- serializers = {
155+ body_serializers = {
156 'application/xml': xml_serializer,
157 }
158
159- return wsgi.Resource(controller, serializers=serializers)
160+ serializer = wsgi.ResponseSerializer(body_serializers)
161+
162+ return wsgi.Resource(controller, serializer=serializer)
163
164=== modified file 'nova/api/openstack/ips.py'
165--- nova/api/openstack/ips.py 2011-06-24 12:01:51 +0000
166+++ nova/api/openstack/ips.py 2011-07-11 17:17:32 +0000
167@@ -70,9 +70,10 @@
168 },
169 }
170
171- serializers = {
172+ body_serializers = {
173 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
174 xmlns=wsgi.XMLNS_V10),
175 }
176+ serializer = wsgi.ResponseSerializer(body_serializers)
177
178- return wsgi.Resource(Controller(), serializers=serializers)
179+ return wsgi.Resource(Controller(), serializer=serializer)
180
181=== modified file 'nova/api/openstack/limits.py'
182--- nova/api/openstack/limits.py 2011-06-14 01:14:26 +0000
183+++ nova/api/openstack/limits.py 2011-07-11 17:17:32 +0000
184@@ -97,12 +97,14 @@
185 },
186 }
187
188- serializers = {
189+ body_serializers = {
190 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
191 metadata=metadata),
192 }
193
194- return wsgi.Resource(controller, serializers=serializers)
195+ serializer = wsgi.ResponseSerializer(body_serializers)
196+
197+ return wsgi.Resource(controller, serializer=serializer)
198
199
200 class Limit(object):
201
202=== modified file 'nova/api/openstack/server_metadata.py'
203--- nova/api/openstack/server_metadata.py 2011-06-24 12:01:51 +0000
204+++ nova/api/openstack/server_metadata.py 2011-07-11 17:17:32 +0000
205@@ -123,8 +123,10 @@
206
207
208 def create_resource():
209- serializers = {
210+ body_serializers = {
211 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
212 }
213
214- return wsgi.Resource(Controller(), serializers=serializers)
215+ serializer = wsgi.ResponseSerializer(body_serializers)
216+
217+ return wsgi.Resource(Controller(), serializer=serializer)
218
219=== modified file 'nova/api/openstack/servers.py'
220--- nova/api/openstack/servers.py 2011-07-08 15:59:17 +0000
221+++ nova/api/openstack/servers.py 2011-07-11 17:17:32 +0000
222@@ -624,14 +624,16 @@
223 '1.1': wsgi.XMLNS_V11,
224 }[version]
225
226- serializers = {
227+ body_serializers = {
228 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
229 xmlns=xmlns),
230 }
231
232- deserializers = {
233+ body_deserializers = {
234 'application/xml': helper.ServerXMLDeserializer(),
235 }
236
237- return wsgi.Resource(controller, serializers=serializers,
238- deserializers=deserializers)
239+ serializer = wsgi.ResponseSerializer(body_serializers)
240+ deserializer = wsgi.RequestDeserializer(body_deserializers)
241+
242+ return wsgi.Resource(controller, deserializer, serializer)
243
244=== modified file 'nova/api/openstack/shared_ip_groups.py'
245--- nova/api/openstack/shared_ip_groups.py 2011-05-20 18:42:19 +0000
246+++ nova/api/openstack/shared_ip_groups.py 2011-07-11 17:17:32 +0000
247@@ -24,27 +24,27 @@
248 class Controller(object):
249 """ The Shared IP Groups Controller for the Openstack API """
250
251- def index(self, req):
252+ def index(self, req, **kwargs):
253 """ Returns a list of Shared IP Groups for the user """
254 raise faults.Fault(exc.HTTPNotImplemented())
255
256- def show(self, req, id):
257+ def show(self, req, id, **kwargs):
258 """ Shows in-depth information on a specific Shared IP Group """
259 raise faults.Fault(exc.HTTPNotImplemented())
260
261- def update(self, req, id, body):
262+ def update(self, req, id, **kwargs):
263 """ You can't update a Shared IP Group """
264 raise faults.Fault(exc.HTTPNotImplemented())
265
266- def delete(self, req, id):
267+ def delete(self, req, id, **kwargs):
268 """ Deletes a Shared IP Group """
269 raise faults.Fault(exc.HTTPNotImplemented())
270
271- def detail(self, req):
272+ def detail(self, req, **kwargs):
273 """ Returns a complete list of Shared IP Groups """
274 raise faults.Fault(exc.HTTPNotImplemented())
275
276- def create(self, req, body):
277+ def create(self, req, **kwargs):
278 """ Creates a new Shared IP group """
279 raise faults.Fault(exc.HTTPNotImplemented())
280
281
282=== modified file 'nova/api/openstack/users.py'
283--- nova/api/openstack/users.py 2011-05-20 18:42:19 +0000
284+++ nova/api/openstack/users.py 2011-07-11 17:17:32 +0000
285@@ -105,8 +105,10 @@
286 },
287 }
288
289- serializers = {
290+ body_serializers = {
291 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
292 }
293
294- return wsgi.Resource(Controller(), serializers=serializers)
295+ serializer = wsgi.ResponseSerializer(body_serializers)
296+
297+ return wsgi.Resource(Controller(), serializer=serializer)
298
299=== modified file 'nova/api/openstack/versions.py'
300--- nova/api/openstack/versions.py 2011-06-07 18:48:13 +0000
301+++ nova/api/openstack/versions.py 2011-07-11 17:17:32 +0000
302@@ -31,11 +31,12 @@
303 }
304 }
305
306- serializers = {
307+ body_serializers = {
308 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
309 }
310+ serializer = wsgi.ResponseSerializer(body_serializers)
311
312- wsgi.Resource.__init__(self, None, serializers=serializers)
313+ wsgi.Resource.__init__(self, None, serializer=serializer)
314
315 def dispatch(self, request, *args):
316 """Respond to a request for all OpenStack API versions."""
317
318=== modified file 'nova/api/openstack/wsgi.py'
319--- nova/api/openstack/wsgi.py 2011-07-01 19:53:06 +0000
320+++ nova/api/openstack/wsgi.py 2011-07-11 17:17:32 +0000
321@@ -46,38 +46,51 @@
322
323 """
324 if not "Content-Type" in self.headers:
325- raise exception.InvalidContentType(content_type=None)
326+ return None
327
328 allowed_types = ("application/xml", "application/json")
329 content_type = self.content_type
330
331 if content_type not in allowed_types:
332 raise exception.InvalidContentType(content_type=content_type)
333- else:
334- return content_type
335-
336-
337-class TextDeserializer(object):
338- """Custom request body deserialization based on controller action name."""
339+
340+ return content_type
341+
342+
343+class ActionDispatcher(object):
344+ """Maps method name to local methods through action name."""
345+
346+ def dispatch(self, *args, **kwargs):
347+ """Find and call local method."""
348+ action = kwargs.pop('action', 'default')
349+ action_method = getattr(self, str(action), self.default)
350+ return action_method(*args, **kwargs)
351+
352+ def default(self, data):
353+ raise NotImplementedError()
354+
355+
356+class TextDeserializer(ActionDispatcher):
357+ """Default request body deserialization"""
358
359 def deserialize(self, datastring, action='default'):
360- """Find local deserialization method and parse request body."""
361- action_method = getattr(self, str(action), self.default)
362- return action_method(datastring)
363+ return self.dispatch(datastring, action=action)
364
365 def default(self, datastring):
366- """Default deserialization code should live here"""
367- raise NotImplementedError()
368+ return {}
369
370
371 class JSONDeserializer(TextDeserializer):
372
373- def default(self, datastring):
374+ def _from_json(self, datastring):
375 try:
376 return utils.loads(datastring)
377 except ValueError:
378- raise exception.MalformedRequestBody(
379- reason=_("malformed JSON in request body"))
380+ msg = _("cannot understand JSON")
381+ raise exception.MalformedRequestBody(reason=msg)
382+
383+ def default(self, datastring):
384+ return {'body': self._from_json(datastring)}
385
386
387 class XMLDeserializer(TextDeserializer):
388@@ -90,15 +103,15 @@
389 super(XMLDeserializer, self).__init__()
390 self.metadata = metadata or {}
391
392- def default(self, datastring):
393+ def _from_xml(self, datastring):
394 plurals = set(self.metadata.get('plurals', {}))
395
396 try:
397 node = minidom.parseString(datastring).childNodes[0]
398 return {node.nodeName: self._from_xml_node(node, plurals)}
399 except expat.ExpatError:
400- raise exception.MalformedRequestBody(
401- reason=_("malformed XML in request body"))
402+ msg = _("cannot understand XML")
403+ raise exception.MalformedRequestBody(reason=msg)
404
405 def _from_xml_node(self, node, listnames):
406 """Convert a minidom node to a simple Python type.
407@@ -121,21 +134,32 @@
408 listnames)
409 return result
410
411+ def default(self, datastring):
412+ return {'body': self._from_xml(datastring)}
413+
414+
415+class RequestHeadersDeserializer(ActionDispatcher):
416+ """Default request headers deserializer"""
417+
418+ def deserialize(self, request, action):
419+ return self.dispatch(request, action=action)
420+
421+ def default(self, request):
422+ return {}
423+
424
425 class RequestDeserializer(object):
426 """Break up a Request object into more useful pieces."""
427
428- def __init__(self, deserializers=None):
429- """
430- :param deserializers: dictionary of content-type-specific deserializers
431-
432- """
433- self.deserializers = {
434+ def __init__(self, body_deserializers=None, headers_deserializer=None):
435+ self.body_deserializers = {
436 'application/xml': XMLDeserializer(),
437 'application/json': JSONDeserializer(),
438 }
439+ self.body_deserializers.update(body_deserializers or {})
440
441- self.deserializers.update(deserializers or {})
442+ self.headers_deserializer = headers_deserializer or \
443+ RequestHeadersDeserializer()
444
445 def deserialize(self, request):
446 """Extract necessary pieces of the request.
447@@ -149,26 +173,42 @@
448 action_args = self.get_action_args(request.environ)
449 action = action_args.pop('action', None)
450
451- if request.method.lower() in ('post', 'put'):
452- if len(request.body) == 0:
453- action_args['body'] = None
454- else:
455- content_type = request.get_content_type()
456- deserializer = self.get_deserializer(content_type)
457-
458- try:
459- body = deserializer.deserialize(request.body, action)
460- action_args['body'] = body
461- except exception.InvalidContentType:
462- action_args['body'] = None
463+ action_args.update(self.deserialize_headers(request, action))
464+ action_args.update(self.deserialize_body(request, action))
465
466 accept = self.get_expected_content_type(request)
467
468 return (action, action_args, accept)
469
470- def get_deserializer(self, content_type):
471- try:
472- return self.deserializers[content_type]
473+ def deserialize_headers(self, request, action):
474+ return self.headers_deserializer.deserialize(request, action)
475+
476+ def deserialize_body(self, request, action):
477+ try:
478+ content_type = request.get_content_type()
479+ except exception.InvalidContentType:
480+ LOG.debug(_("Unrecognized Content-Type provided in request"))
481+ return {}
482+
483+ if content_type is None:
484+ LOG.debug(_("No Content-Type provided in request"))
485+ return {}
486+
487+ if not len(request.body) > 0:
488+ LOG.debug(_("Empty body provided in request"))
489+ return {}
490+
491+ try:
492+ deserializer = self.get_body_deserializer(content_type)
493+ except exception.InvalidContentType:
494+ LOG.debug(_("Unable to deserialize body as provided Content-Type"))
495+ raise
496+
497+ return deserializer.deserialize(request.body, action)
498+
499+ def get_body_deserializer(self, content_type):
500+ try:
501+ return self.body_deserializers[content_type]
502 except (KeyError, TypeError):
503 raise exception.InvalidContentType(content_type=content_type)
504
505@@ -195,20 +235,18 @@
506 return args
507
508
509-class DictSerializer(object):
510- """Custom response body serialization based on controller action name."""
511+class DictSerializer(ActionDispatcher):
512+ """Default request body serialization"""
513
514 def serialize(self, data, action='default'):
515- """Find local serialization method and encode response body."""
516- action_method = getattr(self, str(action), self.default)
517- return action_method(data)
518+ return self.dispatch(data, action=action)
519
520 def default(self, data):
521- """Default serialization code should live here"""
522- raise NotImplementedError()
523+ return ""
524
525
526 class JSONDictSerializer(DictSerializer):
527+ """Default JSON request body serialization"""
528
529 def default(self, data):
530 return utils.dumps(data)
531@@ -295,19 +333,28 @@
532 return result
533
534
535+class ResponseHeadersSerializer(ActionDispatcher):
536+ """Default response headers serialization"""
537+
538+ def serialize(self, response, data, action):
539+ self.dispatch(response, data, action=action)
540+
541+ def default(self, response, data):
542+ response.status_int = 200
543+
544+
545 class ResponseSerializer(object):
546 """Encode the necessary pieces into a response object"""
547
548- def __init__(self, serializers=None):
549- """
550- :param serializers: dictionary of content-type-specific serializers
551-
552- """
553- self.serializers = {
554+ def __init__(self, body_serializers=None, headers_serializer=None):
555+ self.body_serializers = {
556 'application/xml': XMLDictSerializer(),
557 'application/json': JSONDictSerializer(),
558 }
559- self.serializers.update(serializers or {})
560+ self.body_serializers.update(body_serializers or {})
561+
562+ self.headers_serializer = headers_serializer or \
563+ ResponseHeadersSerializer()
564
565 def serialize(self, response_data, content_type, action='default'):
566 """Serialize a dict into a string and wrap in a wsgi.Request object.
567@@ -317,16 +364,21 @@
568
569 """
570 response = webob.Response()
571+ self.serialize_headers(response, response_data, action)
572+ self.serialize_body(response, response_data, content_type, action)
573+ return response
574+
575+ def serialize_headers(self, response, data, action):
576+ self.headers_serializer.serialize(response, data, action)
577+
578+ def serialize_body(self, response, data, content_type, action):
579 response.headers['Content-Type'] = content_type
580-
581- serializer = self.get_serializer(content_type)
582- response.body = serializer.serialize(response_data, action)
583-
584- return response
585-
586- def get_serializer(self, content_type):
587+ serializer = self.get_body_serializer(content_type)
588+ response.body = serializer.serialize(data, action)
589+
590+ def get_body_serializer(self, content_type):
591 try:
592- return self.serializers[content_type]
593+ return self.body_serializers[content_type]
594 except (KeyError, TypeError):
595 raise exception.InvalidContentType(content_type=content_type)
596
597@@ -343,16 +395,18 @@
598 serialized by requested content type.
599
600 """
601- def __init__(self, controller, serializers=None, deserializers=None):
602+ def __init__(self, controller, deserializer=None, serializer=None):
603 """
604 :param controller: object that implement methods created by routes lib
605- :param serializers: dict of content-type specific text serializers
606- :param deserializers: dict of content-type specific text deserializers
607+ :param deserializer: object that can serialize the output of a
608+ controller into a webob response
609+ :param serializer: object that can deserialize a webob request
610+ into necessary pieces
611
612 """
613 self.controller = controller
614- self.serializer = ResponseSerializer(serializers)
615- self.deserializer = RequestDeserializer(deserializers)
616+ self.deserializer = deserializer or RequestDeserializer()
617+ self.serializer = serializer or ResponseSerializer()
618
619 @webob.dec.wsgify(RequestClass=Request)
620 def __call__(self, request):
621@@ -362,8 +416,7 @@
622 "url": request.url})
623
624 try:
625- action, action_args, accept = self.deserializer.deserialize(
626- request)
627+ action, args, accept = self.deserializer.deserialize(request)
628 except exception.InvalidContentType:
629 msg = _("Unsupported Content-Type")
630 return webob.exc.HTTPBadRequest(explanation=msg)
631@@ -371,11 +424,13 @@
632 msg = _("Malformed request body")
633 return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
634
635- action_result = self.dispatch(request, action, action_args)
636+ action_result = self.dispatch(request, action, args)
637
638 #TODO(bcwaldon): find a more elegant way to pass through non-dict types
639- if type(action_result) is dict:
640- response = self.serializer.serialize(action_result, accept, action)
641+ if type(action_result) is dict or action_result is None:
642+ response = self.serializer.serialize(action_result,
643+ accept,
644+ action=action)
645 else:
646 response = action_result
647
648@@ -394,4 +449,8 @@
649 """Find action-spefic method on controller and call it."""
650
651 controller_method = getattr(self.controller, action)
652- return controller_method(req=request, **action_args)
653+ try:
654+ return controller_method(req=request, **action_args)
655+ except TypeError, exc:
656+ LOG.debug(str(exc))
657+ return webob.exc.HTTPBadRequest()
658
659=== modified file 'nova/api/openstack/zones.py'
660--- nova/api/openstack/zones.py 2011-06-14 19:59:16 +0000
661+++ nova/api/openstack/zones.py 2011-07-11 17:17:32 +0000
662@@ -196,14 +196,15 @@
663 },
664 }
665
666- serializers = {
667+ body_serializers = {
668 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
669 metadata=metadata),
670 }
671+ serializer = wsgi.ResponseSerializer(body_serializers)
672
673- deserializers = {
674+ body_deserializers = {
675 'application/xml': helper.ServerXMLDeserializer(),
676 }
677+ deserializer = wsgi.RequestDeserializer(body_deserializers)
678
679- return wsgi.Resource(controller, serializers=serializers,
680- deserializers=deserializers)
681+ return wsgi.Resource(controller, deserializer, serializer)
682
683=== modified file 'nova/tests/api/openstack/contrib/test_floating_ips.py'
684--- nova/tests/api/openstack/contrib/test_floating_ips.py 2011-06-27 16:36:53 +0000
685+++ nova/tests/api/openstack/contrib/test_floating_ips.py 2011-07-11 17:17:32 +0000
686@@ -139,7 +139,9 @@
687 def test_floating_ip_allocate(self):
688 req = webob.Request.blank('/v1.1/os-floating-ips')
689 req.method = 'POST'
690+ req.headers['Content-Type'] = 'application/json'
691 res = req.get_response(fakes.wsgi_app())
692+ print res
693 self.assertEqual(res.status_int, 200)
694 ip = json.loads(res.body)['allocated']
695 expected = {
696@@ -177,6 +179,7 @@
697 def test_floating_ip_disassociate(self):
698 req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate')
699 req.method = 'POST'
700+ req.headers['Content-Type'] = 'application/json'
701 res = req.get_response(fakes.wsgi_app())
702 self.assertEqual(res.status_int, 200)
703 ip = json.loads(res.body)['disassociated']
704
705=== modified file 'nova/tests/api/openstack/test_servers.py'
706--- nova/tests/api/openstack/test_servers.py 2011-07-08 16:47:34 +0000
707+++ nova/tests/api/openstack/test_servers.py 2011-07-11 17:17:32 +0000
708@@ -905,7 +905,7 @@
709 req = webob.Request.blank('/v1.0/servers/1')
710 req.method = 'PUT'
711 res = req.get_response(fakes.wsgi_app())
712- self.assertEqual(res.status_int, 422)
713+ self.assertEqual(res.status_int, 400)
714
715 def test_update_nonstring_name(self):
716 """ Confirm that update is filtering params """
717@@ -1608,7 +1608,7 @@
718 "imageId": "1",
719 "flavorId": "1",
720 }}
721- self.assertEquals(request, expected)
722+ self.assertEquals(request['body'], expected)
723
724 def test_request_with_empty_metadata(self):
725 serial_request = """
726@@ -1623,7 +1623,7 @@
727 "flavorId": "1",
728 "metadata": {},
729 }}
730- self.assertEquals(request, expected)
731+ self.assertEquals(request['body'], expected)
732
733 def test_request_with_empty_personality(self):
734 serial_request = """
735@@ -1638,7 +1638,7 @@
736 "flavorId": "1",
737 "personality": [],
738 }}
739- self.assertEquals(request, expected)
740+ self.assertEquals(request['body'], expected)
741
742 def test_request_with_empty_metadata_and_personality(self):
743 serial_request = """
744@@ -1655,7 +1655,7 @@
745 "metadata": {},
746 "personality": [],
747 }}
748- self.assertEquals(request, expected)
749+ self.assertEquals(request['body'], expected)
750
751 def test_request_with_empty_metadata_and_personality_reversed(self):
752 serial_request = """
753@@ -1672,7 +1672,7 @@
754 "metadata": {},
755 "personality": [],
756 }}
757- self.assertEquals(request, expected)
758+ self.assertEquals(request['body'], expected)
759
760 def test_request_with_one_personality(self):
761 serial_request = """
762@@ -1684,7 +1684,7 @@
763 </server>"""
764 request = self.deserializer.deserialize(serial_request, 'create')
765 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
766- self.assertEquals(request["server"]["personality"], expected)
767+ self.assertEquals(request['body']["server"]["personality"], expected)
768
769 def test_request_with_two_personalities(self):
770 serial_request = """
771@@ -1695,7 +1695,7 @@
772 request = self.deserializer.deserialize(serial_request, 'create')
773 expected = [{"path": "/etc/conf", "contents": "aabbccdd"},
774 {"path": "/etc/sudoers", "contents": "abcd"}]
775- self.assertEquals(request["server"]["personality"], expected)
776+ self.assertEquals(request['body']["server"]["personality"], expected)
777
778 def test_request_second_personality_node_ignored(self):
779 serial_request = """
780@@ -1710,7 +1710,7 @@
781 </server>"""
782 request = self.deserializer.deserialize(serial_request, 'create')
783 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
784- self.assertEquals(request["server"]["personality"], expected)
785+ self.assertEquals(request['body']["server"]["personality"], expected)
786
787 def test_request_with_one_personality_missing_path(self):
788 serial_request = """
789@@ -1719,7 +1719,7 @@
790 <personality><file>aabbccdd</file></personality></server>"""
791 request = self.deserializer.deserialize(serial_request, 'create')
792 expected = [{"contents": "aabbccdd"}]
793- self.assertEquals(request["server"]["personality"], expected)
794+ self.assertEquals(request['body']["server"]["personality"], expected)
795
796 def test_request_with_one_personality_empty_contents(self):
797 serial_request = """
798@@ -1728,7 +1728,7 @@
799 <personality><file path="/etc/conf"></file></personality></server>"""
800 request = self.deserializer.deserialize(serial_request, 'create')
801 expected = [{"path": "/etc/conf", "contents": ""}]
802- self.assertEquals(request["server"]["personality"], expected)
803+ self.assertEquals(request['body']["server"]["personality"], expected)
804
805 def test_request_with_one_personality_empty_contents_variation(self):
806 serial_request = """
807@@ -1737,7 +1737,7 @@
808 <personality><file path="/etc/conf"/></personality></server>"""
809 request = self.deserializer.deserialize(serial_request, 'create')
810 expected = [{"path": "/etc/conf", "contents": ""}]
811- self.assertEquals(request["server"]["personality"], expected)
812+ self.assertEquals(request['body']["server"]["personality"], expected)
813
814 def test_request_with_one_metadata(self):
815 serial_request = """
816@@ -1749,7 +1749,7 @@
817 </server>"""
818 request = self.deserializer.deserialize(serial_request, 'create')
819 expected = {"alpha": "beta"}
820- self.assertEquals(request["server"]["metadata"], expected)
821+ self.assertEquals(request['body']["server"]["metadata"], expected)
822
823 def test_request_with_two_metadata(self):
824 serial_request = """
825@@ -1762,7 +1762,7 @@
826 </server>"""
827 request = self.deserializer.deserialize(serial_request, 'create')
828 expected = {"alpha": "beta", "foo": "bar"}
829- self.assertEquals(request["server"]["metadata"], expected)
830+ self.assertEquals(request['body']["server"]["metadata"], expected)
831
832 def test_request_with_metadata_missing_value(self):
833 serial_request = """
834@@ -1774,7 +1774,7 @@
835 </server>"""
836 request = self.deserializer.deserialize(serial_request, 'create')
837 expected = {"alpha": ""}
838- self.assertEquals(request["server"]["metadata"], expected)
839+ self.assertEquals(request['body']["server"]["metadata"], expected)
840
841 def test_request_with_two_metadata_missing_value(self):
842 serial_request = """
843@@ -1787,7 +1787,7 @@
844 </server>"""
845 request = self.deserializer.deserialize(serial_request, 'create')
846 expected = {"alpha": "", "delta": ""}
847- self.assertEquals(request["server"]["metadata"], expected)
848+ self.assertEquals(request['body']["server"]["metadata"], expected)
849
850 def test_request_with_metadata_missing_key(self):
851 serial_request = """
852@@ -1799,7 +1799,7 @@
853 </server>"""
854 request = self.deserializer.deserialize(serial_request, 'create')
855 expected = {"": "beta"}
856- self.assertEquals(request["server"]["metadata"], expected)
857+ self.assertEquals(request['body']["server"]["metadata"], expected)
858
859 def test_request_with_two_metadata_missing_key(self):
860 serial_request = """
861@@ -1812,7 +1812,7 @@
862 </server>"""
863 request = self.deserializer.deserialize(serial_request, 'create')
864 expected = {"": "gamma"}
865- self.assertEquals(request["server"]["metadata"], expected)
866+ self.assertEquals(request['body']["server"]["metadata"], expected)
867
868 def test_request_with_metadata_duplicate_key(self):
869 serial_request = """
870@@ -1825,7 +1825,7 @@
871 </server>"""
872 request = self.deserializer.deserialize(serial_request, 'create')
873 expected = {"foo": "baz"}
874- self.assertEquals(request["server"]["metadata"], expected)
875+ self.assertEquals(request['body']["server"]["metadata"], expected)
876
877 def test_canonical_request_from_docs(self):
878 serial_request = """
879@@ -1871,7 +1871,7 @@
880 ],
881 }}
882 request = self.deserializer.deserialize(serial_request, 'create')
883- self.assertEqual(request, expected)
884+ self.assertEqual(request['body'], expected)
885
886 def test_request_xmlser_with_flavor_image_href(self):
887 serial_request = """
888@@ -1881,9 +1881,9 @@
889 flavorRef="http://localhost:8774/v1.1/flavors/1">
890 </server>"""
891 request = self.deserializer.deserialize(serial_request, 'create')
892- self.assertEquals(request["server"]["flavorRef"],
893+ self.assertEquals(request['body']["server"]["flavorRef"],
894 "http://localhost:8774/v1.1/flavors/1")
895- self.assertEquals(request["server"]["imageRef"],
896+ self.assertEquals(request['body']["server"]["imageRef"],
897 "http://localhost:8774/v1.1/images/1")
898
899
900@@ -1948,7 +1948,7 @@
901
902 def _get_create_request_json(self, body_dict):
903 req = webob.Request.blank('/v1.0/servers')
904- req.content_type = 'application/json'
905+ req.headers['Content-Type'] = 'application/json'
906 req.method = 'POST'
907 req.body = json.dumps(body_dict)
908 return req
909
910=== modified file 'nova/tests/api/openstack/test_wsgi.py'
911--- nova/tests/api/openstack/test_wsgi.py 2011-06-24 12:01:51 +0000
912+++ nova/tests/api/openstack/test_wsgi.py 2011-07-11 17:17:32 +0000
913@@ -12,8 +12,7 @@
914 def test_content_type_missing(self):
915 request = wsgi.Request.blank('/tests/123', method='POST')
916 request.body = "<body />"
917- self.assertRaises(exception.InvalidContentType,
918- request.get_content_type)
919+ self.assertEqual(None, request.get_content_type())
920
921 def test_content_type_unsupported(self):
922 request = wsgi.Request.blank('/tests/123', method='POST')
923@@ -76,24 +75,48 @@
924 self.assertEqual(result, "application/json")
925
926
927+class ActionDispatcherTest(test.TestCase):
928+ def test_dispatch(self):
929+ serializer = wsgi.ActionDispatcher()
930+ serializer.create = lambda x: 'pants'
931+ self.assertEqual(serializer.dispatch({}, action='create'), 'pants')
932+
933+ def test_dispatch_action_None(self):
934+ serializer = wsgi.ActionDispatcher()
935+ serializer.create = lambda x: 'pants'
936+ serializer.default = lambda x: 'trousers'
937+ self.assertEqual(serializer.dispatch({}, action=None), 'trousers')
938+
939+ def test_dispatch_default(self):
940+ serializer = wsgi.ActionDispatcher()
941+ serializer.create = lambda x: 'pants'
942+ serializer.default = lambda x: 'trousers'
943+ self.assertEqual(serializer.dispatch({}, action='update'), 'trousers')
944+
945+
946+class ResponseHeadersSerializerTest(test.TestCase):
947+ def test_default(self):
948+ serializer = wsgi.ResponseHeadersSerializer()
949+ response = webob.Response()
950+ serializer.serialize(response, {'v': '123'}, 'asdf')
951+ self.assertEqual(response.status_int, 200)
952+
953+ def test_custom(self):
954+ class Serializer(wsgi.ResponseHeadersSerializer):
955+ def update(self, response, data):
956+ response.status_int = 404
957+ response.headers['X-Custom-Header'] = data['v']
958+ serializer = Serializer()
959+ response = webob.Response()
960+ serializer.serialize(response, {'v': '123'}, 'update')
961+ self.assertEqual(response.status_int, 404)
962+ self.assertEqual(response.headers['X-Custom-Header'], '123')
963+
964+
965 class DictSerializerTest(test.TestCase):
966- def test_dispatch(self):
967- serializer = wsgi.DictSerializer()
968- serializer.create = lambda x: 'pants'
969- serializer.default = lambda x: 'trousers'
970- self.assertEqual(serializer.serialize({}, 'create'), 'pants')
971-
972 def test_dispatch_default(self):
973 serializer = wsgi.DictSerializer()
974- serializer.create = lambda x: 'pants'
975- serializer.default = lambda x: 'trousers'
976- self.assertEqual(serializer.serialize({}, 'update'), 'trousers')
977-
978- def test_dispatch_action_None(self):
979- serializer = wsgi.DictSerializer()
980- serializer.create = lambda x: 'pants'
981- serializer.default = lambda x: 'trousers'
982- self.assertEqual(serializer.serialize({}, None), 'trousers')
983+ self.assertEqual(serializer.serialize({}, 'update'), '')
984
985
986 class XMLDictSerializerTest(test.TestCase):
987@@ -117,23 +140,9 @@
988
989
990 class TextDeserializerTest(test.TestCase):
991- def test_dispatch(self):
992- deserializer = wsgi.TextDeserializer()
993- deserializer.create = lambda x: 'pants'
994- deserializer.default = lambda x: 'trousers'
995- self.assertEqual(deserializer.deserialize({}, 'create'), 'pants')
996-
997 def test_dispatch_default(self):
998 deserializer = wsgi.TextDeserializer()
999- deserializer.create = lambda x: 'pants'
1000- deserializer.default = lambda x: 'trousers'
1001- self.assertEqual(deserializer.deserialize({}, 'update'), 'trousers')
1002-
1003- def test_dispatch_action_None(self):
1004- deserializer = wsgi.TextDeserializer()
1005- deserializer.create = lambda x: 'pants'
1006- deserializer.default = lambda x: 'trousers'
1007- self.assertEqual(deserializer.deserialize({}, None), 'trousers')
1008+ self.assertEqual(deserializer.deserialize({}, 'update'), {})
1009
1010
1011 class JSONDeserializerTest(test.TestCase):
1012@@ -144,12 +153,17 @@
1013 "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
1014 "d": {"e": "1"},
1015 "f": "1"}}"""
1016- as_dict = dict(a={
1017- 'a1': '1',
1018- 'a2': '2',
1019- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
1020- 'd': {'e': '1'},
1021- 'f': '1'})
1022+ as_dict = {
1023+ 'body': {
1024+ 'a': {
1025+ 'a1': '1',
1026+ 'a2': '2',
1027+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
1028+ 'd': {'e': '1'},
1029+ 'f': '1',
1030+ },
1031+ },
1032+ }
1033 deserializer = wsgi.JSONDeserializer()
1034 self.assertEqual(deserializer.deserialize(data), as_dict)
1035
1036@@ -163,23 +177,44 @@
1037 <f>1</f>
1038 </a>
1039 """.strip()
1040- as_dict = dict(a={
1041- 'a1': '1',
1042- 'a2': '2',
1043- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
1044- 'd': {'e': '1'},
1045- 'f': '1'})
1046+ as_dict = {
1047+ 'body': {
1048+ 'a': {
1049+ 'a1': '1',
1050+ 'a2': '2',
1051+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
1052+ 'd': {'e': '1'},
1053+ 'f': '1',
1054+ },
1055+ },
1056+ }
1057 metadata = {'plurals': {'bs': 'b', 'ts': 't'}}
1058 deserializer = wsgi.XMLDeserializer(metadata=metadata)
1059 self.assertEqual(deserializer.deserialize(xml), as_dict)
1060
1061 def test_xml_empty(self):
1062 xml = """<a></a>"""
1063- as_dict = {"a": {}}
1064+ as_dict = {"body": {"a": {}}}
1065 deserializer = wsgi.XMLDeserializer()
1066 self.assertEqual(deserializer.deserialize(xml), as_dict)
1067
1068
1069+class RequestHeadersDeserializerTest(test.TestCase):
1070+ def test_default(self):
1071+ deserializer = wsgi.RequestHeadersDeserializer()
1072+ req = wsgi.Request.blank('/')
1073+ self.assertEqual(deserializer.deserialize(req, 'asdf'), {})
1074+
1075+ def test_custom(self):
1076+ class Deserializer(wsgi.RequestHeadersDeserializer):
1077+ def update(self, request):
1078+ return {'a': request.headers['X-Custom-Header']}
1079+ deserializer = Deserializer()
1080+ req = wsgi.Request.blank('/')
1081+ req.headers['X-Custom-Header'] = 'b'
1082+ self.assertEqual(deserializer.deserialize(req, 'update'), {'a': 'b'})
1083+
1084+
1085 class ResponseSerializerTest(test.TestCase):
1086 def setUp(self):
1087 class JSONSerializer(object):
1088@@ -190,29 +225,36 @@
1089 def serialize(self, data, action='default'):
1090 return 'pew_xml'
1091
1092- self.serializers = {
1093+ class HeadersSerializer(object):
1094+ def serialize(self, response, data, action):
1095+ response.status_int = 404
1096+
1097+ self.body_serializers = {
1098 'application/json': JSONSerializer(),
1099 'application/XML': XMLSerializer(),
1100 }
1101
1102- self.serializer = wsgi.ResponseSerializer(serializers=self.serializers)
1103+ self.serializer = wsgi.ResponseSerializer(self.body_serializers,
1104+ HeadersSerializer())
1105
1106 def tearDown(self):
1107 pass
1108
1109 def test_get_serializer(self):
1110- self.assertEqual(self.serializer.get_serializer('application/json'),
1111- self.serializers['application/json'])
1112+ ctype = 'application/json'
1113+ self.assertEqual(self.serializer.get_body_serializer(ctype),
1114+ self.body_serializers[ctype])
1115
1116 def test_get_serializer_unknown_content_type(self):
1117 self.assertRaises(exception.InvalidContentType,
1118- self.serializer.get_serializer,
1119+ self.serializer.get_body_serializer,
1120 'application/unknown')
1121
1122 def test_serialize_response(self):
1123 response = self.serializer.serialize({}, 'application/json')
1124 self.assertEqual(response.headers['Content-Type'], 'application/json')
1125 self.assertEqual(response.body, 'pew_json')
1126+ self.assertEqual(response.status_int, 404)
1127
1128 def test_serialize_response_dict_to_unknown_content_type(self):
1129 self.assertRaises(exception.InvalidContentType,
1130@@ -230,24 +272,23 @@
1131 def deserialize(self, data, action='default'):
1132 return 'pew_xml'
1133
1134- self.deserializers = {
1135+ self.body_deserializers = {
1136 'application/json': JSONDeserializer(),
1137 'application/XML': XMLDeserializer(),
1138 }
1139
1140- self.deserializer = wsgi.RequestDeserializer(
1141- deserializers=self.deserializers)
1142+ self.deserializer = wsgi.RequestDeserializer(self.body_deserializers)
1143
1144 def tearDown(self):
1145 pass
1146
1147 def test_get_deserializer(self):
1148- expected = self.deserializer.get_deserializer('application/json')
1149- self.assertEqual(expected, self.deserializers['application/json'])
1150+ expected = self.deserializer.get_body_deserializer('application/json')
1151+ self.assertEqual(expected, self.body_deserializers['application/json'])
1152
1153 def test_get_deserializer_unknown_content_type(self):
1154 self.assertRaises(exception.InvalidContentType,
1155- self.deserializer.get_deserializer,
1156+ self.deserializer.get_body_deserializer,
1157 'application/unknown')
1158
1159 def test_get_expected_content_type(self):
1160
1161=== modified file 'nova/tests/integrated/api/client.py'
1162--- nova/tests/integrated/api/client.py 2011-06-24 12:01:51 +0000
1163+++ nova/tests/integrated/api/client.py 2011-07-11 17:17:32 +0000
1164@@ -71,8 +71,8 @@
1165 self.auth_uri = auth_uri
1166
1167 def request(self, url, method='GET', body=None, headers=None):
1168- if headers is None:
1169- headers = {}
1170+ _headers = {'Content-Type': 'application/json'}
1171+ _headers.update(headers or {})
1172
1173 parsed_url = urlparse.urlparse(url)
1174 port = parsed_url.port
1175@@ -94,9 +94,8 @@
1176 LOG.info(_("Doing %(method)s on %(relative_url)s") % locals())
1177 if body:
1178 LOG.info(_("Body: %s") % body)
1179- headers.setdefault('Content-Type', 'application/json')
1180
1181- conn.request(method, relative_url, body, headers)
1182+ conn.request(method, relative_url, body, _headers)
1183 response = conn.getresponse()
1184 return response
1185