Refactor to separate serializers from wsgi controller

Remove the serializers from heat.common.wsgi, so we break the
circular import which happens if you want to import
heat.api.aws.exceptions to do a determination based on exception
type, which is required to avoid the faultwrap exception disguise
which is not applicable to the CFN API.

Partial-Bug: #1291079

Change-Id: I7498d78f8ec6098b28fb183eaaa04aa81fced3eb
This commit is contained in:
Steven Hardy 2014-04-08 16:53:16 +01:00
parent 15892aac16
commit ba48137e24
13 changed files with 220 additions and 163 deletions

View File

@ -18,7 +18,7 @@
import webob.exc
from heat.common import wsgi
from heat.common import serializers
from heat.openstack.common.gettextutils import _
from heat.openstack.common.rpc import common as rpc_common
@ -44,7 +44,7 @@ class HeatAPIException(webob.exc.HTTPError):
paste pipeline. We serialize in XML by default (as AWS does)
'''
webob.exc.HTTPError.__init__(self, detail=detail)
serializer = wsgi.XMLResponseSerializer()
serializer = serializers.XMLResponseSerializer()
serializer.default(self, self.get_unserialized_body())
def get_unserialized_body(self):

View File

@ -27,6 +27,7 @@ import webob
cfg.CONF.import_opt('debug', 'heat.openstack.common.log')
from heat.common import serializers
from heat.common import exception
from heat.openstack.common import log as logging
import heat.openstack.common.rpc.common as rpc_common
@ -44,9 +45,9 @@ class Fault(object):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
if req.content_type == 'application/xml':
serializer = wsgi.XMLResponseSerializer()
serializer = serializers.XMLResponseSerializer()
else:
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
resp = webob.Response(request=req)
default_webob_exc = webob.exc.HTTPInternalServerError()
resp.status_code = self.error.get('code', default_webob_exc.code)

View File

@ -14,6 +14,7 @@
from webob import exc
from heat.api.openstack.v1 import util
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import client as rpc_client
@ -62,5 +63,5 @@ def create_resource(options):
Actions action factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(ActionController(options), deserializer, serializer)

View File

@ -14,6 +14,7 @@
from oslo.config import cfg
from heat.api.openstack.v1 import util
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import client as rpc_client
@ -46,6 +47,6 @@ def create_resource(options):
BuildInfo factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(BuildInfoController(options), deserializer,
serializer)

View File

@ -17,6 +17,7 @@ from webob import exc
from heat.api.openstack.v1 import util
from heat.common import identifier
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import api as engine_api
from heat.rpc import client as rpc_client
@ -128,5 +129,5 @@ def create_resource(options):
Events resource factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(EventController(options), deserializer, serializer)

View File

@ -15,6 +15,7 @@ import itertools
from heat.api.openstack.v1 import util
from heat.common import identifier
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import api as engine_api
from heat.rpc import client as rpc_client
@ -113,5 +114,5 @@ def create_resource(options):
Resources resource factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(ResourceController(options), deserializer, serializer)

View File

@ -14,6 +14,7 @@
from webob import exc
from heat.api.openstack.v1 import util
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import client as rpc_client
@ -77,6 +78,6 @@ def create_resource(options):
Software configs resource factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(
SoftwareConfigController(options), deserializer, serializer)

View File

@ -14,6 +14,7 @@
from webob import exc
from heat.api.openstack.v1 import util
from heat.common import serializers
from heat.common import wsgi
from heat.rpc import client as rpc_client
@ -109,6 +110,6 @@ def create_resource(options):
Software deployments resource factory method.
"""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
return wsgi.Resource(
SoftwareDeploymentController(options), deserializer, serializer)

View File

@ -21,6 +21,7 @@ from heat.api.openstack.v1 import util
from heat.api.openstack.v1.views import stacks_view
from heat.common import environment_format
from heat.common import identifier
from heat.common import serializers
from heat.common import template_format
from heat.common import urlfetch
from heat.common import wsgi
@ -378,7 +379,7 @@ class StackController(object):
return self.rpc_client.generate_template(req.context, type_name)
class StackSerializer(wsgi.JSONResponseSerializer):
class StackSerializer(serializers.JSONResponseSerializer):
"""Handles serialization of specific controller method responses."""
def _populate_response_header(self, response, location, status):

View File

@ -0,0 +1,89 @@
#
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Utility methods for serializing responses
"""
import datetime
import json
from lxml import etree
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
class JSONResponseSerializer(object):
def to_json(self, data):
def sanitizer(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return obj
response = json.dumps(data, default=sanitizer)
logger.debug("JSON response : %s" % response)
return response
def default(self, response, result):
response.content_type = 'application/json'
response.body = self.to_json(result)
# Escape XML serialization for these keys, as the AWS API defines them as
# JSON inside XML when the response format is XML.
JSON_ONLY_KEYS = ('TemplateBody', 'Metadata')
class XMLResponseSerializer(object):
def object_to_element(self, obj, element):
if isinstance(obj, list):
for item in obj:
subelement = etree.SubElement(element, "member")
self.object_to_element(item, subelement)
elif isinstance(obj, dict):
for key, value in obj.items():
subelement = etree.SubElement(element, key)
if key in JSON_ONLY_KEYS:
if value:
# Need to use json.dumps for the JSON inside XML
# otherwise quotes get mangled and json.loads breaks
try:
subelement.text = json.dumps(value)
except TypeError:
subelement.text = str(value)
else:
self.object_to_element(value, subelement)
else:
element.text = str(obj)
def to_xml(self, data):
# Assumption : root node is dict with single key
root = data.keys()[0]
eltree = etree.Element(root)
self.object_to_element(data.get(root), eltree)
response = etree.tostring(eltree)
logger.debug("XML response : %s" % response)
return response
def default(self, response, result):
response.content_type = 'application/xml'
response.body = self.to_xml(result)

View File

@ -20,7 +20,6 @@
Utility methods for working with WSGI servers
"""
import datetime
import errno
import json
import logging
@ -34,7 +33,6 @@ from eventlet.green import socket
from eventlet.green import ssl
import eventlet.greenio
import eventlet.wsgi
from lxml import etree
from oslo.config import cfg
from paste import deploy
import routes
@ -43,6 +41,7 @@ import webob.dec
import webob.exc
from heat.common import exception
from heat.common import serializers
from heat.openstack.common import gettextutils
from heat.openstack.common import importutils
@ -565,65 +564,6 @@ class JSONRequestDeserializer(object):
return {}
class JSONResponseSerializer(object):
def to_json(self, data):
def sanitizer(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return obj
response = json.dumps(data, default=sanitizer)
logging.debug("JSON response : %s" % response)
return response
def default(self, response, result):
response.content_type = 'application/json'
response.body = self.to_json(result)
# Escape XML serialization for these keys, as the AWS API defines them as
# JSON inside XML when the response format is XML.
JSON_ONLY_KEYS = ('TemplateBody', 'Metadata')
class XMLResponseSerializer(object):
def object_to_element(self, obj, element):
if isinstance(obj, list):
for item in obj:
subelement = etree.SubElement(element, "member")
self.object_to_element(item, subelement)
elif isinstance(obj, dict):
for key, value in obj.items():
subelement = etree.SubElement(element, key)
if key in JSON_ONLY_KEYS:
if value:
# Need to use json.dumps for the JSON inside XML
# otherwise quotes get mangled and json.loads breaks
try:
subelement.text = json.dumps(value)
except TypeError:
subelement.text = str(value)
else:
self.object_to_element(value, subelement)
else:
element.text = str(obj)
def to_xml(self, data):
# Assumption : root node is dict with single key
root = data.keys()[0]
eltree = etree.Element(root)
self.object_to_element(data.get(root), eltree)
response = etree.tostring(eltree)
logging.debug("XML response : %s" % response)
return response
def default(self, response, result):
response.content_type = 'application/xml'
response.body = self.to_xml(result)
class Resource(object):
"""
WSGI app that handles (de)serialization and controller dispatch.
@ -708,9 +648,9 @@ class Resource(object):
serializer = self.serializer
if serializer is None:
if content_type == "JSON":
serializer = JSONResponseSerializer()
serializer = serializers.JSONResponseSerializer()
else:
serializer = XMLResponseSerializer()
serializer = serializers.XMLResponseSerializer()
response = webob.Response(request=request)
self.dispatch(serializer, action, response, action_result)

View File

@ -0,0 +1,109 @@
#
# Copyright 2010-2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import webob
from heat.common import serializers
from heat.tests.common import HeatTestCase
class JSONResponseSerializerTest(HeatTestCase):
def test_to_json(self):
fixture = {"key": "value"}
expected = '{"key": "value"}'
actual = serializers.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_to_json_with_date_format_value(self):
fixture = {"date": datetime.datetime(1, 3, 8, 2)}
expected = '{"date": "0001-03-08T02:00:00"}'
actual = serializers.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_to_json_with_more_deep_format(self):
fixture = {"is_public": True, "name": [{"name1": "test"}]}
expected = '{"is_public": true, "name": [{"name1": "test"}]}'
actual = serializers.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_default(self):
fixture = {"key": "value"}
response = webob.Response()
serializers.JSONResponseSerializer().default(response, fixture)
self.assertEqual(200, response.status_int)
content_types = filter(lambda h: h[0] == 'Content-Type',
response.headerlist)
self.assertEqual(1, len(content_types))
self.assertEqual('application/json', response.content_type)
self.assertEqual('{"key": "value"}', response.body)
class XMLResponseSerializerTest(HeatTestCase):
def test_to_xml(self):
fixture = {"key": "value"}
expected = '<key>value</key>'
actual = serializers.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_date_format_value(self):
fixture = {"date": datetime.datetime(1, 3, 8, 2)}
expected = '<date>0001-03-08 02:00:00</date>'
actual = serializers.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_list(self):
fixture = {"name": ["1", "2"]}
expected = '<name><member>1</member><member>2</member></name>'
actual = serializers.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_more_deep_format(self):
# Note we expect tree traversal from one root key, which is compatible
# with the AWS format responses we need to serialize
fixture = {"aresponse":
{"is_public": True, "name": [{"name1": "test"}]}}
expected = ('<aresponse><is_public>True</is_public>'
'<name><member><name1>test</name1></member></name>'
'</aresponse>')
actual = serializers.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_json_only_keys(self):
# Certain keys are excluded from serialization because CFN
# format demands a json blob in the XML body
fixture = {"aresponse":
{"is_public": True,
"TemplateBody": {"name1": "test"},
"Metadata": {"name2": "test2"}}}
expected = ('<aresponse><is_public>True</is_public>'
'<TemplateBody>{"name1": "test"}</TemplateBody>'
'<Metadata>{"name2": "test2"}</Metadata></aresponse>')
actual = serializers.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_default(self):
fixture = {"key": "value"}
response = webob.Response()
serializers.XMLResponseSerializer().default(response, fixture)
self.assertEqual(200, response.status_int)
content_types = filter(lambda h: h[0] == 'Content-Type',
response.headerlist)
self.assertEqual(1, len(content_types))
self.assertEqual('application/xml', response.content_type)
self.assertEqual('<key>value</key>', response.body)

View File

@ -15,7 +15,6 @@
# under the License.
import datetime
import json
from oslo.config import cfg
import stubout
@ -258,94 +257,6 @@ class ResourceExceptionHandlingTest(HeatTestCase):
self.assertNotIn(str(e), self.logger.output)
class JSONResponseSerializerTest(HeatTestCase):
def test_to_json(self):
fixture = {"key": "value"}
expected = '{"key": "value"}'
actual = wsgi.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_to_json_with_date_format_value(self):
fixture = {"date": datetime.datetime(1, 3, 8, 2)}
expected = '{"date": "0001-03-08T02:00:00"}'
actual = wsgi.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_to_json_with_more_deep_format(self):
fixture = {"is_public": True, "name": [{"name1": "test"}]}
expected = '{"is_public": true, "name": [{"name1": "test"}]}'
actual = wsgi.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_default(self):
fixture = {"key": "value"}
response = webob.Response()
wsgi.JSONResponseSerializer().default(response, fixture)
self.assertEqual(200, response.status_int)
content_types = filter(lambda h: h[0] == 'Content-Type',
response.headerlist)
self.assertEqual(1, len(content_types))
self.assertEqual('application/json', response.content_type)
self.assertEqual('{"key": "value"}', response.body)
class XMLResponseSerializerTest(HeatTestCase):
def test_to_xml(self):
fixture = {"key": "value"}
expected = '<key>value</key>'
actual = wsgi.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_date_format_value(self):
fixture = {"date": datetime.datetime(1, 3, 8, 2)}
expected = '<date>0001-03-08 02:00:00</date>'
actual = wsgi.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_list(self):
fixture = {"name": ["1", "2"]}
expected = '<name><member>1</member><member>2</member></name>'
actual = wsgi.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_more_deep_format(self):
# Note we expect tree traversal from one root key, which is compatible
# with the AWS format responses we need to serialize
fixture = {"aresponse":
{"is_public": True, "name": [{"name1": "test"}]}}
expected = ('<aresponse><is_public>True</is_public>'
'<name><member><name1>test</name1></member></name>'
'</aresponse>')
actual = wsgi.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_to_xml_with_json_only_keys(self):
# Certain keys are excluded from serialization because CFN
# format demands a json blob in the XML body
fixture = {"aresponse":
{"is_public": True,
"TemplateBody": {"name1": "test"},
"Metadata": {"name2": "test2"}}}
expected = ('<aresponse><is_public>True</is_public>'
'<TemplateBody>{"name1": "test"}</TemplateBody>'
'<Metadata>{"name2": "test2"}</Metadata></aresponse>')
actual = wsgi.XMLResponseSerializer().to_xml(fixture)
self.assertEqual(expected, actual)
def test_default(self):
fixture = {"key": "value"}
response = webob.Response()
wsgi.XMLResponseSerializer().default(response, fixture)
self.assertEqual(200, response.status_int)
content_types = filter(lambda h: h[0] == 'Content-Type',
response.headerlist)
self.assertEqual(1, len(content_types))
self.assertEqual('application/xml', response.content_type)
self.assertEqual('<key>value</key>', response.body)
class JSONRequestDeserializerTest(HeatTestCase):
def test_has_body_no_content_length(self):