]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
heat api/engine : Implement UpdateStack functionality
authorSteven Hardy <shardy@redhat.com>
Tue, 17 Jul 2012 14:46:49 +0000 (15:46 +0100)
committerSteven Hardy <shardy@redhat.com>
Fri, 20 Jul 2012 14:31:03 +0000 (15:31 +0100)
Implements initial support for UpdateStack, currently
all resources default to delete/create on update.
Ref #171

Change-Id: I3e6e63143d554c21ccdee19879c4dfb8b6e693d7

17 files changed:
heat/api/v1/stacks.py
heat/db/api.py
heat/db/sqlalchemy/api.py
heat/engine/autoscaling.py
heat/engine/cloud_watch.py
heat/engine/eip.py
heat/engine/instance.py
heat/engine/loadbalancer.py
heat/engine/manager.py
heat/engine/parser.py
heat/engine/resources.py
heat/engine/security_group.py
heat/engine/stack.py
heat/engine/user.py
heat/engine/volume.py
heat/engine/wait_condition.py
heat/tests/test_parser.py

index 4e133ecfab280ac6dd6e21c8d6fd9da368b35e55..41b8dbcdbbdefe82cf69a8341bca49518f54836c 100644 (file)
@@ -281,10 +281,21 @@ class StackController(object):
 
         return None
 
+    CREATE_OR_UPDATE_ACTION = (
+        CREATE_STACK, UPDATE_STACK
+        ) = (
+        "CreateStack", "UpdateStack")
+
     def create(self, req):
+        return self.create_or_update(req, self.CREATE_STACK)
+
+    def update(self, req):
+        return self.create_or_update(req, self.UPDATE_STACK)
+
+    def create_or_update(self, req, action=None):
         """
-        Implements CreateStack API action
-        Create stack as defined in template file
+        Implements CreateStack and UpdateStack API actions
+        Create or update stack as defined in template file
         """
         def extract_args(params):
             """
@@ -302,6 +313,14 @@ class StackController(object):
 
             return result
 
+        if action not in self.CREATE_OR_UPDATE_ACTION:
+            msg = _("Unexpected action %s" % action)
+            # This should not happen, so return HeatInternalFailureError
+            return exception.HeatInternalFailureError(detail=msg)
+
+        engine_action = {self.CREATE_STACK: "create_stack",
+                         self.UPDATE_STACK: "update_stack"}
+
         con = req.context
 
         # Extract the stack input parameters
@@ -328,7 +347,7 @@ class StackController(object):
 
         try:
             res = rpc.call(con, 'engine',
-                            {'method': 'create_stack',
+                            {'method': engine_action[action],
                              'args': {'stack_name': req.params['StackName'],
                                       'template': stack,
                                       'params': stack_parms,
@@ -336,8 +355,7 @@ class StackController(object):
         except rpc_common.RemoteError as ex:
             return self._remote_error(ex)
 
-        return self._format_response('CreateStack',
-            self._stackid_addprefix(res))
+        return self._format_response(action, self._stackid_addprefix(res))
 
     def get_template(self, req):
         """
index 7b27efc5b59459d52af7fd7e203d5afe53b4125c..62616c08b9b75c07a799c1a21ba498d9fda2cf73 100644 (file)
@@ -107,6 +107,10 @@ def stack_create(context, values):
     return IMPL.stack_create(context, values)
 
 
+def stack_update(context, stack_id, values):
+    return IMPL.stack_update(context, stack_id, values)
+
+
 def stack_delete(context, stack_id):
     return IMPL.stack_delete(context, stack_id)
 
index 31bd1393c4e66bd0bcc9ef7088b146ba0aa3ccdb..fd20af1ef309f09bfc85f289ebfa35dd2f7d35f9 100644 (file)
@@ -154,6 +154,27 @@ def stack_create(context, values):
     return stack_ref
 
 
+def stack_update(context, stack_id, values):
+    stack = stack_get(context, stack_id)
+
+    if not stack:
+        raise NotFound('Attempt to update a stack with id: %s %s' %
+                        (stack_id, 'that does not exist'))
+
+    old_template_id = stack.raw_template_id
+
+    stack.update(values)
+    stack.save()
+
+    # When the raw_template ID changes, we delete the old template
+    # after storing the new template ID
+    if stack.raw_template_id != old_template_id:
+        session = Session.object_session(stack)
+        rt = raw_template_get(context, old_template_id)
+        session.delete(rt)
+        session.flush()
+
+
 def stack_delete(context, stack_id):
     s = stack_get(context, stack_id)
     if not s:
index 8146e2e919046f8c997aa2f3f41ffd0dfc02211c..5fe21fb5c12a3ffb86fb1f91e36175af8fa9a372 100644 (file)
@@ -62,6 +62,9 @@ class AutoScalingGroup(Resource):
         self.adjust(int(self.properties['MinSize']),
                     adjustment_type='ExactCapacity')
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         if self.instance_id is not None:
             conf = self.properties['LaunchConfigurationName']
index cdb5c1e0d5ed6df634337ec66ab70828f47f99fd..a82f9b0c119aeb68ea2fa97317523b4c50201c38 100644 (file)
@@ -73,6 +73,9 @@ class CloudWatchAlarm(Resource):
         wr = db_api.watch_rule_create(self.context, wr_values)
         self.instance_id = wr.id
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         try:
             db_api.watch_rule_delete(self.context, self.name)
index de22eeae5c51a28b3c4c00ae3831c1c5d1e46b78..854db76fba0cdde3a53417721d5d41c9758ecaf2 100644 (file)
@@ -49,6 +49,9 @@ class ElasticIp(Resource):
         self.ipaddress = ips.ip
         self.instance_id_set(ips.id)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def validate(self):
         '''
         Validate the ip address here
index 351886f837581ce34f76835d05dc9dfcddd3d7d6..77eb1b7fc260fd2c84b598f304e89de24b161aa7 100644 (file)
@@ -258,6 +258,9 @@ class Instance(resources.Resource):
                                   ('nova reported unexpected',
                                    self.name, server.status))
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def validate(self):
         '''
         Validate any of the provided params
index 0bfade85bebc1213442df8d74a6e7a3327c8913a..12e8115c88131a4ae8dc1c5ad3c5a0c2922a7273 100644 (file)
@@ -312,6 +312,9 @@ class LoadBalancer(stack.Stack):
     def FnGetRefId(self):
         return unicode(self.name)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def FnGetAtt(self, key):
         '''
         We don't really support any of these yet.
index 31c95e9e1ad8610665d38ccaee3b12e62bdbe9d4..5db4d3e65c0c78866688eab8e9a687351be0f116 100644 (file)
@@ -122,6 +122,46 @@ class EngineManager(manager.Manager):
 
         return {'StackName': stack.name, 'StackId': stack.id}
 
+    def update_stack(self, context, stack_name, template, params, args):
+        """
+        The update_stack method updates an existing stack based on the
+        provided template and parameters.
+        Note that at this stage the template has already been fetched from the
+        heat-api process if using a template-url.
+        arg1 -> RPC context.
+        arg2 -> Name of the stack you want to create.
+        arg3 -> Template of stack you want to create.
+        arg4 -> Stack Input Params
+        arg4 -> Request parameters/args passed from API
+        """
+        logger.info('template is %s' % template)
+
+        auth.authenticate(context)
+
+        # Get the database representation of the existing stack
+        db_stack = db_api.stack_get_by_name(None, stack_name)
+        if not db_stack:
+            return {'Error': 'No stack exists with that name.'}
+
+        current_stack = parser.Stack.load(context, db_stack.id)
+
+        # Now parse the template and any parameters for the updated
+        # stack definition.
+        tmpl = parser.Template(template)
+        template_params = parser.Parameters(stack_name, tmpl, params)
+        common_params = api.extract_args(args)
+
+        updated_stack = parser.Stack(context, stack_name, tmpl,
+                                     template_params, **common_params)
+
+        response = updated_stack.validate()
+        if response['Description'] != 'Successfully validated':
+            return response
+
+        greenpool.spawn_n(current_stack.update, updated_stack)
+
+        return {'StackName': current_stack.name, 'StackId': current_stack.id}
+
     def validate_template(self, context, template, params):
         """
         The validate_template method uses the stack parser to check
index 230b522e2b4e50c0602297aed22594d06a5b052f..f41f2a160585893dac3dba17f6d58867183ef424 100644 (file)
@@ -227,13 +227,18 @@ class Template(object):
 
 
 class Stack(object):
-    IN_PROGRESS = 'IN_PROGRESS'
+    CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
     CREATE_FAILED = 'CREATE_FAILED'
     CREATE_COMPLETE = 'CREATE_COMPLETE'
+
     DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
     DELETE_FAILED = 'DELETE_FAILED'
     DELETE_COMPLETE = 'DELETE_COMPLETE'
 
+    UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
+    UPDATE_COMPLETE = 'UPDATE_COMPLETE'
+    UPDATE_FAILED = 'UPDATE_FAILED'
+
     created_time = resources.Timestamp(db_api.stack_get, 'created_at')
     updated_time = resources.Timestamp(db_api.stack_get, 'updated_at')
 
@@ -290,21 +295,26 @@ class Stack(object):
         return stack
 
     def store(self, owner=None):
-        '''Store the stack in the database and return its ID'''
-        if self.id is None:
-            new_creds = db_api.user_creds_create(self.context.to_dict())
-
-            s = {
-                'name': self.name,
-                'raw_template_id': self.t.store(),
-                'parameters': self.parameters.user_parameters(),
-                'owner_id': owner and owner.id,
-                'user_creds_id': new_creds.id,
-                'username': self.context.username,
-                'status': self.state,
-                'status_reason': self.state_description,
-                'timeout': self.timeout_mins,
-            }
+        '''
+        Store the stack in the database and return its ID
+        If self.id is set, we update the existing stack
+        '''
+        new_creds = db_api.user_creds_create(self.context.to_dict())
+
+        s = {
+            'name': self.name,
+            'raw_template_id': self.t.store(),
+            'parameters': self.parameters.user_parameters(),
+            'owner_id': owner and owner.id,
+            'user_creds_id': new_creds.id,
+            'username': self.context.username,
+            'status': self.state,
+            'status_reason': self.state_description,
+            'timeout': self.timeout_mins,
+        }
+        if self.id:
+            db_api.stack_update(self.context, self.id, s)
+        else:
             new_s = db_api.stack_create(self.context, s)
             self.id = new_s.id
 
@@ -332,6 +342,10 @@ class Stack(object):
         '''Get the resource with the specified name.'''
         return self.resources[key]
 
+    def __setitem__(self, key, value):
+        '''Set the resource with the specified name to a specific value'''
+        self.resources[key] = value
+
     def __contains__(self, key):
         '''Determine whether the stack contains the specified resource'''
         return key in self.resources
@@ -394,7 +408,7 @@ class Stack(object):
         Creation will fail if it exceeds the specified timeout. The default is
         60 minutes, set in the constructor
         '''
-        self.state_set(self.IN_PROGRESS, 'Stack creation started')
+        self.state_set(self.CREATE_IN_PROGRESS, 'Stack creation started')
 
         stack_status = self.CREATE_COMPLETE
         reason = 'Stack successfully created'
@@ -424,6 +438,116 @@ class Stack(object):
 
         self.state_set(stack_status, reason)
 
+    def update(self, newstack):
+        '''
+        Compare the current stack with newstack,
+        and where necessary create/update/delete the resources until
+        this stack aligns with newstack.
+
+        Note update of existing stack resources depends on update
+        being implemented in the underlying resource types
+
+        Update will fail if it exceeds the specified timeout. The default is
+        60 minutes, set in the constructor
+        '''
+        self.state_set(self.UPDATE_IN_PROGRESS, 'Stack update started')
+
+        # Now make the resources match the new stack definition
+        failures = []
+        with eventlet.Timeout(self.timeout_mins * 60) as tmo:
+            try:
+                # First delete any resources which are not in newstack
+                for res in self:
+                    if not res.name in newstack.keys():
+                        logger.debug("resource %s not found in updated stack"
+                                      % res.name + " definition, deleting")
+                        result = res.destroy()
+                        if result:
+                            failures.append('Resource %s delete failed'
+                                            % res.name)
+                        else:
+                            del self.resources[res.name]
+
+                # Then create any which are defined in newstack but not self
+                for res in newstack:
+                    if not res.name in self.keys():
+                        logger.debug("resource %s not found in current stack"
+                                      % res.name + " definition, adding")
+                        res.stack = self
+                        self[res.name] = res
+                        result = self[res.name].create()
+                        if result:
+                            failures.append('Resource %s create failed'
+                                            % res.name)
+
+                # Now (the hard part :) update existing resources
+                # The Resource base class allows equality-test of resources,
+                # based on the parsed template snippet for the resource.
+                # If this  test fails, we call the underlying resource.update
+                #
+                # FIXME : Implement proper update logic for the resources
+                # AWS define three update strategies, applied depending
+                # on the resource and what is being updated within a
+                # resource :
+                # - Update with no interruption
+                # - Update with some interruption
+                # - Update requires replacement
+                #
+                # Currently all resource have a default handle_update method
+                # which returns "requires replacement" (res.UPDATE_REPLACE)
+                for res in newstack:
+                    if self[res.name] != res:
+                        # Can fail if underlying resource class does not
+                        # implement update logic or update requires replacement
+                        retval = self[res.name].update(res.parsed_template())
+                        if retval == self[res.name].UPDATE_REPLACE:
+                            logger.info("Resource %s for stack %s" %
+                                        (res.name, self.name) +
+                                        " update requires replacement")
+                            # Resource requires replacement for update
+                            result = self[res.name].destroy()
+                            if result:
+                                failures.append('Resource %s delete failed'
+                                                % res.name)
+                            else:
+                                res.stack = self
+                                self[res.name] = res
+                                result = self[res.name].create()
+                                if result:
+                                    failures.append('Resource %s create failed'
+                                                    % res.name)
+                        else:
+                            logger.warning("Cannot update resource %s," %
+                                            res.name + " reason %s" % retval)
+                            failures.append('Resource %s update failed'
+                                            % res.name)
+
+                # Set stack status values
+                if not failures:
+                    # flip the template & parameters to the newstack values
+                    self.t = newstack.t
+                    self.parameters = newstack.parameters
+                    self.outputs = self.resolve_static_data(self.t[OUTPUTS])
+                    self.dependencies = self._get_dependencies(
+                        self.resources.itervalues())
+                    self.store()
+
+                    stack_status = self.UPDATE_COMPLETE
+                    reason = 'Stack successfully updated'
+                else:
+                    stack_status = self.UPDATE_FAILED
+                    reason = ",".join(failures)
+
+            except eventlet.Timeout as t:
+                if t is tmo:
+                    stack_status = self.UPDATE_FAILED
+                    reason = 'Timed out waiting for %s' % str(res)
+                else:
+                    # not my timeout
+                    raise
+
+        self.state_set(stack_status, reason)
+
     def delete(self):
         '''
         Delete all of the resources, and then the stack itself.
index b868d65964a488a6a1db821c71a21dec0a6319a1..77453e23b4050ada90190a3584731b44cb83eeaa 100644 (file)
@@ -100,6 +100,7 @@ class Timestamp(object):
 
 
 class Resource(object):
+    # Status strings
     CREATE_IN_PROGRESS = 'IN_PROGRESS'
     CREATE_FAILED = 'CREATE_FAILED'
     CREATE_COMPLETE = 'CREATE_COMPLETE'
@@ -110,6 +111,12 @@ class Resource(object):
     UPDATE_FAILED = 'UPDATE_FAILED'
     UPDATE_COMPLETE = 'UPDATE_COMPLETE'
 
+    # Status values, returned from subclasses to indicate update method
+    UPDATE_REPLACE = 'UPDATE_REPLACE'
+    UPDATE_INTERRUPTION = 'UPDATE_INTERRUPTION'
+    UPDATE_NO_INTERRUPTION = 'UPDATE_NO_INTERRUPTION'
+    UPDATE_NOT_IMPLEMENTED = 'UPDATE_NOT_IMPLEMENTED'
+
     # If True, this resource must be created before it can be referenced.
     strict_dependency = True
 
@@ -153,6 +160,21 @@ class Resource(object):
         self._nova = {}
         self._keystone = None
 
+    def __eq__(self, other):
+        '''Allow == comparison of two resources'''
+        # For the purposes of comparison, we declare two resource objects
+        # equal if their parsed_templates are the same
+        if isinstance(other, Resource):
+            return self.parsed_template() == other.parsed_template()
+        return NotImplemented
+
+    def __ne__(self, other):
+        '''Allow != comparison of two resources'''
+        result = self.__eq__(other)
+        if result is NotImplemented:
+            return result
+        return not result
+
     def parsed_template(self, section=None, default={}):
         '''
         Return the parsed template data for the resource. May be limited to
@@ -232,6 +254,43 @@ class Resource(object):
         else:
             self.state_set(self.CREATE_COMPLETE)
 
+    def update(self, json_snippet=None):
+        '''
+        update the resource. Subclasses should provide a handle_update() method
+        to customise update, the base-class handle_update will fail by default.
+        '''
+        if self.state in (self.CREATE_IN_PROGRESS, self.UPDATE_IN_PROGRESS):
+            return 'Resource update already requested'
+
+        if not json_snippet:
+            return 'Must specify json snippet for resource update!'
+
+        logger.info('updating %s' % str(self))
+
+        result = self.UPDATE_NOT_IMPLEMENTED
+        try:
+            self.state_set(self.UPDATE_IN_PROGRESS)
+            self.t = self.stack.resolve_static_data(json_snippet)
+            self.properties = checkeddict.Properties(self.name,
+                                                     self.properties_schema)
+            self.calculate_properties()
+            self.properties.validate()
+            if callable(getattr(self, 'handle_update', None)):
+                result = self.handle_update()
+        except Exception as ex:
+            logger.exception('update %s : %s' % (str(self), str(ex)))
+            self.state_set(self.UPDATE_FAILED, str(ex))
+            return str(ex)
+        else:
+            # If resource was updated (with or without interruption),
+            # then we set the resource to UPDATE_COMPLETE
+            if not result == self.UPDATE_REPLACE:
+                self.state_set(self.UPDATE_COMPLETE)
+            return result
+
+    def validate(self):
+        logger.info('Validating %s' % str(self))
+
     def validate(self):
         logger.info('Validating %s' % str(self))
 
@@ -375,6 +434,10 @@ class Resource(object):
         '''
         return base64.b64encode(data)
 
+    def handle_update(self):
+        raise NotImplementedError("Update not implemented for Resource %s"
+                                  % type(self))
+
 
 class GenericResource(Resource):
     properties_schema = {}
@@ -382,3 +445,7 @@ class GenericResource(Resource):
     def handle_create(self):
         logger.warning('Creating generic resource (Type "%s")' %
                 self.t['Type'])
+
+    def handle_update(self):
+        logger.warning('Updating generic resource (Type "%s")' %
+                self.t['Type'])
index 65a01dda5e307b4bd6ba63da7d8a650bce8a8f96..dcebff9d518c0d652ecb6b9f703caf9c237fac74 100644 (file)
@@ -66,6 +66,9 @@ class SecurityGroup(Resource):
                         # unexpected error
                         raise
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         if self.instance_id is not None:
             try:
index 145c8d3bdb6aacc3b17eaab5ae217eace3d99d03..4642b94e322ebbf03a9465191ebacfecf01c7f9f 100644 (file)
@@ -79,6 +79,9 @@ class Stack(Resource):
 
         self.create_with_template(template)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         try:
             stack = self.nested()
index be09b48534268215e16024945153ce4e4ead58cc..cabda2b3663a392fd65a400eef563d2e9cb217de 100644 (file)
@@ -56,6 +56,9 @@ class User(Resource):
                                             enabled=True)
         self.instance_id_set(user.id)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         try:
             user = self.keystone().users.get(DummyId(self.instance_id))
@@ -137,6 +140,9 @@ class AccessKey(Resource):
         self.instance_id_set(cred.access)
         self._secret = cred.secret
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         user = self._user_from_name(self.properties['UserName'])
         if user and self.instance_id:
index dee0debe984f9bc9582917b5740f46dc27ee38d2..8330fff8290765a489a19f87da7ed44ad9a9024e 100644 (file)
@@ -46,6 +46,9 @@ class Volume(Resource):
         else:
             raise exception.Error(vol.status)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         if self.instance_id is not None:
             vol = self.nova('volume').volumes.get(self.instance_id)
@@ -87,6 +90,9 @@ class VolumeAttachment(Resource):
         else:
             raise exception.Error(vol.status)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def handle_delete(self):
         server_id = self.properties['InstanceId']
         volume_id = self.properties['VolumeId']
index fc908a573b41a823367bff71e9cbfe59e6126a65..85f50c083e62d3113521cc2478e4e0492e468ea3 100644 (file)
@@ -43,6 +43,8 @@ class WaitConditionHandle(resources.Resource):
                             self.stack.id,
                             self.name)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
 
 WAIT_STATUSES = (
     WAITING,
@@ -111,6 +113,9 @@ class WaitCondition(resources.Resource):
         if status != SUCCESS:
             raise exception.Error(reason)
 
+    def handle_update(self):
+        return self.UPDATE_REPLACE
+
     def FnGetAtt(self, key):
         res = None
         if key == 'Data':
index d58ed45591e69e8d41bc31b59c45346379c2637b..ab25cdb853446652d701f3be8b9d8d949b8318db 100644 (file)
@@ -369,7 +369,7 @@ class StackTest(unittest.TestCase):
         self.assertEqual(stack.updated_time, None)
         stack.store()
         stored_time = stack.updated_time
-        stack.state_set(stack.IN_PROGRESS, 'testing')
+        stack.state_set(stack.CREATE_IN_PROGRESS, 'testing')
         self.assertNotEqual(stack.updated_time, None)
         self.assertNotEqual(stack.updated_time, stored_time)