From 413fde3247b804e408a9ea9fd863d85391e326aa Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Wed, 18 Feb 2015 16:53:43 +0000 Subject: [PATCH] Allow lists and strings for Json parameters via provider resources Currently we have a somewhat bogus internal equivalence between the "json" parameter type and the MAP property type. This is a problem when exposing json parameters via provider resources, because the schema of the exposed property does not exactly match the capabilities of the underlying parameter. Specifically, json parameters can take strings (json serialized list or map), and also lists and maps directly. Currently, we can only pass maps, which means some data, for example a list of maps (which is possible when interacting directly with a json parameter) is not possible when using the same parameter abstracted via a TemplateResource. To work around this add a flag which relaxes the type rules and enables some coercion of map values, only when they're going via a schema derived from a json parameter. Change-Id: I8eff36c4343a07b644b84aa9f2f74eceeb62b4a9 Closes-Bug: #1420196 --- heat/engine/properties.py | 23 +++++++++++++++++++++-- heat/tests/test_properties.py | 11 +++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/heat/engine/properties.py b/heat/engine/properties.py index c342d3b94..e72bae9af 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -13,6 +13,7 @@ import collections +from oslo_serialization import jsonutils import six from heat.common import exception @@ -58,13 +59,15 @@ class Schema(constr.Schema): implemented=True, update_allowed=False, immutable=False, - support_status=support.SupportStatus()): + support_status=support.SupportStatus(), + allow_conversion=False): super(Schema, self).__init__(data_type, description, default, schema, required, constraints) self.implemented = implemented self.update_allowed = update_allowed self.immutable = immutable self.support_status = support_status + self.allow_conversion = allow_conversion # validate structural correctness of schema itself self.validate() @@ -146,6 +149,13 @@ class Schema(constr.Schema): param.BOOLEAN: cls.BOOLEAN } + # allow_conversion allows slightly more flexible type conversion + # where property->parameter types don't align, primarily when + # a json parameter value is passed via a Map property, which requires + # some coercion to pass strings or lists (which are both valid for + # Json parameters but not for Map properties). + allow_conversion = param.type == param.MAP + # make update_allowed true by default on TemplateResources # as the template should deal with this. return cls(data_type=param_type_map.get(param.type, cls.MAP), @@ -153,7 +163,8 @@ class Schema(constr.Schema): required=param.required, constraints=param.constraints, update_allowed=True, - immutable=False) + immutable=False, + allow_conversion=allow_conversion) def allowed_param_prop_type(self): """ @@ -269,6 +280,14 @@ class Property(object): if value is None: value = self.has_default() and self.default() or {} if not isinstance(value, collections.Mapping): + # This is to handle passing Lists via Json parameters exposed + # via a provider resource, in particular lists-of-dicts which + # cannot be handled correctly via comma_delimited_list + if self.schema.allow_conversion: + if isinstance(value, six.string_types): + return value + elif isinstance(value, collections.Sequence): + return jsonutils.dumps(value) raise TypeError(_('"%s" is not a map') % value) return dict(self._get_children(six.iteritems(value), diff --git a/heat/tests/test_properties.py b/heat/tests/test_properties.py index 8bd385772..4d816086b 100644 --- a/heat/tests/test_properties.py +++ b/heat/tests/test_properties.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_serialization import jsonutils import six import testtools @@ -468,6 +469,7 @@ class PropertySchemaTest(testtools.TestCase): self.assertTrue(schema.required) self.assertIsNone(schema.default) self.assertEqual(0, len(schema.constraints)) + self.assertFalse(schema.allow_conversion) def test_from_number_param_min(self): default = "42" @@ -546,6 +548,7 @@ class PropertySchemaTest(testtools.TestCase): self.assertIsNone(schema.default) self.assertFalse(schema.required) self.assertEqual(1, len(schema.constraints)) + self.assertFalse(schema.allow_conversion) allowed_constraint = schema.constraints[0] @@ -563,6 +566,7 @@ class PropertySchemaTest(testtools.TestCase): self.assertEqual(properties.Schema.LIST, schema.type) self.assertIsNone(schema.default) self.assertFalse(schema.required) + self.assertFalse(schema.allow_conversion) def test_from_json_param(self): param = parameters.Schema.from_dict('name', { @@ -575,6 +579,7 @@ class PropertySchemaTest(testtools.TestCase): self.assertEqual(properties.Schema.MAP, schema.type) self.assertIsNone(schema.default) self.assertFalse(schema.required) + self.assertTrue(schema.allow_conversion) class PropertyTest(testtools.TestCase): @@ -862,6 +867,12 @@ class PropertyTest(testtools.TestCase): p = properties.Property({'Type': 'Map'}) self.assertRaises(TypeError, p.get_value, ['foo']) + def test_map_allow_conversion(self): + p = properties.Property({'Type': 'Map'}) + p.schema.allow_conversion = True + self.assertEqual('foo', p.get_value('foo')) + self.assertEqual(jsonutils.dumps(['foo']), p.get_value(['foo'])) + def test_map_schema_good(self): map_schema = {'valid': {'Type': 'Boolean'}} p = properties.Property({'Type': 'Map', 'Schema': map_schema})