if stack_status == self.CREATE_FAILED and not self.disable_rollback:
self.delete(action=self.ROLLBACK)
- def update(self, newstack):
+ def update(self, newstack, action=UPDATE):
'''
Compare the current stack with newstack,
and where necessary create/update/delete the resources until
Update will fail if it exceeds the specified timeout. The default is
60 minutes, set in the constructor
'''
- if self.state not in (self.CREATE_COMPLETE, self.UPDATE_COMPLETE):
- self.state_set(self.UPDATE_FAILED, 'State invalid for update')
+ if action not in (self.UPDATE, self.ROLLBACK):
+ logger.error("Unexpected action %s passed to update!" % action)
+ self.state_set(self.UPDATE_FAILED, "Invalid action %s" % action)
return
- else:
+
+ if self.state not in (self.CREATE_COMPLETE, self.UPDATE_COMPLETE,
+ self.ROLLBACK_COMPLETE):
+ if (action == self.ROLLBACK and
+ self.state == self.UPDATE_IN_PROGRESS):
+ logger.debug("Starting update rollback for %s" % self.name)
+ else:
+ if action == self.UPDATE:
+ self.state_set(self.UPDATE_FAILED,
+ 'State invalid for update')
+ else:
+ self.state_set(self.ROLLBACK_FAILED,
+ 'State invalid for rollback')
+ return
+
+ if action == self.UPDATE:
self.state_set(self.UPDATE_IN_PROGRESS, 'Stack update started')
+ else:
+ self.state_set(self.ROLLBACK_IN_PROGRESS, 'Stack rollback started')
# Now make the resources match the new stack definition
with eventlet.Timeout(self.timeout_mins * 60) as tmo:
raise exception.ResourceUpdateFailed(
resource_name=res.name)
else:
- logger.error("Failed to update %s" % res.name)
+ logger.error("Failed to %s %s" %
+ (action, res.name))
raise exception.ResourceUpdateFailed(
resource_name=res.name)
self.outputs = self.resolve_static_data(template_outputs)
self.store()
- stack_status = self.UPDATE_COMPLETE
- reason = 'Stack successfully updated'
+ if action == self.UPDATE:
+ stack_status = self.UPDATE_COMPLETE
+ reason = 'Stack successfully updated'
+ else:
+ stack_status = self.ROLLBACK_COMPLETE
+ reason = 'Stack rollback completed'
except eventlet.Timeout as t:
if t is tmo:
# not my timeout
raise
except exception.ResourceUpdateFailed as e:
- stack_status = self.UPDATE_FAILED
reason = str(e) or "Error : %s" % type(e)
- self.state_set(stack_status, reason)
+ if action == self.UPDATE:
+ stack_status = self.UPDATE_FAILED
+ # If rollback is enabled, we do another update, with the
+ # existing template, so we roll back to the original state
+ # Note - ensure nothing after the "flip the template..."
+ # section above can raise ResourceUpdateFailed or this
+ # will not work ;)
+ if self.disable_rollback:
+ stack_status = self.UPDATE_FAILED
+ else:
+ oldstack = Stack(self.context, self.name, self.t,
+ self.parameters)
+ self.update(oldstack, action=self.ROLLBACK)
+ return
+ else:
+ stack_status = self.ROLLBACK_FAILED
+
+ self.state_set(stack_status, reason)
def delete(self, action=DELETE):
'''
'Properties': {'Foo': 'abc'}}}}
self.stack = parser.Stack(self.ctx, 'update_test_stack',
- template.Template(tmpl))
+ template.Template(tmpl),
+ disable_rollback=True)
self.stack.store()
self.stack.create()
self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
'Properties': {'Foo': 'abc'}}}}
self.stack = parser.Stack(self.ctx, 'update_test_stack',
- template.Template(tmpl))
+ template.Template(tmpl),
+ disable_rollback=True)
self.stack.store()
self.stack.create()
self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
'Properties': {'Foo': 'abc'}}}}
self.stack = parser.Stack(self.ctx, 'update_test_stack',
- template.Template(tmpl))
+ template.Template(tmpl),
+ disable_rollback=True)
self.stack.store()
self.stack.create()
self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
self.stack.update(updated_stack)
self.assertEqual(self.stack.state, parser.Stack.UPDATE_FAILED)
self.m.VerifyAll()
+
+ @stack_delete_after
+ def test_update_rollback(self):
+ # patch in a dummy property schema for GenericResource
+ dummy_schema = {'Foo': {'Type': 'String'}}
+ resource.GenericResource.properties_schema = dummy_schema
+
+ tmpl = {'Resources': {'AResource': {'Type': 'GenericResourceType',
+ 'Properties': {'Foo': 'abc'}}}}
+
+ self.stack = parser.Stack(self.ctx, 'update_test_stack',
+ template.Template(tmpl))
+ self.stack.store()
+ self.stack.create()
+ self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
+
+ tmpl2 = {'Resources': {'AResource': {'Type': 'GenericResourceType',
+ 'Properties': {'Foo': 'xyz'}}}}
+
+ updated_stack = parser.Stack(self.ctx, 'updated_stack',
+ template.Template(tmpl2))
+
+ # There will be two calls to handle_update, one for the new template
+ # then another (with the initial template) for rollback
+ self.m.StubOutWithMock(resource.GenericResource, 'handle_update')
+ resource.GenericResource.handle_update(
+ tmpl2['Resources']['AResource']).AndReturn(
+ resource.Resource.UPDATE_REPLACE)
+ resource.GenericResource.handle_update(
+ tmpl['Resources']['AResource']).AndReturn(
+ resource.Resource.UPDATE_REPLACE)
+
+ # patch in a dummy handle_create making the replace fail when creating
+ # the replacement resource, but succeed the second call (rollback)
+ self.m.StubOutWithMock(resource.GenericResource, 'handle_create')
+ resource.GenericResource.handle_create().AndRaise(Exception)
+ resource.GenericResource.handle_create().AndReturn(None)
+ self.m.ReplayAll()
+
+ self.stack.update(updated_stack)
+ self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_COMPLETE)
+ self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc')
+ self.m.VerifyAll()
+
+ @stack_delete_after
+ def test_update_rollback_fail(self):
+ # patch in a dummy property schema for GenericResource
+ dummy_schema = {'Foo': {'Type': 'String'}}
+ resource.GenericResource.properties_schema = dummy_schema
+
+ tmpl = {'Resources': {'AResource': {'Type': 'GenericResourceType',
+ 'Properties': {'Foo': 'abc'}}}}
+
+ self.stack = parser.Stack(self.ctx, 'update_test_stack',
+ template.Template(tmpl))
+ self.stack.store()
+ self.stack.create()
+ self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
+
+ tmpl2 = {'Resources': {'AResource': {'Type': 'GenericResourceType',
+ 'Properties': {'Foo': 'xyz'}}}}
+
+ updated_stack = parser.Stack(self.ctx, 'updated_stack',
+ template.Template(tmpl2))
+
+ # There will be two calls to handle_update, one for the new template
+ # then another (with the initial template) for rollback
+ self.m.StubOutWithMock(resource.GenericResource, 'handle_update')
+ resource.GenericResource.handle_update(
+ tmpl2['Resources']['AResource']).AndReturn(
+ resource.Resource.UPDATE_REPLACE)
+ resource.GenericResource.handle_update(
+ tmpl['Resources']['AResource']).AndReturn(
+ resource.Resource.UPDATE_REPLACE)
+
+ # patch in a dummy handle_create making the replace fail when creating
+ # the replacement resource, and again on the second call (rollback)
+ self.m.StubOutWithMock(resource.GenericResource, 'handle_create')
+ resource.GenericResource.handle_create().AndRaise(Exception)
+ resource.GenericResource.handle_create().AndRaise(Exception)
+ self.m.ReplayAll()
+
+ self.stack.update(updated_stack)
+ self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_FAILED)
+ self.m.VerifyAll()
+
+ @stack_delete_after
+ def test_update_rollback_add(self):
+ tmpl = {'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
+
+ self.stack = parser.Stack(self.ctx, 'update_test_stack',
+ template.Template(tmpl))
+ self.stack.store()
+ self.stack.create()
+ self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
+
+ tmpl2 = {'Resources': {
+ 'AResource': {'Type': 'GenericResourceType'},
+ 'BResource': {'Type': 'GenericResourceType'}}}
+
+ updated_stack = parser.Stack(self.ctx, 'updated_stack',
+ template.Template(tmpl2))
+
+ # patch in a dummy handle_create making the replace fail when creating
+ # the replacement resource, and succeed on the second call (rollback)
+ self.m.StubOutWithMock(resource.GenericResource, 'handle_create')
+ resource.GenericResource.handle_create().AndRaise(Exception)
+ self.m.ReplayAll()
+
+ self.stack.update(updated_stack)
+ self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_COMPLETE)
+ self.assertFalse('BResource' in self.stack)
+ self.m.VerifyAll()
+
+ @stack_delete_after
+ def test_update_rollback_remove(self):
+ tmpl = {'Resources': {
+ 'AResource': {'Type': 'GenericResourceType'},
+ 'BResource': {'Type': 'GenericResourceType'}}}
+
+ self.stack = parser.Stack(self.ctx, 'update_test_stack',
+ template.Template(tmpl))
+ self.stack.store()
+ self.stack.create()
+ self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE)
+
+ tmpl2 = {'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
+
+ updated_stack = parser.Stack(self.ctx, 'updated_stack',
+ template.Template(tmpl2))
+
+ # patch in a dummy destroy making the delete fail
+ self.m.StubOutWithMock(resource.Resource, 'destroy')
+ resource.Resource.destroy().AndReturn('Error')
+ self.m.ReplayAll()
+
+ self.stack.update(updated_stack)
+ self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_COMPLETE)
+ self.assertTrue('BResource' in self.stack)
+ self.m.VerifyAll()
+ # Unset here so destroy() is not stubbed for stack.delete cleanup
+ self.m.UnsetStubs()