Merge "implement stack metadata"
This commit is contained in:
commit
f50cbe4ac2
@ -59,7 +59,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
|
||||
@ -82,6 +82,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()
|
||||
|
||||
@ -127,7 +128,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)
|
||||
@ -139,7 +141,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
|
||||
|
||||
@ -544,6 +547,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])
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
'''
|
||||
|
@ -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
|
||||
|
161
heat/tests/test_stack_resource.py
Normal file
161
heat/tests/test_stack_resource.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user