diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 7a91a34dd9..d326e50537 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -171,6 +171,7 @@ For example, Heat currently supports the following values for the digest resource_facade str_replace + str_split @@ -1046,3 +1047,43 @@ In the example above, one can imagine that MySQL is being configured on a compute instance and the root password is going to be set based on a user provided parameter. The script for doing this is provided as userdata to the compute instance, leveraging the ``str_replace`` function. + +str_split +--------- +The *str_split* function allows for splitting a string into a list by providing +an arbitrary delimiter, the opposite of list_join. + +The syntax of the str_split function is as follows: + +:: + + str_split: + - ',' + - string,to,split +Or: + +:: + + str_split: [',', 'string,to,split'] + +The result of which is: + +:: + + ['string', 'to', 'split'] + +Optionally, an index may be provided to select a specific entry from the +resulting list, similar to get_attr/get_param: + +:: + + str_split: [',', 'string,to,split', 0] + +The result of which is: + +:: + + 'string' + +Note: The index starts at zero, and any value outside the maximum (e.g the +length of the list minus one) will cause an error. diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 29a1529fe0..0d5f8f3c38 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -424,3 +424,66 @@ class Digest(function.Function): self.validate_usage(args) return self.digest(*args) + + +class StrSplit(function.Function): + ''' + A function for splitting delimited strings into a list + and optionally extracting a specific list member by index. + + Takes the form:: + + str_split: [delimiter, string, ] + + or:: + + str_split: + - delimiter + - string + - + If is specified, the specified list item will be returned + otherwise, the whole list is returned, similar to get_attr with + path based attributes accessing lists. + ''' + + def __init__(self, stack, fn_name, args): + super(StrSplit, self).__init__(stack, fn_name, args) + example = '"%s" : [ ",", "apples,pears", ]' % fn_name + self.fmt_data = {'fn_name': fn_name, + 'example': example} + self.fn_name = fn_name + + if isinstance(args, (six.string_types, collections.Mapping)): + raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % self.fmt_data) + + def result(self): + args = function.resolve(self.args) + + try: + delim = args.pop(0) + str_to_split = args.pop(0) + except (AttributeError, IndexError): + raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % self.fmt_data) + split_list = str_to_split.split(delim) + + # Optionally allow an index to be specified + if args: + try: + index = int(args.pop(0)) + except ValueError: + raise ValueError(_('Incorrect index to "%(fn_name)s" ' + 'should be: %(example)s') % self.fmt_data) + else: + try: + res = split_list[index] + except IndexError: + raise ValueError(_('Incorrect index to "%(fn_name)s" ' + 'should be between 0 and ' + '%(max_index)s') + % {'fn_name': self.fn_name, + 'max_index': len(split_list) - 1}) + else: + res = split_list + return res diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index e329e836b8..4666e82b32 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -339,6 +339,9 @@ class HOTemplate20151015(HOTemplate20150430): 'resource_facade': hot_funcs.ResourceFacade, 'str_replace': hot_funcs.Replace, + # functions added since 20150430 + 'str_split': hot_funcs.StrSplit, + # functions removed from 20150430 'Fn::Select': hot_funcs.Removed, diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index fcf3dbeb2d..b0349f4a5e 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -746,6 +746,51 @@ class HOTemplateTest(common.HeatTestCase): exc = self.assertRaises(ValueError, self.resolve, snippet, tmpl) self.assertIn('Algorithm must be one of', six.text_type(exc)) + def test_str_split(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',', 'bar,baz']} + snippet_resolved = ['bar', 'baz'] + self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + + def test_str_split_index(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',', 'bar,baz', 1]} + snippet_resolved = 'baz' + self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + + def test_str_split_index_str(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',', 'bar,baz', '1']} + snippet_resolved = 'baz' + self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + + def test_str_split_index_bad(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',', 'bar,baz', 'bad']} + exc = self.assertRaises(ValueError, self.resolve, snippet, tmpl) + self.assertIn('Incorrect index to \"str_split\"', six.text_type(exc)) + + def test_str_split_index_out_of_range(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',', 'bar,baz', '2']} + exc = self.assertRaises(ValueError, self.resolve, snippet, tmpl) + expected = 'Incorrect index to \"str_split\" should be between 0 and 1' + self.assertEqual(expected, six.text_type(exc)) + + def test_str_split_bad_novalue(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': [',']} + exc = self.assertRaises(ValueError, self.resolve, snippet, tmpl) + self.assertIn('Incorrect arguments to \"str_split\"', + six.text_type(exc)) + + def test_str_split_bad_empty(self): + tmpl = template.Template(hot_liberty_tpl_empty) + snippet = {'str_split': []} + exc = self.assertRaises(ValueError, self.resolve, snippet, tmpl) + self.assertIn('Incorrect arguments to \"str_split\"', + six.text_type(exc)) + def test_prevent_parameters_access(self): """ Test that the parameters section can't be accessed using the template