--- /dev/null
+from sqlalchemy import *
+from migrate import *
+
+
+def upgrade(migrate_engine):
+ meta = MetaData(bind=migrate_engine)
+ stack = Table('stack', meta, autoload=True)
+
+ # Note hard-coded default 60 (minutes) here from the value in the
+ # engine, means we can upgrade and populate existing rows
+ try:
+ col = Column('disable_rollback', Boolean, nullable=False, default=True)
+ col.create(stack, populate_default=True)
+ except Exception as ex:
+ print "Caught exception adding disable_rollback column %s" % ex
+ # *Hack-alert* Sqlite in the unit tests can't handle the above
+ # approach to nullable=False, so retry with nullable=True
+ Column('disable_rollback', Boolean, nullable=True,
+ default=60).create(stack)
+
+
+def downgrade(migrate_engine):
+ meta = MetaData(bind=migrate_engine)
+ stack = Table('stack', meta, autoload=True)
+
+ stack.c.disable_rollback.drop()
nullable=False)
owner_id = Column(Integer, nullable=True)
timeout = Column(Integer)
+ disable_rollback = Column(Boolean)
class UserCreds(BASE, HeatBase):
class Stack(object):
+
+ ACTIONS = (CREATE, DELETE, UPDATE, ROLLBACK
+ ) = ('CREATE', 'DELETE', 'UPDATE', 'ROLLBACK')
+
CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
CREATE_FAILED = 'CREATE_FAILED'
CREATE_COMPLETE = 'CREATE_COMPLETE'
UPDATE_COMPLETE = 'UPDATE_COMPLETE'
UPDATE_FAILED = 'UPDATE_FAILED'
+ ROLLBACK_IN_PROGRESS = 'ROLLBACK_IN_PROGRESS'
+ ROLLBACK_COMPLETE = 'ROLLBACK_COMPLETE'
+ ROLLBACK_FAILED = 'ROLLBACK_FAILED'
+
created_time = timestamp.Timestamp(db_api.stack_get, 'created_at')
updated_time = timestamp.Timestamp(db_api.stack_get, 'updated_at')
def __init__(self, context, stack_name, tmpl, parameters=None,
stack_id=None, state=None, state_description='',
- timeout_mins=60, resolve_data=True):
+ timeout_mins=60, resolve_data=True, disable_rollback=False):
'''
Initialise from a context, name, Template object and (optionally)
Parameters object. The database ID may also be initialised, if the
self.state = state
self.state_description = state_description
self.timeout_mins = timeout_mins
+ self.disable_rollback = disable_rollback
if parameters is None:
parameters = Parameters(self.name, self.t)
params = Parameters(stack.name, template, stack.parameters)
stack = cls(context, stack.name, template, params,
stack.id, stack.status, stack.status_reason, stack.timeout,
- resolve_data)
+ resolve_data, stack.disable_rollback)
return stack
'status': self.state,
'status_reason': self.state_description,
'timeout': self.timeout_mins,
+ 'disable_rollback': self.disable_rollback,
}
if self.id:
db_api.stack_update(self.context, self.id, s)
self.state_set(stack_status, reason)
+ if stack_status == self.CREATE_FAILED and not self.disable_rollback:
+ self.delete(action=self.ROLLBACK)
+
def update(self, newstack):
'''
Compare the current stack with newstack,
self.state_set(stack_status, reason)
- def delete(self):
+ def delete(self, action=DELETE):
'''
Delete all of the resources, and then the stack itself.
+ The action parameter is used to differentiate between a user
+ initiated delete and an automatic stack rollback after a failed
+ create, which amount to the same thing, but the states are recorded
+ differently.
'''
- self.state_set(self.DELETE_IN_PROGRESS, 'Stack deletion started')
+ if action == self.DELETE:
+ self.state_set(self.DELETE_IN_PROGRESS, 'Stack deletion started')
+ elif action == self.ROLLBACK:
+ self.state_set(self.ROLLBACK_IN_PROGRESS, 'Stack rollback started')
+ else:
+ logger.error("Unexpected action %s passed to delete!" % action)
+ self.state_set(self.DELETE_FAILED, "Invalid action %s" % action)
+ return
failures = []
for res in reversed(self):
failures.append(str(res))
if failures:
- self.state_set(self.DELETE_FAILED,
- 'Failed to delete ' + ', '.join(failures))
+ if action == self.DELETE:
+ self.state_set(self.DELETE_FAILED,
+ 'Failed to delete ' + ', '.join(failures))
+ elif action == self.ROLLBACK:
+ self.state_set(self.ROLLBACK_FAILED,
+ 'Failed to rollback ' + ', '.join(failures))
else:
- self.state_set(self.DELETE_COMPLETE, 'Deleted successfully')
+ if action == self.DELETE:
+ self.state_set(self.DELETE_COMPLETE, 'Deleted successfully')
+ elif action == self.ROLLBACK:
+ self.state_set(self.ROLLBACK_COMPLETE, 'Rollback completed')
db_api.stack_delete(self.context, self.id)
def output(self, key):
params = parser.Parameters(self.physical_resource_name(), template,
user_params)
+ # Note we disable rollback for nested stacks, since they
+ # should be rolled back by the parent stack on failure
self._nested = parser.Stack(self.context,
self.physical_resource_name(),
template,
- params)
+ params,
+ disable_rollback=True)
nested_id = self._nested.store(self.stack)
self.resource_id_set(nested_id)
template = parser.Template(t)
params = parser.Parameters('test_stack', template, {'KeyName': 'test'})
stack = parser.Stack(create_context(self.m), 'test_stack', template,
- params, stack_id=None)
+ params, stack_id=None, disable_rollback=True)
stack.store()
return stack
from heat.engine import template
from heat.engine.resource import Resource
from heat.tests.utils import stack_delete_after
+import heat.db as db_api
def join(raw):
self.stack.state_set(self.stack.CREATE_IN_PROGRESS, 'testing')
self.assertNotEqual(self.stack.updated_time, None)
self.assertNotEqual(self.stack.updated_time, stored_time)
+
+ @stack_delete_after
+ def test_delete(self):
+ self.stack = parser.Stack(self.ctx, 'delete_test',
+ parser.Template({}))
+ stack_id = self.stack.store()
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertNotEqual(db_s, None)
+
+ self.stack.delete()
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertEqual(db_s, None)
+ self.assertEqual(self.stack.state, self.stack.DELETE_COMPLETE)
+
+ @stack_delete_after
+ def test_delete_rollback(self):
+ self.stack = parser.Stack(self.ctx, 'delete_rollback_test',
+ parser.Template({}))
+ stack_id = self.stack.store()
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertNotEqual(db_s, None)
+
+ self.stack.delete(action=self.stack.ROLLBACK)
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertEqual(db_s, None)
+ self.assertEqual(self.stack.state, self.stack.ROLLBACK_COMPLETE)
+
+ @stack_delete_after
+ def test_delete_badaction(self):
+ self.stack = parser.Stack(self.ctx, 'delete_badaction_test',
+ parser.Template({}))
+ stack_id = self.stack.store()
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertNotEqual(db_s, None)
+
+ self.stack.delete(action="wibble")
+
+ db_s = db_api.stack_get(self.ctx, stack_id)
+ self.assertNotEqual(db_s, None)
+ self.assertEqual(self.stack.state, self.stack.DELETE_FAILED)
parameters = parser.Parameters(stack_name, template, params)
ctx = context.get_admin_context()
ctx.tenant_id = 'test_tenant'
- stack = parser.Stack(ctx, stack_name, template, parameters)
+ stack = parser.Stack(ctx, stack_name, template, parameters,
+ disable_rollback=True)
self.stack_id = stack.store()
parameters = parser.Parameters(stack_name, template, params)
ctx = context.get_admin_context()
ctx.tenant_id = 'test_tenant'
- stack = parser.Stack(ctx, stack_name, template, parameters)
+ stack = parser.Stack(ctx, stack_name, template, parameters,
+ disable_rollback=True)
# Stub out the UUID for this test, so we can get an expected signature
self.m.StubOutWithMock(uuid, 'uuid4')
uuid.uuid4().AndReturn('STACKABCD1234')