diff --git a/heat/api/aws/exception.py b/heat/api/aws/exception.py index 5f5ba2ecec..1a8834339c 100644 --- a/heat/api/aws/exception.py +++ b/heat/api/aws/exception.py @@ -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): diff --git a/heat/api/middleware/fault.py b/heat/api/middleware/fault.py index 8e3d31ec0f..3894b4b7be 100644 --- a/heat/api/middleware/fault.py +++ b/heat/api/middleware/fault.py @@ -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) diff --git a/heat/api/openstack/v1/actions.py b/heat/api/openstack/v1/actions.py index b5b93dcefe..819edfe7bf 100644 --- a/heat/api/openstack/v1/actions.py +++ b/heat/api/openstack/v1/actions.py @@ -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) diff --git a/heat/api/openstack/v1/build_info.py b/heat/api/openstack/v1/build_info.py index dfe8b06c67..2cdfa6a3da 100644 --- a/heat/api/openstack/v1/build_info.py +++ b/heat/api/openstack/v1/build_info.py @@ -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) diff --git a/heat/api/openstack/v1/events.py b/heat/api/openstack/v1/events.py index a894467ec4..e49bfcfbc0 100644 --- a/heat/api/openstack/v1/events.py +++ b/heat/api/openstack/v1/events.py @@ -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) diff --git a/heat/api/openstack/v1/resources.py b/heat/api/openstack/v1/resources.py index d650c93840..c324dc9ac4 100644 --- a/heat/api/openstack/v1/resources.py +++ b/heat/api/openstack/v1/resources.py @@ -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) diff --git a/heat/api/openstack/v1/software_configs.py b/heat/api/openstack/v1/software_configs.py index 0ba6592522..2bc5cdc75d 100644 --- a/heat/api/openstack/v1/software_configs.py +++ b/heat/api/openstack/v1/software_configs.py @@ -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) diff --git a/heat/api/openstack/v1/software_deployments.py b/heat/api/openstack/v1/software_deployments.py index 27fda9c394..58d18a206c 100644 --- a/heat/api/openstack/v1/software_deployments.py +++ b/heat/api/openstack/v1/software_deployments.py @@ -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) diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 500088b72e..78cba7e309 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -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): diff --git a/heat/common/serializers.py b/heat/common/serializers.py new file mode 100644 index 0000000000..d94448ab79 --- /dev/null +++ b/heat/common/serializers.py @@ -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) diff --git a/heat/common/wsgi.py b/heat/common/wsgi.py index d521855d9f..0e027f36a1 100644 --- a/heat/common/wsgi.py +++ b/heat/common/wsgi.py @@ -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) diff --git a/heat/tests/test_common_serializers.py b/heat/tests/test_common_serializers.py new file mode 100644 index 0000000000..9c03b0c599 --- /dev/null +++ b/heat/tests/test_common_serializers.py @@ -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 = 'value' + 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 = '0001-03-08 02:00:00' + actual = serializers.XMLResponseSerializer().to_xml(fixture) + self.assertEqual(expected, actual) + + def test_to_xml_with_list(self): + fixture = {"name": ["1", "2"]} + expected = '12' + 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 = ('True' + 'test' + '') + 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 = ('True' + '{"name1": "test"}' + '{"name2": "test2"}') + 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('value', response.body) diff --git a/heat/tests/test_wsgi.py b/heat/tests/test_wsgi.py index f1c17f1c44..d68f23853c 100644 --- a/heat/tests/test_wsgi.py +++ b/heat/tests/test_wsgi.py @@ -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 = 'value' - 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 = '0001-03-08 02:00:00' - actual = wsgi.XMLResponseSerializer().to_xml(fixture) - self.assertEqual(expected, actual) - - def test_to_xml_with_list(self): - fixture = {"name": ["1", "2"]} - expected = '12' - 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 = ('True' - 'test' - '') - 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 = ('True' - '{"name1": "test"}' - '{"name2": "test2"}') - 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('value', response.body) - - class JSONRequestDeserializerTest(HeatTestCase): def test_has_body_no_content_length(self):