Implement HOT intrinsic function get_file

This function takes a single string argument and uses that as the key
in a lookup into the template files. The resulting content is substituted
as a chunk of text.

The files keys can be arbitrary and the files section can be built manually
when generating the API request. However by convention heatclient will use
the absolute URL of the file as the key.

Change-Id: I83a73b14e1fbf56545151da1d8a0d7afe89149a4
This commit is contained in:
Steve Baker 2014-02-07 08:32:42 +13:00
parent ba0f1afd8b
commit 0cc40facca
5 changed files with 128 additions and 1 deletions

View File

@ -612,3 +612,45 @@ 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.
get_file
------------
The *get_file* function allows string content to be substituted into the
template. It is generally used as a file inclusion mechanism for files
containing non-heat scripts or configuration files.
The syntax of the get_file function is as follows:
::
get_file: <content key>
The *content key* will be used to look up the files dictionary that is
provided in the REST API call. The *heat* client command from
python-heatclient is *get_file* aware and will populate the *files* with
the actual content of fetched paths and URLs. The *heat* client command
supports relative paths and will transform these to absolute URLs which
will be used as the *content key* in the files dictionary.
The example below demonstrates *get_file* usage with both relative and
absolute URLs.
::
resources:
my_instance:
type: OS::Nova::Server
properties:
# general properties ...
user_data:
get_file: my_instance_user_data.sh
my_other_instance:
type: OS::Nova::Server
properties:
# general properties ...
user_data:
get_file: http://example.com/my_other_instance_user_data.sh
If this template was launched from a local file this would result in
a *files* dictionary containing entries with keys
*file:///path/to/my_instance_user_data.sh* and
*http://example.com/my_other_instance_user_data.sh*.

View File

@ -253,6 +253,32 @@ class HOTemplate(template.Template):
return template._resolve(match_str_replace,
handle_str_replace, s, transform)
def resolve_get_file(self, s, transform=None):
"""
Resolve file inclusion via function get_file. For any key provided
the contents of the value in the template files dictionary
will be substituted.
Resolves the get_file function of the form::
get_file:
<string key>
"""
def handle_get_file(args):
if not (isinstance(args, basestring)):
raise TypeError(
_('Argument to "get_file" must be a string'))
f = self.files.get(args)
if f is None:
raise ValueError(_('No content found in the "files" section '
'for get_file path: %s') % args)
return f
match_get_file = lambda k, v: k == 'get_file'
return template._resolve(match_get_file,
handle_get_file, s, transform)
def param_schemata(self):
params = self.t.get(self.PARAMETERS, {}).iteritems()
return dict((name, HOTParamSchema.from_dict(schema))

View File

@ -752,7 +752,8 @@ def resolve_static_data(template, stack, parameters, snippet):
functools.partial(template.resolve_resource_facade,
stack=stack),
template.resolve_find_in_map,
template.reduce_joins])
template.reduce_joins,
template.resolve_get_file])
def resolve_runtime_data(template, resources, snippet):

View File

@ -481,6 +481,13 @@ class Template(collections.Mapping):
handle_resource_facade,
s, transform)
@staticmethod
def resolve_get_file(s, transform=None):
# cfn templates do not have any analog to get_file so this function
# should remain not implemented. Attempts to use get_file in a cfn
# template will be passed through with no modification.
return s
def param_schemata(self):
params = self.t.get(self.PARAMETERS, {}).iteritems()
return dict((name, parameters.Schema.from_dict(schema))

View File

@ -205,6 +205,57 @@ class HOTemplateTest(HeatTestCase):
self.assertRaises(TypeError, tmpl.resolve_replace, snippet)
def test_get_file(self):
"""Test get_file function."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
snippet_resolved = 'foo contents'
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/foo.yaml': 'foo contents'
})
self.assertEqual(snippet_resolved, tmpl.resolve_get_file(snippet))
def test_get_file_not_string(self):
"""Test get_file function with non-string argument."""
snippet = {'get_file': ['file:///tmp/foo.yaml']}
tmpl = parser.Template(hot_tpl_empty)
notStrErr = self.assertRaises(
TypeError, tmpl.resolve_get_file, snippet)
self.assertEqual(
'Argument to "get_file" must be a string',
str(notStrErr))
def test_get_file_missing_files(self):
"""Test get_file function with no matching key in files section."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/bar.yaml': 'bar contents'
})
missingErr = self.assertRaises(
ValueError, tmpl.resolve_get_file, snippet)
self.assertEqual(
('No content found in the "files" section for '
'get_file path: file:///tmp/foo.yaml'),
str(missingErr))
def test_get_file_nested_does_not_resolve(self):
"""Test get_file function does not resolve nested calls."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
snippet_resolved = '{get_file: file:///tmp/bar.yaml}'
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/foo.yaml': snippet_resolved,
'file:///tmp/bar.yaml': 'bar content',
})
self.assertEqual(snippet_resolved, tmpl.resolve_get_file(snippet))
def test_prevent_parameters_access(self):
"""
Test that the parameters section can't be accesed using the template