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.

Changes required for backport:

* Changed import of oslo_serialization.jsonutils to
  heat.openstack.common.jsonutils.

Change-Id: I8eff36c4343a07b644b84aa9f2f74eceeb62b4a9
Closes-Bug: #1420196
(cherry-picked from 413fde3247)
This commit is contained in:
Steven Hardy 2015-05-18 12:02:35 +02:00 committed by Johannes Grassler
parent 0083fb09ca
commit 40bfa60d84
2 changed files with 32 additions and 2 deletions

View File

@ -20,6 +20,7 @@ from heat.engine import constraints as constr
from heat.engine import function
from heat.engine import parameters
from heat.engine import support
from heat.openstack.common import jsonutils
SCHEMA_KEYS = (
REQUIRED, IMPLEMENTED, DEFAULT, TYPE, SCHEMA,
@ -57,13 +58,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()
@ -145,6 +148,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),
@ -152,7 +162,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):
"""
@ -268,6 +279,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),

View File

@ -22,6 +22,7 @@ from heat.engine import parameters
from heat.engine import properties
from heat.engine import resources
from heat.engine import support
from heat.openstack.common import jsonutils
class PropertySchemaTest(testtools.TestCase):
@ -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})