diff --git a/bin/heat b/bin/heat index 8273d61c16..5442f8a440 100755 --- a/bin/heat +++ b/bin/heat @@ -137,6 +137,8 @@ def stack_create(options, arguments): parameters['Parameters.member.%d.ParameterValue' % count] = v count = count + 1 + parameters['Timeout'] = options.timeout + if options.template_file: parameters['TemplateBody'] = open(options.template_file).read() elif options.template_url: @@ -367,8 +369,11 @@ def create_options(parser): parser.add_option('-u', '--template-url', metavar="template_url", default=None, help="URL of template. Default: None") - parser.add_option('-t', '--template-file', metavar="template_file", + parser.add_option('-f', '--template-file', metavar="template_file", default=None, help="Path to the template. Default: None") + parser.add_option('-t', '--timeout', metavar="timeout", + default='60', + help='Stack creation timeout in minutes. Default: 60') parser.add_option('-P', '--parameters', metavar="parameters", default=None, help="Parameter values used to create the stack.") diff --git a/docs/man/man1/heat.1 b/docs/man/man1/heat.1 index 4eaeca37fc..727b428fcb 100644 --- a/docs/man/man1/heat.1 +++ b/docs/man/man1/heat.1 @@ -34,7 +34,7 @@ heat command\&. .RE .SH "OPTIONS" .PP -\fB\-t\fR, \fB\-\-template\fR +\fB\-f\fR, \fB\-\-template\fR .RS 4 The template to use set up a stack\&. .RE diff --git a/heat/api/v1/stacks.py b/heat/api/v1/stacks.py index 8bc32b0946..06eeabb815 100644 --- a/heat/api/v1/stacks.py +++ b/heat/api/v1/stacks.py @@ -138,6 +138,8 @@ class StackController(object): msg = _("The Template must be a JSON document.") return webob.exc.HTTPBadRequest(explanation=msg) stack['StackName'] = req.params['StackName'] + if 'Timeout' in req.params: + stack['Timeout'] = req.params['Timeout'] try: return rpc.call(con, 'engine', diff --git a/heat/cloudformations.py b/heat/cloudformations.py index 2bb00e03d4..c6ef264455 100644 --- a/heat/cloudformations.py +++ b/heat/cloudformations.py @@ -16,4 +16,4 @@ SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'TemplateUrl', 'NotificationARNs', 'Parameters', 'Version', 'SignatureVersion', 'Timestamp', 'AWSAccessKeyId', - 'Signature', 'KeyStoneCreds') + 'Signature', 'KeyStoneCreds', 'Timeout') diff --git a/heat/engine/manager.py b/heat/engine/manager.py index 00f04f7b5c..e220a0fd86 100644 --- a/heat/engine/manager.py +++ b/heat/engine/manager.py @@ -115,11 +115,12 @@ class EngineManager(manager.Manager): mem['updated_at'] = str(s.updated_at) mem['NotificationARNs'] = 'TODO' mem['Parameters'] = ps.t['Parameters'] - mem['StackStatusReason'] = 'TODO' - mem['TimeoutInMinutes'] = 'TODO' + mem['TimeoutInMinutes'] = ps.t.get('Timeout', '60') mem['TemplateDescription'] = ps.t.get('Description', 'No description') mem['StackStatus'] = ps.t.get('stack_status', 'unknown') + mem['StackStatusReason'] = ps.t.get('stack_status_reason', + 'State changed') # only show the outputs on a completely created stack if ps.t['stack_status'] == ps.CREATE_COMPLETE: diff --git a/heat/engine/parser.py b/heat/engine/parser.py index b37a79ae9a..0041bd8ccc 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -40,6 +40,7 @@ class Stack(object): self.t = template self.maps = self.t.get('Mappings', {}) self.outputs = self.t.get('Outputs', {}) + self.timeout = self.t.get('Timeout', None) self.res = {} self.doc = None self.name = stack_name @@ -174,6 +175,7 @@ class Stack(object): def status_set(self, new_status, reason='change in resource state'): self.t['stack_status'] = new_status + self.t['stack_status_reason'] = reason self.update_parsed_template() def create_blocking(self): @@ -181,28 +183,51 @@ class Stack(object): create all the resources in the order specified by get_create_order ''' order = self.get_create_order() - failed = False - self.status_set(self.IN_PROGRESS) + self.status_set(self.IN_PROGRESS, 'Stack creation started') - for r in order: - res = self.resources[r] - if not failed: - try: - res.create() - except Exception as ex: - logger.exception('create') - failed = True - res.state_set(res.CREATE_FAILED, str(ex)) + stack_status = self.CREATE_COMPLETE + reason = 'Stack successfully created' - try: - self.update_parsed_template() - except Exception as ex: - logger.exception('update_parsed_template') + # Timeout is in minutes (default to 1 hour) + secs_tmo = 60 * 60 + if self.timeout: + try: + secs_tmo = int(self.timeout) * 60 + except ValueError as ve: + logger.exception('create timeout conversion') + tmo = eventlet.Timeout(secs_tmo) + try: + for r in order: + res = self.resources[r] + if stack_status != self.CREATE_FAILED: + try: + res.create() + except Exception as ex: + logger.exception('create') + stack_status = self.CREATE_FAILED + reason = 'resource %s failed with: %s' % (res.name, + str(ex)) + res.state_set(res.CREATE_FAILED, str(ex)) + try: + self.update_parsed_template() + except Exception as ex: + logger.exception('update_parsed_template') + + else: + res.state_set(res.CREATE_FAILED) + + except eventlet.Timeout, t: + if t is not tmo: + # not my timeout + raise else: - res.state_set(res.CREATE_FAILED) + stack_status = self.CREATE_FAILED + reason = 'Timed out waiting for %s' % (res.name) + finally: + tmo.cancel() - self.status_set(failed and self.CREATE_FAILED or self.CREATE_COMPLETE) + self.status_set(stack_status, reason) def create(self): @@ -222,12 +247,20 @@ class Stack(object): res = self.resources[r] try: res.delete() - re = db_api.resource_get(self.context, self.resources[r].id) - re.delete() except Exception as ex: failed = True res.state_set(res.DELETE_FAILED) logger.error('delete: %s' % str(ex)) + try: + re = db_api.resource_get(self.context, self.resources[r].id) + re.delete() + except Exception as ex: + # don't fail the delete if the db entry has + # not been created yet. + if 'not found' not in str(ex): + failed = True + res.state_set(res.DELETE_FAILED) + logger.error('delete: %s' % str(ex)) self.status_set(failed and self.DELETE_FAILED or self.DELETE_COMPLETE) if not failed: