]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
implement stack metadata
authorLiang Chen <cbjchen@cn.ibm.com>
Sun, 23 Jun 2013 15:17:18 +0000 (23:17 +0800)
committerLiang Chen <cbjchen@cn.ibm.com>
Tue, 2 Jul 2013 08:03:22 +0000 (16:03 +0800)
make metadata and delete/update policy accessible to stack resource

Blueprint stack-metadata

Change-Id: I2251b730ce7fb70b8b28f065479b9b6a7e8a1f33

heat/engine/parser.py
heat/engine/stack_resource.py
heat/engine/template.py
heat/tests/test_parser.py
heat/tests/test_stack_resource.py [new file with mode: 0644]

index 501d6590f7c1b3d928b91e0f9bf3b4175190d3f9..6345c7ac303f94d772b8ef1a8a6ee101300c4af7 100644 (file)
@@ -57,7 +57,7 @@ class Stack(object):
     def __init__(self, context, stack_name, tmpl, env=None,
                  stack_id=None, action=None, status=None,
                  status_reason='', timeout_mins=60, resolve_data=True,
-                 disable_rollback=True):
+                 disable_rollback=True, parent_resource=None):
         '''
         Initialise from a context, name, Template object and (optionally)
         Environment object. The database ID may also be initialised, if the
@@ -80,6 +80,7 @@ class Stack(object):
         self.status_reason = status_reason
         self.timeout_mins = timeout_mins
         self.disable_rollback = disable_rollback
+        self.parent_resource = parent_resource
 
         resources.initialise()
 
@@ -125,7 +126,8 @@ class Stack(object):
         return deps
 
     @classmethod
-    def load(cls, context, stack_id=None, stack=None, resolve_data=True):
+    def load(cls, context, stack_id=None, stack=None, resolve_data=True,
+             parent_resource=None):
         '''Retrieve a Stack from the database.'''
         if stack is None:
             stack = db_api.stack_get(context, stack_id)
@@ -137,7 +139,8 @@ class Stack(object):
         env = environment.Environment(stack.parameters)
         stack = cls(context, stack.name, template, env,
                     stack.id, stack.action, stack.status, stack.status_reason,
-                    stack.timeout, resolve_data, stack.disable_rollback)
+                    stack.timeout, resolve_data, stack.disable_rollback,
+                    parent_resource)
 
         return stack
 
@@ -542,6 +545,8 @@ def resolve_static_data(template, stack, parameters, snippet):
                                         parameters=parameters),
                       functools.partial(template.resolve_availability_zones,
                                         stack=stack),
+                      functools.partial(template.resolve_resource_facade,
+                                        stack=stack),
                       template.resolve_find_in_map,
                       template.reduce_joins])
 
index a79b55f96e96c5b0f1f71991989edf2151c8dd73..9b35d02568f530e66c39db8d9f1d4ebe245dfb9d 100644 (file)
@@ -51,7 +51,8 @@ class StackResource(resource.Resource):
         '''
         if self._nested is None and self.resource_id is not None:
             self._nested = parser.Stack.load(self.context,
-                                             self.resource_id)
+                                             self.resource_id,
+                                             parent_resource=self)
 
             if self._nested is None:
                 raise exception.NotFound('Nested stack not found in DB')
@@ -73,7 +74,8 @@ class StackResource(resource.Resource):
                                     template,
                                     environment.Environment(user_params),
                                     timeout_mins=timeout_mins,
-                                    disable_rollback=True)
+                                    disable_rollback=True,
+                                    parent_resource=self)
 
         nested_id = self._nested.store(self.stack)
         self.resource_id_set(nested_id)
index fb33cb898e99fda4f56ddb2d070d48e821cbe27d..87d4df8b40ed9e4de43c1abb089f39cac1d88487 100644 (file)
@@ -361,6 +361,30 @@ class Template(collections.Mapping):
 
         return _resolve(lambda k, v: k == 'Fn::Base64', handle_base64, s)
 
+    @staticmethod
+    def resolve_resource_facade(s, stack):
+        '''
+        Resolve constructs of the form {'Fn::ResourceFacade': 'Metadata'}
+        '''
+        resource_attributes = ('Metadata', 'DeletionPolicy', 'UpdatePolicy')
+
+        def handle_resource_facade(arg):
+            if arg not in resource_attributes:
+                raise ValueError(
+                    'Incorrect arguments to "Fn::ResourceFacade" %s: %s' %
+                    ('should be one of', str(resource_attributes)))
+            try:
+                if arg == 'Metadata':
+                    return stack.parent_resource.metadata
+                return stack.parent_resource.t[arg]
+            except KeyError:
+                raise KeyError('"%s" is not specified in parent resource' %
+                               arg)
+
+        return _resolve(lambda k, v: k == 'Fn::ResourceFacade',
+                        handle_resource_facade,
+                        s)
+
 
 def _resolve(match, handle, snippet):
     '''
index 73fcf0f39daa4afb9e72e148cd2c50fd1cbdea71..22832cb6a2db6638127dc47f64998fa515fdcce6 100644 (file)
@@ -18,6 +18,7 @@ import time
 import uuid
 
 from heat.common import context
+from heat.engine import environment
 from heat.common import exception
 from heat.common import template_format
 from heat.engine import clients
@@ -457,6 +458,54 @@ Mappings:
             parser.Template.resolve_replace(snippet),
             '"foo" is "${var3}"')
 
+    def test_resource_facade(self):
+        metadata_snippet = {'Fn::ResourceFacade': 'Metadata'}
+        deletion_policy_snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}
+        update_policy_snippet = {'Fn::ResourceFacade': 'UpdatePolicy'}
+
+        class DummyClass:
+            pass
+        parent_resource = DummyClass()
+        parent_resource.metadata = '{"foo": "bar"}'
+        parent_resource.t = {'DeletionPolicy': 'Retain',
+                             'UpdatePolicy': '{"foo": "bar"}'}
+        stack = parser.Stack(None, 'test_stack',
+                             parser.Template({}),
+                             parent_resource=parent_resource)
+        self.assertEqual(
+            parser.Template.resolve_resource_facade(metadata_snippet, stack),
+            '{"foo": "bar"}')
+        self.assertEqual(
+            parser.Template.resolve_resource_facade(deletion_policy_snippet,
+                                                    stack), 'Retain')
+        self.assertEqual(
+            parser.Template.resolve_resource_facade(update_policy_snippet,
+                                                    stack), '{"foo": "bar"}')
+
+    def test_resource_facade_invalid_arg(self):
+        snippet = {'Fn::ResourceFacade': 'wibble'}
+        stack = parser.Stack(None, 'test_stack', parser.Template({}))
+        self.assertRaises(ValueError,
+                          parser.Template.resolve_resource_facade,
+                          snippet,
+                          stack)
+
+    def test_resource_facade_missing_key(self):
+        snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}
+
+        class DummyClass:
+            pass
+        parent_resource = DummyClass()
+        parent_resource.metadata = '{"foo": "bar"}'
+        parent_resource.t = {}
+        stack = parser.Stack(None, 'test_stack',
+                             parser.Template({}),
+                             parent_resource=parent_resource)
+        self.assertRaises(KeyError,
+                          parser.Template.resolve_resource_facade,
+                          snippet,
+                          stack)
+
 
 class StackTest(HeatTestCase):
     def setUp(self):
@@ -517,6 +566,33 @@ class StackTest(HeatTestCase):
         self.assertRaises(exception.NotFound, parser.Stack.load,
                           None, -1)
 
+    @stack_delete_after
+    def test_load_parent_resource(self):
+        self.stack = parser.Stack(self.ctx, 'load_parent_resource',
+                                  parser.Template({}))
+        self.stack.store()
+        stack = db_api.stack_get(self.ctx, self.stack.id)
+
+        t = template.Template.load(self.ctx, stack.raw_template_id)
+        self.m.StubOutWithMock(template.Template, 'load')
+        template.Template.load(self.ctx, stack.raw_template_id).AndReturn(t)
+
+        env = environment.Environment(stack.parameters)
+        self.m.StubOutWithMock(environment, 'Environment')
+        environment.Environment(stack.parameters).AndReturn(env)
+
+        self.m.StubOutWithMock(parser.Stack, '__init__')
+        parser.Stack.__init__(self.ctx, stack.name, t, env, stack.id,
+                              stack.action, stack.status, stack.status_reason,
+                              stack.timeout, True, stack.disable_rollback,
+                              'parent')
+
+        self.m.ReplayAll()
+        parser.Stack.load(self.ctx, stack_id=self.stack.id,
+                          parent_resource='parent')
+
+        self.m.VerifyAll()
+
     # Note tests creating a stack should be decorated with @stack_delete_after
     # to ensure the self.stack is properly cleaned up
     @stack_delete_after
diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py
new file mode 100644 (file)
index 0000000..a174b56
--- /dev/null
@@ -0,0 +1,161 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+from heat.common import template_format
+from heat.common import context
+from heat.common import exception
+from heat.engine import parser
+from heat.engine import resource
+from heat.engine import stack_resource
+from heat.engine import template
+from heat.openstack.common import uuidutils
+from heat.tests.common import HeatTestCase
+from heat.tests import generic_resource as generic_rsrc
+from heat.tests.utils import setup_dummy_db
+from heat.tests.utils import stack_delete_after
+
+ws_res_snippet = {"Type": "some_magic_type",
+                  "metadata": {
+                      "key": "value",
+                      "some": "more stuff"}}
+
+wp_template = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "WordPress",
+  "Parameters" : {
+    "KeyName" : {
+      "Description" : "KeyName",
+      "Type" : "String",
+      "Default" : "test"
+    }
+  },
+  "Resources" : {
+    "WebServer": {
+      "Type": "AWS::EC2::Instance",
+      "metadata": {"Fn::ResourceFacade": "Metadata"},
+      "Properties": {
+        "ImageId" : "F17-x86_64-gold",
+        "InstanceType"   : "m1.large",
+        "KeyName"        : "test",
+        "UserData"       : "wordpress"
+      }
+    }
+  }
+}
+'''
+
+
+class MyStackResource(stack_resource.StackResource,
+                      generic_rsrc.GenericResource):
+    def physical_resource_name(self):
+        return "cb2f2b28-a663-4683-802c-4b40c916e1ff"
+
+
+class StackResourceTest(HeatTestCase):
+
+    def setUp(self):
+        super(StackResourceTest, self).setUp()
+        setup_dummy_db()
+        resource._register_class('some_magic_type',
+                                 MyStackResource)
+        t = parser.Template({template.RESOURCES:
+                             {"provider_resource": ws_res_snippet}})
+        self.parent_stack = parser.Stack(None, 'test_stack', t,
+                                         stack_id=uuidutils.generate_uuid())
+        self.parent_resource = MyStackResource('test',
+                                               ws_res_snippet,
+                                               self.parent_stack)
+        self.parent_resource.context = context.get_admin_context()
+        self.templ = template_format.parse(wp_template)
+
+    @stack_delete_after
+    def test_create_with_template_ok(self):
+        self.parent_resource.create_with_template(self.templ,
+                                                  {"KeyName": "key"})
+        self.stack = self.parent_resource.nested()
+
+        self.assertEqual(self.parent_resource, self.stack.parent_resource)
+        self.assertEqual("cb2f2b28-a663-4683-802c-4b40c916e1ff",
+                         self.stack.name)
+        self.assertEqual(self.templ, self.stack.t.t)
+        self.assertEqual(self.stack.id, self.parent_resource.resource_id)
+
+    @stack_delete_after
+    def test_load_nested_ok(self):
+        self.parent_resource.create_with_template(self.templ,
+                                                  {"KeyName": "key"})
+        self.stack = self.parent_resource.nested()
+
+        self.parent_resource._nested = None
+        self.m.StubOutWithMock(parser.Stack, 'load')
+        parser.Stack.load(self.parent_resource.context,
+                          self.parent_resource.resource_id,
+                          parent_resource=self.parent_resource).AndReturn('s')
+        self.m.ReplayAll()
+
+        self.parent_resource.nested()
+        self.m.VerifyAll()
+
+    @stack_delete_after
+    def test_load_nested_non_exist(self):
+        self.parent_resource.create_with_template(self.templ,
+                                                  {"KeyName": "key"})
+        self.stack = self.parent_resource.nested()
+
+        self.parent_resource._nested = None
+        self.m.StubOutWithMock(parser.Stack, 'load')
+        parser.Stack.load(self.parent_resource.context,
+                          self.parent_resource.resource_id,
+                          parent_resource=self.parent_resource)
+        self.m.ReplayAll()
+
+        self.assertRaises(exception.NotFound, self.parent_resource.nested)
+        self.m.VerifyAll()
+
+    def test_delete_nested_ok(self):
+        nested = self.m.CreateMockAnything()
+        self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
+        stack_resource.StackResource.nested().AndReturn(nested)
+        nested.delete()
+        self.m.ReplayAll()
+
+        self.parent_resource.delete_nested()
+        self.m.VerifyAll()
+
+    def test_get_output_ok(self):
+        nested = self.m.CreateMockAnything()
+        self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
+        stack_resource.StackResource.nested().AndReturn(nested)
+        nested.outputs = {"key": "value"}
+        nested.output('key').AndReturn("value")
+        self.m.ReplayAll()
+
+        self.assertEqual("value", self.parent_resource.get_output("key"))
+
+        self.m.VerifyAll()
+
+    def test_get_output_key_not_found(self):
+        nested = self.m.CreateMockAnything()
+        self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
+        stack_resource.StackResource.nested().AndReturn(nested)
+        nested.outputs = {}
+        self.m.ReplayAll()
+
+        self.assertRaises(exception.InvalidTemplateAttribute,
+                          self.parent_resource.get_output,
+                          "key")
+
+        self.m.VerifyAll()