]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Adding HOT str_replace and get_resource functions
authorThomas Spatzier <thomas.spatzier@de.ibm.com>
Mon, 12 Aug 2013 16:10:48 +0000 (18:10 +0200)
committerThomas Spatzier <thomas.spatzier@de.ibm.com>
Tue, 13 Aug 2013 06:41:10 +0000 (08:41 +0200)
This patch adds support for two functions, get_resource and str_replace to HOT
to enable the definition of new HOT samples. Especially str_replace aims to
simplify the definition of scripts in HOT templates.

Change-Id: Ic14fee36bdc0b9c5d79ade7ece5fa1b5331c864a

doc/source/template_guide/hot_spec.rst
heat/engine/hot.py
heat/tests/test_hot.py

index 31219c36e74598a1199ac3b32c0d6a1c844c8f58..aa6bea20c9c7679771b8fb4f8e201284ed5a1049 100644 (file)
@@ -442,3 +442,95 @@ An example of using the get_attr function is shown below:
 
   outputs:
     instance_ip: { get_attr: [my_instance, PublicIp] }
+
+
+get_resource
+------------
+The *get_resource* function allows for referencing another resource within the
+same template. At runtime, it will be resolved to reference ID of the resource,
+which is resource type specific. For example, a reference to a floating IP
+resource will return the respective IP address at runtime.
+The syntax of the get_resource function is as follows:
+
+::
+
+  get_resource: <resource ID>
+
+The *resource ID* of the referenced resources as used in the current template is
+given as single parameter to the get_resource function.
+
+
+str_replace
+-----------
+The *str_replace* function allows for dynamically constructing strings by
+providing a template string with placeholders and a list of mappings to assign
+values to those placeholders at runtime. The functionality of this function is
+similar to that of Python Template strings.
+The syntax of the str_replace function is as follows:
+
+::
+
+  str_replace:
+    template: <template string>
+    params: <parameter mappings>
+
+template
+    The *template* argument defines the template string that contains
+    placeholders which will be substituted at runtime.
+params
+    The *params* argument provides parameter mappings in the form of a
+    dictionary, which will be used for placeholder substitution in the template
+    string at runtime. Within parameter mappings one can make use of other
+    functions (e.g. get_attr to use resource attribute values) for template
+    substitution.
+
+The example below 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 application.
+
+::
+
+  resources:
+    my_instance:
+      type: OS::Nova::Compute
+      # general metadata and properties ...
+
+  outputs:
+    Login_URL:
+      description: The URL to log into the deployed application
+      value:
+        str_replace:
+          template: http://$host/MyApplication
+          params:
+            host: { get_attr: [ my_instance, PublicIp ] }
+
+The str_replace function can also be used for constructing bigger chunks of text
+like scripts for initializing compute instances as shown in the example below:
+
+::
+
+  parameters:
+    DBRootPassword:
+      type: string
+      description: Root password for MySQL
+      hidden: true
+
+  resources:
+    my_instance:
+      type: OS::Nova::Compute
+      properties:
+        # general properties ...
+        userdata:
+          str_replace:
+            template: |
+              #!/bin/bash
+              echo "Hello world"
+              echo "Setting MySQL root password"
+              mysqladmin -u root password $db_rootpassword
+              # do more things ...
+            params:
+              db_rootpassword: { get_param: DBRootPassword }
+
+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.
index caed57ec476042757ab959fd4241a695f8bd20fe..0dccb232eebf545bf03a34ac059e49715c2d9c06 100644 (file)
@@ -12,6 +12,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from string import Template
+
 from heat.common import exception
 from heat.engine import template
 from heat.engine.parameters import ParamSchema
@@ -190,6 +192,19 @@ class HOTemplate(template.Template):
 
         return template._resolve(match_param_ref, handle_param_ref, s)
 
+    @staticmethod
+    def resolve_resource_refs(s, resources):
+        '''
+        Resolve constructs of the form { "get_resource" : "resource" }
+        '''
+        def match_resource_ref(key, value):
+            return key == 'get_resource' and value in resources
+
+        def handle_resource_ref(arg):
+            return resources[arg].FnGetRefId()
+
+        return template._resolve(match_resource_ref, handle_resource_ref, s)
+
     @staticmethod
     def resolve_attributes(s, resources):
         """
@@ -218,6 +233,46 @@ class HOTemplate(template.Template):
 
         return template._resolve(match_get_attr, handle_get_attr, s)
 
+    @staticmethod
+    def resolve_replace(s):
+        """
+        Resolve template string substitution via function str_replace
+
+        Resolves the str_replace function of the form
+
+          str_replace:
+            template: <string template>
+            params:
+              <param dictionary>
+        """
+        def handle_str_replace(args):
+            if not isinstance(args, dict):
+                raise TypeError('Arguments to "str_replace" must be a'
+                                'dictionary')
+
+            try:
+                template = args['template']
+                params = args['params']
+            except KeyError:
+                example = ('''str_replace:
+                  template: This is $var1 template $var2
+                  params:
+                    - var1: a
+                    - var2: string''')
+                raise KeyError('"str_replace" syntax should be %s' %
+                               example)
+
+            if not isinstance(template, basestring):
+                raise TypeError('"template" parameter must be a string')
+            if not isinstance(params, dict):
+                raise TypeError(
+                    '"params" parameter must be a dictionary')
+
+            return Template(template).substitute(params)
+
+        return template._resolve(lambda k, v: k == 'str_replace',
+                                 handle_str_replace, s)
+
     def param_schemata(self):
         params = self[PARAMETERS].iteritems()
         return dict((name, HOTParamSchema(schema)) for name, schema in params)
index 26319afc7f1dd261de829087f8dd9b7f06eb14a9..21f970b1f2d86a6a99aca5810cc135c6ffffa98b 100644 (file)
@@ -259,6 +259,72 @@ class HOTemplateTest(HeatTestCase):
         self.assertEqual(tmpl.resolve_param_refs(snippet, params),
                          snippet_resolved)
 
+    def test_str_replace(self):
+        """Test str_replace function."""
+
+        snippet = {'str_replace': {'template': 'Template $var1 string $var2',
+                                   'params': {'var1': 'foo', 'var2': 'bar'}}}
+        snippet_resolved = 'Template foo string bar'
+
+        tmpl = parser.Template(hot_tpl_empty)
+
+        self.assertEqual(tmpl.resolve_replace(snippet), snippet_resolved)
+
+    def test_str_replace_syntax(self):
+        """
+        Test str_replace function syntax.
+
+        Pass wrong syntax (array instead of dictionary) to function and
+        validate that we get a TypeError.
+        """
+
+        snippet = {'str_replace': [{'template': 'Template $var1 string $var2'},
+                                   {'params': {'var1': 'foo', 'var2': 'bar'}}]}
+
+        tmpl = parser.Template(hot_tpl_empty)
+
+        self.assertRaises(TypeError, tmpl.resolve_replace, snippet)
+
+    def test_str_replace_invalid_param_keys(self):
+        """
+        Test str_replace function parameter keys.
+
+        Pass wrong parameters to function and verify that we get
+        a KeyError.
+        """
+
+        snippet = {'str_replace': {'tmpl': 'Template $var1 string $var2',
+                                   'params': {'var1': 'foo', 'var2': 'bar'}}}
+
+        tmpl = parser.Template(hot_tpl_empty)
+
+        self.assertRaises(KeyError, tmpl.resolve_replace, snippet)
+
+        snippet = {'str_replace': {'tmpl': 'Template $var1 string $var2',
+                                   'parms': {'var1': 'foo', 'var2': 'bar'}}}
+
+        self.assertRaises(KeyError, tmpl.resolve_replace, snippet)
+
+    def test_str_replace_invalid_param_types(self):
+        """
+        Test str_replace function parameter values.
+
+        Pass parameter values of wrong type to function and verify that we get
+        a TypeError.
+        """
+
+        snippet = {'str_replace': {'template': 12345,
+                                   'params': {'var1': 'foo', 'var2': 'bar'}}}
+
+        tmpl = parser.Template(hot_tpl_empty)
+
+        self.assertRaises(TypeError, tmpl.resolve_replace, snippet)
+
+        snippet = {'str_replace': {'template': 'Template $var1 string $var2',
+                                   'params': ['var1', 'foo', 'var2', 'bar']}}
+
+        self.assertRaises(TypeError, tmpl.resolve_replace, snippet)
+
 
 class StackTest(test_parser.StackTest):
     """Test stack function when stack was created from HOT template."""
@@ -292,6 +358,28 @@ class StackTest(test_parser.StackTest):
                           {'Value': {'get_attr': ['resource1', 'NotThere']}},
                           self.stack)
 
+    @utils.stack_delete_after
+    def test_get_resource(self):
+        """Test resolution of get_resource occurrences in HOT template."""
+
+        hot_tpl = template_format.parse('''
+        heat_template_version: 2013-05-23
+        resources:
+          resource1:
+            type: GenericResourceType
+        ''')
+
+        self.stack = parser.Stack(self.ctx, 'test_get_resource',
+                                  template.Template(hot_tpl))
+        self.stack.store()
+        self.stack.create()
+        self.assertEqual(self.stack.state,
+                         (parser.Stack.CREATE, parser.Stack.COMPLETE))
+
+        snippet = {'value': {'get_resource': 'resource1'}}
+        resolved = hot.HOTemplate.resolve_resource_refs(snippet, self.stack)
+        self.assertEqual(resolved, {'value': 'resource1'})
+
 
 class HOTParamValidatorTest(HeatTestCase):
     "Test HOTParamValidator"