From 6044ef1b78678916611764ba023f3514ab447d14 Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Sun, 17 Jun 2012 16:05:58 +0200 Subject: [PATCH] Implement Nested Stacks Fixes #123. Change-Id: I89affe471b4df898c7d3157ff23f9b64003c2893 Signed-off-by: Zane Bitter --- heat/db/sqlalchemy/api.py | 9 +- .../versions/006_nested_stacks.py | 23 ++++ heat/db/sqlalchemy/models.py | 7 +- heat/engine/checkeddict.py | 5 + heat/engine/parser.py | 16 +-- heat/engine/resource_types.py | 2 + heat/engine/stack.py | 117 ++++++++++++++++++ 7 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 heat/db/sqlalchemy/migrate_repo/versions/006_nested_stacks.py create mode 100644 heat/engine/stack.py diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index 613ceca5bf..61366bbfc9 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -124,8 +124,9 @@ def resource_get_all_by_stack(context, stack_id): return results -def stack_get_by_name(context, stack_name): +def stack_get_by_name(context, stack_name, owner_id=None): result = model_query(context, models.Stack).\ + filter_by(owner_id=owner_id).\ filter_by(name=stack_name).first() if (result is not None and context is not None and result.username != context.username): @@ -143,13 +144,15 @@ def stack_get(context, stack_id): def stack_get_all(context): - results = model_query(context, models.Stack).all() + results = model_query(context, models.Stack).\ + filter_by(owner_id=None).all() return results def stack_get_by_user(context): results = model_query(context, models.Stack).\ - filter_by(username=context.username).all() + filter_by(owner_id=None).\ + filter_by(username=context.username).all() return results diff --git a/heat/db/sqlalchemy/migrate_repo/versions/006_nested_stacks.py b/heat/db/sqlalchemy/migrate_repo/versions/006_nested_stacks.py new file mode 100644 index 0000000000..1dd989a6b0 --- /dev/null +++ b/heat/db/sqlalchemy/migrate_repo/versions/006_nested_stacks.py @@ -0,0 +1,23 @@ +from sqlalchemy import * +from migrate import * + + +def upgrade(migrate_engine): + meta = MetaData(bind=migrate_engine) + + # This was unused + resource = Table('resource', meta, autoload=True) + resource.c.depends_on.drop() + + stack = Table('stack', meta, autoload=True) + Column('owner_id', Integer, nullable=True).create(stack) + + +def downgrade(migrate_engine): + meta = MetaData(bind=migrate_engine) + + resource = Table('resource', meta, autoload=True) + Column('depends_on', Integer).create(resource) + + stack = Table('stack', meta, autoload=True) + stack.c.owner_id.drop() diff --git a/heat/db/sqlalchemy/models.py b/heat/db/sqlalchemy/models.py index eea66a37ea..e4721d253c 100644 --- a/heat/db/sqlalchemy/models.py +++ b/heat/db/sqlalchemy/models.py @@ -131,12 +131,12 @@ class ParsedTemplate(BASE, HeatBase): class Stack(BASE, HeatBase): - """Represents an generated by the heat engine.""" + """Represents a stack created by the heat engine.""" __tablename__ = 'stack' id = Column(Integer, primary_key=True) - name = Column(String, unique=True) + name = Column(String) raw_template_id = Column(Integer, ForeignKey('raw_template.id'), nullable=False) raw_template = relationship(RawTemplate, @@ -144,6 +144,7 @@ class Stack(BASE, HeatBase): username = Column(String) user_creds_id = Column(Integer, ForeignKey('user_creds.id'), nullable=False) + owner_id = Column(Integer, nullable=True) class UserCreds(BASE, HeatBase): @@ -204,8 +205,6 @@ class Resource(BASE, HeatBase): nullable=False) stack = relationship(Stack, backref=backref('resources')) - depends_on = Column(Integer) - class WatchRule(BASE, HeatBase): """Represents a watch_rule created by the heat engine.""" diff --git a/heat/engine/checkeddict.py b/heat/engine/checkeddict.py index 41367a945b..dd09d578ee 100644 --- a/heat/engine/checkeddict.py +++ b/heat/engine/checkeddict.py @@ -88,6 +88,11 @@ class CheckedDict(collections.MutableMapping): raise ValueError('%s: %s is out of range' % (self.name, key)) + elif t == 'Map': + if not isinstance(value, dict): + raise ValueError('%s: %s Value must be a map' % + (self.name, key)) + elif t == 'List': if not isinstance(value, (list, tuple)): raise ValueError('%s: %s Value must be a list' % diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 5b0f3b38ea..ef44731f4e 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -41,7 +41,7 @@ class Stack(object): self.context = context self.t = template self.maps = self.t.get('Mappings', {}) - self.outputs = self.t.get('Outputs', {}) + self.outputs = self.resolve_static_data(self.t.get('Outputs', {})) self.res = {} self.doc = None self.name = stack_name @@ -244,16 +244,18 @@ class Stack(object): self.state_set(self.DELETE_COMPLETE, 'Deleted successfully') db_api.stack_delete(self.context, self.id) + def output(self, key): + value = self.outputs[key].get('Value', '') + return self.resolve_runtime_data(value) + def get_outputs(self): - outputs = self.resolve_runtime_data(self.outputs) - def output_dict(k): - return {'Description': outputs[k].get('Description', - 'No description given'), + return {'Description': self.outputs[k].get('Description', + 'No description given'), 'OutputKey': k, - 'OutputValue': outputs[k].get('Value', '')} + 'OutputValue': self.output(k)} - return [output_dict(key) for key in outputs] + return [output_dict(key) for key in self.outputs] def restart_resource(self, resource_name): ''' diff --git a/heat/engine/resource_types.py b/heat/engine/resource_types.py index e0ed030f63..cd9ec49b3f 100644 --- a/heat/engine/resource_types.py +++ b/heat/engine/resource_types.py @@ -24,12 +24,14 @@ from heat.engine import cloud_watch from heat.engine import eip from heat.engine import instance from heat.engine import security_group +from heat.engine import stack from heat.engine import user from heat.engine import volume from heat.engine import wait_condition _resource_classes = { + 'AWS::CloudFormation::Stack': stack.Stack, 'AWS::CloudFormation::WaitCondition': wait_condition.WaitCondition, 'AWS::CloudFormation::WaitConditionHandle': wait_condition.WaitConditionHandle, diff --git a/heat/engine/stack.py b/heat/engine/stack.py new file mode 100644 index 0000000000..c9474fe8bc --- /dev/null +++ b/heat/engine/stack.py @@ -0,0 +1,117 @@ +# 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. + +import urllib2 +import json +import logging + +from heat.common import exception +from heat.engine.resources import Resource +from heat.db import api as db_api +from heat.engine import parser + +logger = logging.getLogger(__file__) + + +(PROP_TEMPLATE_URL, + PROP_TIMEOUT_MINS, + PROP_PARAMETERS) = ('TemplateURL', 'TimeoutInMinutes', 'Parameters') + + +class Stack(Resource): + properties_schema = {PROP_TEMPLATE_URL: {'Type': 'String', + 'Required': True}, + PROP_TIMEOUT_MINS: {'Type': 'Number'}, + PROP_PARAMETERS: {'Type': 'Map'}} + + def __init__(self, name, json_snippet, stack): + Resource.__init__(self, name, json_snippet, stack) + self._nested = None + + def _params(self): + p = self.stack.resolve_runtime_data(self.properties[PROP_PARAMETERS]) + return p + + def nested(self): + if self._nested is None: + if self.instance_id is None: + return None + + st = db_api.stack_get(self.stack.context, self.instance_id) + if not st: + raise exception.NotFound('Nested stack not found in DB') + + n = parser.Stack(self.stack.context, st.name, + st.raw_template.parsed_template.template, + self.instance_id, self._params()) + self._nested = n + + return self._nested + + def handle_create(self): + response = urllib2.urlopen(self.properties[PROP_TEMPLATE_URL]) + child_template = json.loads(response.read()) + + self._nested = parser.Stack(self.stack.context, + self.name, + child_template, + parms=self._params(), + metadata_server=self.stack.metadata_server) + + rt = {'template': child_template, 'stack_name': self.name} + new_rt = db_api.raw_template_create(None, rt) + + parent_stack = db_api.stack_get(self.stack.context, self.stack.id) + + s = {'name': self.name, + 'owner_id': self.stack.id, + 'raw_template_id': new_rt.id, + 'user_creds_id': parent_stack.user_creds_id, + 'username': self.stack.context.username} + new_s = db_api.stack_create(None, s) + self._nested.id = new_s.id + + pt = {'template': self._nested.t, 'raw_template_id': new_rt.id} + new_pt = db_api.parsed_template_create(None, pt) + + self._nested.parsed_template_id = new_pt.id + + self._nested.create() + self.instance_id_set(self._nested.id) + + def handle_delete(self): + try: + stack = self.nested() + except exception.NotFound: + logger.info("Stack not found to delete") + else: + if stack is not None: + stack.delete() + + def FnGetAtt(self, key): + if not key.startswith('Outputs.'): + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) + + prefix, dot, op = key.partition('.') + stack = self.nested() + if stack is None: + # This seems like a hack, to get past validation + return '' + if op not in self.nested().outputs: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) + + return stack.output(op)