From ac1b51a2d19e7108d7c74a4022a6e33a50ffb826 Mon Sep 17 00:00:00 2001 From: Liang Chen Date: Sun, 23 Jun 2013 23:17:18 +0800 Subject: [PATCH] implement stack metadata make metadata and delete/update policy accessible to stack resource Blueprint stack-metadata Change-Id: I2251b730ce7fb70b8b28f065479b9b6a7e8a1f33 --- heat/engine/parser.py | 11 +- heat/engine/stack_resource.py | 6 +- heat/engine/template.py | 24 +++++ heat/tests/test_parser.py | 76 ++++++++++++++ heat/tests/test_stack_resource.py | 161 ++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 heat/tests/test_stack_resource.py diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 501d6590..6345c7ac 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -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]) diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index a79b55f9..9b35d025 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -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) diff --git a/heat/engine/template.py b/heat/engine/template.py index fb33cb89..87d4df8b 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -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): ''' diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 73fcf0f3..22832cb6 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -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 index 00000000..a174b568 --- /dev/null +++ b/heat/tests/test_stack_resource.py @@ -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() -- 2.45.2