]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement an "Action in progress" error.
authorJason Dunsmore <jasondunsmore@gmail.com>
Thu, 1 Aug 2013 17:37:29 +0000 (12:37 -0500)
committerJason Dunsmore <jasondunsmore@gmail.com>
Wed, 7 Aug 2013 15:27:16 +0000 (10:27 -0500)
Raises an "Action in progress" error if an action is in progress when
another action is issued.

Fixes bug #1207032

Change-Id: I2b6fd92cfa4b93ac2531dd3d76bf2dfc01e58c50

heat/api/aws/exception.py
heat/api/middleware/fault.py
heat/common/exception.py
heat/engine/service.py
heat/tests/test_api_cfn_v1.py
heat/tests/test_api_openstack_v1.py
heat/tests/test_engine_service.py

index a0207b67381f13bc11fd041716161d7d7d408b52..fc9281af7e5f994e18a8a1ed28f15aef8a0d4943 100644 (file)
@@ -246,6 +246,15 @@ class HeatAPINotImplementedError(HeatAPIException):
     err_type = "Server"
 
 
+class HeatInvalidStateError(HeatAPIException):
+    '''
+    Cannot perform action on stack in its current state
+    '''
+    code = 400
+    title = 'InvalidAction'
+    explanation = "Cannot perform action on stack in its current state"
+
+
 def map_remote_error(ex):
         """
         Map rpc_common.RemoteError exceptions returned by the engine
@@ -268,7 +277,8 @@ def map_remote_error(ex):
             'UserParameterMissing',
         )
         denied_errors = ('Forbidden', 'NotAuthorized')
-        already_exists_errors = ('StackExists')
+        already_exists_errors = ('StackExists',)
+        invalid_state_errors = ('ActionInProgress',)
 
         if ex.exc_type in inval_param_errors:
             return HeatInvalidParameterValueError(detail=ex.value)
@@ -276,6 +286,8 @@ def map_remote_error(ex):
             return HeatAccessDeniedError(detail=ex.value)
         elif ex.exc_type in already_exists_errors:
             return AlreadyExistsError(detail=ex.value)
+        elif ex.exc_type in invalid_state_errors:
+            return HeatInvalidStateError(detail=ex.value)
         else:
             # Map everything else to internal server error for now
             return HeatInternalFailureError(detail=ex.value)
index c8e77093fe0dddef732c267dfcf9a0235de9fe43..5c1287102c186417d043d44bc2615f5189391780 100644 (file)
@@ -55,6 +55,7 @@ class FaultWrapper(wsgi.Middleware):
 
     error_map = {
         'AttributeError': webob.exc.HTTPBadRequest,
+        'ActionInProgress': webob.exc.HTTPConflict,
         'ValueError': webob.exc.HTTPBadRequest,
         'StackNotFound': webob.exc.HTTPNotFound,
         'ResourceNotFound': webob.exc.HTTPNotFound,
index 4f0c08153082fe03fe061e8dd0a9bd45efdbab42..3492c3ac1d80e0a6454e7bd6782401af03629fdd 100644 (file)
@@ -263,6 +263,11 @@ class WatchRuleNotFound(OpenstackException):
     message = _("The Watch Rule (%(watch_name)s) could not be found.")
 
 
+class ActionInProgress(OpenstackException):
+    message = _("Stack %(stack_name)s already has an action (%(action)s) "
+                "in progress")
+
+
 class ResourceFailure(OpenstackException):
     message = _("%(exc_type)s: %(message)s")
 
index 56eb5a17c0cc37e872eb933b7de6ef22de465109..fed7d3bf140d3fdb9bf5b7ebeff8fa7a914e5dae 100644 (file)
@@ -277,6 +277,10 @@ class EngineService(service.Service):
         # Get the database representation of the existing stack
         db_stack = self._get_stack(cnxt, stack_identity)
 
+        if db_stack.status != parser.Stack.COMPLETE:
+            raise exception.ActionInProgress(stack_name=db_stack.name,
+                                             action=db_stack.action)
+
         current_stack = parser.Stack.load(cnxt, stack=db_stack)
 
         # Now parse the template and any parameters for the updated
@@ -378,8 +382,11 @@ class EngineService(service.Service):
         """
         st = self._get_stack(cnxt, stack_identity)
 
-        logger.info('deleting stack %s' % st.name)
+        if st.status not in (parser.Stack.COMPLETE, parser.Stack.FAILED):
+            raise exception.ActionInProgress(stack_name=st.name,
+                                             action=st.action)
 
+        logger.info('deleting stack %s' % st.name)
         stack = parser.Stack.load(cnxt, stack=st)
 
         # Kill any pending threads by calling ThreadGroup.stop()
index b302770e6336c2e8938249851840912c5b1dee4e..703a6d4bfa8c3ac2410afa92a23ef73a30838520 100644 (file)
@@ -821,6 +821,48 @@ class CfnStackControllerTest(HeatTestCase):
                          exception.AlreadyExistsError)
         self.m.VerifyAll()
 
+    def test_invalid_state_err(self):
+        '''
+        Test that an ActionInProgress exception results in a
+        HeatInvalidStateError.
+
+        '''
+        # Format a dummy request
+        stack_name = "wordpress"
+        template = {u'Foo': u'bar'}
+        json_template = json.dumps(template)
+        params = {'Action': 'CreateStack', 'StackName': stack_name,
+                  'TemplateBody': '%s' % json_template,
+                  'TimeoutInMinutes': 30,
+                  'Parameters.member.1.ParameterKey': 'InstanceType',
+                  'Parameters.member.1.ParameterValue': 'm1.xlarge'}
+        engine_parms = {u'InstanceType': u'm1.xlarge'}
+        engine_args = {'timeout_mins': u'30'}
+        dummy_req = self._dummy_GET_request(params)
+
+        # Insert an engine RPC error and ensure we map correctly to the
+        # heat exception type
+        self.m.StubOutWithMock(rpc, 'call')
+
+        rpc.call(dummy_req.context, self.topic,
+                 {'namespace': None,
+                  'method': 'create_stack',
+                  'args': {'stack_name': stack_name,
+                           'template': template,
+                           'params': engine_parms,
+                           'files': {},
+                           'args': engine_args},
+                  'version': self.api_version}, None
+                 ).AndRaise(rpc_common.RemoteError("ActionInProgress"))
+
+        self.m.ReplayAll()
+
+        result = self.controller.create(dummy_req)
+
+        self.assertEqual(type(result),
+                         exception.HeatInvalidStateError)
+        self.m.VerifyAll()
+
     def test_create_err_engine(self):
         # Format a dummy request
         stack_name = "wordpress"
index 80d37a689664566b860084613bd379f3dfb0a8ae..6639ec4858a605e56759090ff729331ac1f57294 100644 (file)
@@ -957,6 +957,48 @@ class StackControllerTest(ControllerTest, HeatTestCase):
                           body=body)
         self.m.VerifyAll()
 
+    def test_update_in_progress_err(self):
+        '''
+        Tests that the ActionInProgress exception results in an HTTPConflict.
+
+        '''
+        identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')
+        template = {u'Foo': u'bar'}
+        parameters = {u'InstanceType': u'm1.xlarge'}
+        body = {'template': template,
+                'parameters': parameters,
+                'files': {},
+                'timeout_mins': 30}
+
+        req = self._put('/stacks/%(stack_name)s/%(stack_id)s' % identity,
+                        json.dumps(body))
+
+        self.m.StubOutWithMock(rpc, 'call')
+        error = to_remote_error(heat_exc.ActionInProgress(stack_name="foo",
+                                                          action="UPDATE"))
+        rpc.call(req.context, self.topic,
+                 {'namespace': None,
+                  'method': 'update_stack',
+                  'args': {'stack_identity': dict(identity),
+                           'template': template,
+                           'params': {'parameters': parameters},
+                           'files': {},
+                           'args': {'timeout_mins': 30}},
+                  'version': self.api_version},
+                 None).AndRaise(error)
+        self.m.ReplayAll()
+
+        resp = request_with_middleware(fault.FaultWrapper,
+                                       self.controller.update,
+                                       req, tenant_id=identity.tenant,
+                                       stack_name=identity.stack_name,
+                                       stack_id=identity.stack_id,
+                                       body=body)
+        self.assertEqual(resp.json['code'], 409)
+        self.assertEqual(resp.json['title'], 'Conflict')
+        self.assertEqual(resp.json['error']['type'], 'ActionInProgress')
+        self.m.VerifyAll()
+
     def test_update_bad_name(self):
         identity = identifier.HeatIdentifier(self.tenant, 'wibble', '6')
         template = {u'Foo': u'bar'}
index 069ed1fe6b4ba03eca62f30ec2c7df98de019d37..d9867125166df2743d526409d4fb949f11adcca7 100644 (file)
@@ -413,12 +413,8 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
     def test_stack_delete(self):
         stack_name = 'service_delete_test_stack'
         stack = get_wordpress_stack(stack_name, self.ctx)
-        sid = stack.store()
-
-        s = db_api.stack_get(self.ctx, sid)
-        self.m.StubOutWithMock(parser.Stack, 'load')
-
-        parser.Stack.load(self.ctx, stack=s).AndReturn(stack)
+        stack.status = "COMPLETE"
+        stack.store()
 
         self.man.tg = DummyThreadGroup()
 
@@ -428,16 +424,29 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
                          self.man.delete_stack(self.ctx, stack.identifier()))
         self.m.VerifyAll()
 
+    def test_stack_delete_action_in_progress_err(self):
+        '''
+        Test that deleting a stack with an action in progress results in
+        an ActionInProgress exception.
+
+        '''
+        stack_name = 'service_delete_action_in_progress_err'
+        stack = get_wordpress_stack(stack_name, self.ctx)
+        stack.status = "IN_PROGRESS"
+        stack.store()
+
+        self.assertRaises(exception.ActionInProgress,
+                          self.man.delete_stack,
+                          self.ctx,
+                          stack.identifier())
+
     def test_stack_delete_nonexist(self):
         stack_name = 'service_delete_nonexist_test_stack'
         stack = get_wordpress_stack(stack_name, self.ctx)
 
-        self.m.ReplayAll()
-
         self.assertRaises(exception.StackNotFound,
                           self.man.delete_stack,
                           self.ctx, stack.identifier())
-        self.m.VerifyAll()
 
     def test_stack_update(self):
         stack_name = 'service_update_test_stack'
@@ -445,6 +454,7 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
         template = '{ "Template": "data" }'
 
         old_stack = get_wordpress_stack(stack_name, self.ctx)
+        old_stack.status = 'COMPLETE'
         sid = old_stack.store()
         s = db_api.stack_get(self.ctx, sid)
 
@@ -477,12 +487,33 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
         self.assertTrue(result['stack_id'])
         self.m.VerifyAll()
 
+    def test_stack_update_action_in_progress_err(self):
+        '''
+        Test that updating a stack with an action in progress results
+        in an ActionInProgress exception.
+
+        '''
+        stack_name = 'service_update_action_in_progress_err_test_stack'
+        params = {'foo': 'bar'}
+        template = '{ "Template": "data" }'
+
+        old_stack = get_wordpress_stack(stack_name, self.ctx)
+        old_stack.status = 'IN_PROGRESS'
+        old_stack.store()
+
+        self.assertRaises(
+            exception.ActionInProgress,
+            self.man.update_stack,
+            self.ctx, old_stack.identifier(),
+            template, params, None, {})
+
     def test_stack_update_verify_err(self):
         stack_name = 'service_update_verify_err_test_stack'
         params = {'foo': 'bar'}
         template = '{ "Template": "data" }'
 
         old_stack = get_wordpress_stack(stack_name, self.ctx)
+        old_stack.status = 'COMPLETE'
         old_stack.store()
         sid = old_stack.store()
         s = db_api.stack_get(self.ctx, sid)