From 66f4178d280f8418ae2358fcf1539811b2c2014c Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Wed, 9 Sep 2015 14:34:28 +0100 Subject: [PATCH] Allow map/list items for str_replace Currently any attempt to substitute placeholders using data from a json/map or list parameter/attribute results in an error, which is inconvenient if you wish to provide such data to an instance, e.g so it can be used in a script via jq/python or whatever. So, tolerate non-string parameter values, and attempt to serialize them as json, then substitute the resulting json strings. Change-Id: I362cc76ac3f68d4649a62459455c0dcae2dcd25d Closes-Bug: #1489028 --- doc/source/template_guide/hot_spec.rst | 6 ++- heat/engine/hot/functions.py | 51 ++++++++++++++++++++++++++ heat/engine/hot/template.py | 2 +- heat/tests/test_hot.py | 20 ++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index d2bbf647b..12ad2561a 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -1099,7 +1099,11 @@ template params Provides parameter mappings in the form of dictionary. Each key refers to a - placeholder used in the ``template`` attribute. + placeholder used in the ``template`` attribute. From HOT version + ``2015-10-15`` you may optionally pass non-string parameter values + (e.g json/map/list parameters or attributes) and they will be serialized + as json before replacing, prior heat/HOT versions require string values. + The following example shows a simple use of the ``str_replace`` function in the outputs section of a template to build a URL for logging into a deployed diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 06c2adb4e..f6da415e2 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -262,6 +262,57 @@ class Replace(cfn_funcs.Replace): return mapping, string +class ReplaceJson(Replace): + ''' + A function for performing string substitutions. + + Behaves the same as Replace, but tolerates non-string parameter + values, e.g map/list - these are serialized as json before doing + the string substitution. + ''' + + def result(self): + template = function.resolve(self._string) + mapping = function.resolve(self._mapping) + + if not isinstance(template, six.string_types): + raise TypeError(_('"%s" template must be a string') % self.fn_name) + + if not isinstance(mapping, collections.Mapping): + raise TypeError(_('"%s" params must be a map') % self.fn_name) + + def replace(string, change): + placeholder, value = change + + if not isinstance(placeholder, six.string_types): + raise TypeError(_('"%s" param placeholders must be strings') % + self.fn_name) + + if value is None: + value = '' + + if not isinstance(value, + (six.string_types, six.integer_types, + float, bool)): + if isinstance(value, + (collections.Mapping, collections.Sequence)): + try: + value = jsonutils.dumps(value, default=None) + except TypeError: + raise TypeError(_('"%(name)s" params must be strings, ' + 'numbers, list or map. ' + 'Failed to json serialize %(value)s' + ) % {'name': self.fn_name, + 'value': value}) + else: + raise TypeError(_('"%s" params must be strings, numbers, ' + 'list or map. ') % self.fn_name) + + return string.replace(placeholder, six.text_type(value)) + + return six.moves.reduce(replace, six.iteritems(mapping), template) + + class GetFile(function.Function): """ A function for including a file inline. diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 6372f64a9..c2b66ebad 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -335,7 +335,7 @@ class HOTemplate20151015(HOTemplate20150430): 'list_join': hot_funcs.JoinMultiple, 'repeat': hot_funcs.Repeat, 'resource_facade': hot_funcs.ResourceFacade, - 'str_replace': hot_funcs.Replace, + 'str_replace': hot_funcs.ReplaceJson, # functions added since 20150430 'str_split': hot_funcs.StrSplit, diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index ac2cea4a8..4b4094709 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -490,6 +490,26 @@ class HOTemplateTest(common.HeatTestCase): self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + def test_str_replace_map_param(self): + """Test str_replace function with non-string params.""" + + snippet = {'str_replace': {'template': 'jsonvar1', + 'params': {'jsonvar1': {'foo': 123}}}} + snippet_resolved = '{"foo": 123}' + + tmpl = template.Template(hot_liberty_tpl_empty) + self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + + def test_str_replace_list_param(self): + """Test str_replace function with non-string params.""" + + snippet = {'str_replace': {'template': 'listvar1', + 'params': {'listvar1': ['foo', 123]}}} + snippet_resolved = '["foo", 123]' + + tmpl = template.Template(hot_liberty_tpl_empty) + self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + def test_str_replace_number(self): """Test str_replace function with numbers."""