34
34
from heat.openstack.common import log as logging
35
35
from heat.openstack.common.gettextutils import _
37
from heat.common.exception import ServerError
38
37
from heat.common.exception import StackValidationFailed
40
39
logger = logging.getLogger(__name__)
51
50
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
52
51
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
54
created_time = timestamp.Timestamp(db_api.stack_get, 'created_at')
55
updated_time = timestamp.Timestamp(db_api.stack_get, 'updated_at')
53
created_time = timestamp.Timestamp(functools.partial(db_api.stack_get,
56
updated_time = timestamp.Timestamp(functools.partial(db_api.stack_get,
59
62
def __init__(self, context, stack_name, tmpl, env=None,
60
63
stack_id=None, action=None, status=None,
61
64
status_reason='', timeout_mins=60, resolve_data=True,
62
disable_rollback=True, parent_resource=None):
65
disable_rollback=True, parent_resource=None, owner_id=None):
64
67
Initialise from a context, name, Template object and (optionally)
65
68
Environment object. The database ID may also be initialised, if the
66
69
stack is already in the database.
69
if re.match("[a-zA-Z][a-zA-Z0-9_.-]*$", stack_name) is None:
70
raise ValueError(_('Invalid stack name %s'
71
' must contain only alphanumeric or '
72
'\"_-.\" characters, must start with alpha'
73
if re.match("[a-zA-Z][a-zA-Z0-9_.-]*$", stack_name) is None:
74
raise ValueError(_('Invalid stack name %s'
75
' must contain only alphanumeric or '
76
'\"_-.\" characters, must start with alpha'
80
self.owner_id = owner_id
76
81
self.context = context
77
82
self.clients = Clients(context)
131
136
def load(cls, context, stack_id=None, stack=None, resolve_data=True,
132
parent_resource=None):
137
parent_resource=None, show_deleted=True):
133
138
'''Retrieve a Stack from the database.'''
134
139
if stack is None:
135
stack = db_api.stack_get(context, stack_id)
140
stack = db_api.stack_get(context, stack_id,
141
show_deleted=show_deleted)
136
142
if stack is None:
137
143
message = 'No stack exists with id "%s"' % str(stack_id)
138
144
raise exception.NotFound(message)
142
148
stack = cls(context, stack.name, template, env,
143
149
stack.id, stack.action, stack.status, stack.status_reason,
144
150
stack.timeout, resolve_data, stack.disable_rollback,
151
parent_resource, owner_id=stack.owner_id)
149
def store(self, owner=None):
155
def store(self, backup=False):
151
157
Store the stack in the database and return its ID
152
158
If self.id is set, we update the existing stack
154
new_creds = db_api.user_creds_create(self.context)
162
'name': self._backup_name() if backup else self.name,
158
163
'raw_template_id': self.t.store(self.context),
159
164
'parameters': self.env.user_env_as_dict(),
160
'owner_id': owner and owner.id,
161
'user_creds_id': new_creds.id,
165
'owner_id': self.owner_id,
162
166
'username': self.context.username,
163
167
'tenant': self.context.tenant_id,
164
168
'action': self.action,
171
175
db_api.stack_update(self.context, self.id, s)
177
new_creds = db_api.user_creds_create(self.context)
178
s['user_creds_id'] = new_creds.id
173
179
new_s = db_api.stack_create(self.context, s)
174
180
self.id = new_s.id
328
337
def resource_action(r):
329
338
# Find e.g resource.create and call it
330
339
action_l = action.lower()
331
handle = getattr(r, '%s' % action_l, None)
335
raise exception.ResourceFailure(
336
AttributeError(_('Resource action %s not found') %
340
handle = getattr(r, '%s' % action_l)
339
344
action_task = scheduler.DependencyTaskGroup(self.dependencies,
354
359
if callable(post_func):
357
def update(self, newstack, action=UPDATE):
362
def _backup_stack(self, create_if_missing=True):
364
Get a Stack containing any in-progress resources from the previous
365
stack state prior to an update.
367
s = db_api.stack_get_by_name(self.context, self._backup_name(),
370
logger.debug('Loaded existing backup stack')
371
return self.load(self.context, stack=s)
372
elif create_if_missing:
373
prev = type(self)(self.context, self.name, self.t, self.env,
375
prev.store(backup=True)
376
logger.debug('Created new backup stack')
381
def update(self, newstack):
359
383
Compare the current stack with newstack,
360
384
and where necessary create/update/delete the resources until
366
390
Update will fail if it exceeds the specified timeout. The default is
367
391
60 minutes, set in the constructor
393
updater = scheduler.TaskRunner(self.update_task, newstack)
396
@scheduler.wrappertask
397
def update_task(self, newstack, action=UPDATE):
369
398
if action not in (self.UPDATE, self.ROLLBACK):
370
399
logger.error("Unexpected action %s passed to update!" % action)
371
400
self.state_set(self.UPDATE, self.FAILED,
381
410
'State invalid for %s' % action)
384
current_env = self.env
385
self.env = newstack.env
386
self.parameters = newstack.parameters
388
413
self.state_set(self.UPDATE, self.IN_PROGRESS,
389
414
'Stack %s started' % action)
416
oldstack = Stack(self.context, self.name, self.t, self.env)
417
backup_stack = self._backup_stack()
392
update_task = update.StackUpdate(self, newstack)
420
update_task = update.StackUpdate(self, newstack, backup_stack,
421
rollback=action == self.ROLLBACK)
393
422
updater = scheduler.TaskRunner(update_task)
424
self.env = newstack.env
425
self.parameters = newstack.parameters
395
updater(timeout=self.timeout_secs())
428
updater.start(timeout=self.timeout_secs())
430
while not updater.step():
397
433
cur_deps = self._get_dependencies(self.resources.itervalues())
398
434
self.dependencies = cur_deps
414
450
# If rollback is enabled, we do another update, with the
415
451
# existing template, so we roll back to the original state
416
452
if not self.disable_rollback:
417
oldstack = Stack(self.context, self.name, self.t,
419
self.update(oldstack, action=self.ROLLBACK)
453
yield self.update_task(oldstack, action=self.ROLLBACK)
456
logger.debug('Deleting backup stack')
457
backup_stack.delete()
422
459
self.state_set(action, stack_status, reason)
444
481
"Invalid action %s" % action)
484
stack_status = self.COMPLETE
485
reason = 'Stack %s completed successfully' % action.lower()
447
486
self.state_set(action, self.IN_PROGRESS, 'Stack %s started' % action)
450
for res in reversed(self):
453
except exception.ResourceFailure as ex:
454
logger.error('Failed to delete %s error: %s' % (str(res),
456
failures.append(str(res))
459
self.state_set(action, self.FAILED,
460
'Failed to %s : %s' % (action, ', '.join(failures)))
462
self.state_set(action, self.COMPLETE, '%s completed' % action)
488
backup_stack = self._backup_stack(False)
489
if backup_stack is not None:
490
backup_stack.delete()
491
if backup_stack.status != backup_stack.COMPLETE:
492
errs = backup_stack.status_reason
493
failure = 'Error deleting backup resources: %s' % errs
494
self.state_set(action, self.FAILED,
495
'Failed to %s : %s' % (action, failure))
498
action_task = scheduler.DependencyTaskGroup(self.dependencies,
499
resource.Resource.destroy,
502
scheduler.TaskRunner(action_task)(timeout=self.timeout_secs())
503
except exception.ResourceFailure as ex:
504
stack_status = self.FAILED
505
reason = 'Resource %s failed: %s' % (action.lower(), str(ex))
506
except scheduler.Timeout:
507
stack_status = self.FAILED
508
reason = '%s timed out' % action.title()
510
self.state_set(action, stack_status, reason)
511
if stack_status != self.FAILED:
463
512
db_api.stack_delete(self.context, self.id)