]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement Nested Stacks
authorZane Bitter <zbitter@redhat.com>
Sun, 17 Jun 2012 14:05:58 +0000 (16:05 +0200)
committerZane Bitter <zbitter@redhat.com>
Sun, 17 Jun 2012 16:29:00 +0000 (18:29 +0200)
Fixes #123.

Change-Id: I89affe471b4df898c7d3157ff23f9b64003c2893
Signed-off-by: Zane Bitter <zbitter@redhat.com>
heat/db/sqlalchemy/api.py
heat/db/sqlalchemy/migrate_repo/versions/006_nested_stacks.py [new file with mode: 0644]
heat/db/sqlalchemy/models.py
heat/engine/checkeddict.py
heat/engine/parser.py
heat/engine/resource_types.py
heat/engine/stack.py [new file with mode: 0644]

index 613ceca5bffee8a076e2a814e22138af52c57ffa..61366bbfc947707a0be6f0c99399956cf0af628a 100644 (file)
@@ -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 (file)
index 0000000..1dd989a
--- /dev/null
@@ -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()
index eea66a37ea2429058114382ec7d475de2c07d214..e4721d253c8fbe27d580326bad76d125a341d8a5 100644 (file)
@@ -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."""
index 41367a945b53011e1b29faf9ff877b8de425706c..dd09d578eec4278ed058db5929c31220c93c65bf 100644 (file)
@@ -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' %
index 5b0f3b38ea8289c93b40b621ec70dc8e06999795..ef44731f4e26626f4d86e77f8757267c1b337928 100644 (file)
@@ -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 get_outputs(self):
-        outputs = self.resolve_runtime_data(self.outputs)
+    def output(self, key):
+        value = self.outputs[key].get('Value', '')
+        return self.resolve_runtime_data(value)
 
+    def get_outputs(self):
         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):
         '''
index e0ed030f63b8d05c478ed52a68a4b2342e1337c1..cd9ec49b3ffd1ab0008286d582993c1d91681281 100644 (file)
@@ -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 (file)
index 0000000..c9474fe
--- /dev/null
@@ -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)