diff --git a/heat/engine/properties.py b/heat/engine/properties.py index 1147005ef9..7d77a3519d 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -18,8 +18,10 @@ import six from heat.common import exception from heat.common.i18n import _ +from heat.engine.cfn import functions as cfn_funcs from heat.engine import constraints as constr from heat.engine import function +from heat.engine.hot import functions as hot_funcs from heat.engine.hot import parameters as hot_param from heat.engine import parameters from heat.engine import support @@ -659,17 +661,20 @@ class TranslationRule(object): raise ValueError(_('value must be list type when rule is Add.')) def execute_rule(self): - (source_key, source_data) = self.get_data_from_source_path( - self.source_path) - if self.value_path: - (value_key, value_data) = self.get_data_from_source_path( - self.value_path) - value = (value_data[value_key] - if value_data and value_data.get(value_key) - else self.value) - else: - (value_key, value_data) = None, None - value = self.value + try: + (source_key, source_data) = self.get_data_from_source_path( + self.source_path) + if self.value_path: + (value_key, value_data) = self.get_data_from_source_path( + self.value_path) + value = (value_data[value_key] + if value_data and value_data.get(value_key) + else self.value) + else: + (value_key, value_data) = None, None + value = self.value + except AttributeError: + return if (source_data is None or (self.rule != self.DELETE and (value is None and @@ -728,16 +733,37 @@ class TranslationRule(object): for k, s in schemata.items()) return props + def resolve_param(param): + """Check whether if given item is param and resolve, if it is.""" + # NOTE(prazumovsky): If property uses removed in HOT function, + # we should not translate it for correct validating and raising + # validation error. + if isinstance(param, hot_funcs.Removed): + raise AttributeError(_('Property uses removed function.')) + if isinstance(param, (hot_funcs.GetParam, cfn_funcs.ParamRef)): + return function.resolve(param) + elif isinstance(param, list): + return [resolve_param(param_item) for param_item in param] + else: + return param + source_key = path[0] data = self.properties.data props = self.properties.props for key in path: if isinstance(data, list): source_key = key - elif data.get(key) is not None and isinstance(data.get(key), - (list, dict)): - data = data.get(key) - props = get_props(props, key) + elif data.get(key) is not None: + # NOTE(prazumovsky): There's no need to resolve other functions + # because we can translate all function to another path. But if + # list or map type property equals to get_param function, need + # to resolve it for correct translating. + data[key] = resolve_param(data[key]) + if isinstance(data[key], (dict, list)): + data = data[key] + props = get_props(props, key) + else: + source_key = key elif data.get(key) is None: if (self.rule == TranslationRule.DELETE or (self.rule == TranslationRule.REPLACE and @@ -752,6 +778,4 @@ class TranslationRule(object): continue data = data.get(key) props = get_props(props, key) - else: - source_key = key return source_key, data diff --git a/heat/tests/test_properties.py b/heat/tests/test_properties.py index 898161b526..effff21fc3 100644 --- a/heat/tests/test_properties.py +++ b/heat/tests/test_properties.py @@ -18,6 +18,7 @@ import six from heat.common import exception from heat.engine.cfn import functions as cfn_funcs from heat.engine import constraints +from heat.engine.hot import functions as hot_funcs from heat.engine.hot import parameters as hot_param from heat.engine import parameters from heat.engine import plugin_manager @@ -2289,3 +2290,168 @@ class TestTranslationRule(common.HeatTestCase): rule.execute_rule() self.assertIsNone(props.get('far')) + + def test_property_json_param_correct_translation(self): + """Test case when property with sub-schema takes json param.""" + schema = { + 'far': properties.Schema(properties.Schema.MAP, + schema={ + 'bar': properties.Schema( + properties.Schema.STRING, + ), + 'dar': properties.Schema( + properties.Schema.STRING + ) + }) + } + + class DummyStack(dict): + @property + def parameters(self): + return mock.Mock() + + param = hot_funcs.GetParam(DummyStack(json_far='json_far'), + 'get_param', + 'json_far') + param.parameters = { + 'json_far': parameters.JsonParam( + 'json_far', + {'Type': 'Json'}, + '{"dar": "rad"}').value()} + data = {'far': param} + + props = properties.Properties(schema, data) + + rule = properties.TranslationRule(props, + properties.TranslationRule.REPLACE, + ['far', 'bar'], + value_path=['far', 'dar']) + + rule.execute_rule() + + self.assertEqual('rad', props.get('far').get('bar')) + + def test_property_json_param_to_list_correct_translation(self): + """Test case when list property with sub-schema takes json param.""" + schema = { + 'far': properties.Schema(properties.Schema.LIST, + schema=properties.Schema( + properties.Schema.MAP, + schema={ + 'bar': properties.Schema( + properties.Schema.STRING, + ), + 'dar': properties.Schema( + properties.Schema.STRING + ) + } + )) + } + + class DummyStack(dict): + @property + def parameters(self): + return mock.Mock() + + param = hot_funcs.GetParam(DummyStack(json_far='json_far'), + 'get_param', + 'json_far') + param.parameters = { + 'json_far': parameters.JsonParam( + 'json_far', + {'Type': 'Json'}, + '{"dar": "rad"}').value()} + data = {'far': [param]} + + props = properties.Properties(schema, data) + + rule = properties.TranslationRule(props, + properties.TranslationRule.REPLACE, + ['far', 'bar'], + value_name='dar') + + rule.execute_rule() + + self.assertEqual([{'dar': None, 'bar': 'rad'}], props.get('far')) + + def test_property_commadelimitedlist_param_correct_translation(self): + """Test when property with sub-schema takes comma_delimited_list.""" + schema = { + 'far': properties.Schema( + properties.Schema.LIST, + schema=properties.Schema( + properties.Schema.STRING, + ) + ), + 'boo': properties.Schema( + properties.Schema.STRING + )} + + class DummyStack(dict): + @property + def parameters(self): + return mock.Mock() + + param = hot_funcs.GetParam(DummyStack(list_far='list_far'), + 'get_param', + 'list_far') + param.parameters = { + 'list_far': parameters.CommaDelimitedListParam( + 'list_far', + {'Type': 'CommaDelimitedList'}, + "white,roses").value()} + data = {'far': param, 'boo': 'chrysanthemums'} + + props = properties.Properties(schema, data) + + rule = properties.TranslationRule(props, + properties.TranslationRule.ADD, + ['far'], + [props.get('boo')]) + rule.execute_rule() + + self.assertEqual(['white', 'roses', 'chrysanthemums'], + props.get('far')) + + def test_property_no_translation_removed_function(self): + """Test case when list property with sub-schema takes json param.""" + schema = { + 'far': properties.Schema(properties.Schema.LIST, + schema=properties.Schema( + properties.Schema.MAP, + schema={ + 'bar': properties.Schema( + properties.Schema.STRING, + ), + 'dar': properties.Schema( + properties.Schema.STRING + ) + } + )) + } + + class DummyStack(dict): + @property + def parameters(self): + return mock.Mock() + + param = hot_funcs.Removed(DummyStack(json_far='json_far'), + 'Ref', + 'json_far') + param.parameters = { + 'json_far': parameters.JsonParam( + 'json_far', + {'Type': 'Json'}, + '{"dar": "rad"}').value()} + data = {'far': [param]} + + props = properties.Properties(schema, data) + + rule = properties.TranslationRule(props, + properties.TranslationRule.REPLACE, + ['far', 'bar'], + value_name='dar') + + rule.execute_rule() + + self.assertEqual([param], props.data.get('far'))