heat/heat/engine/resources/stack_resource.py

687 lines
27 KiB
Python

#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import weakref
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import reflection
from heat.common import exception
from heat.common.i18n import _
from heat.common import identifier
from heat.common import template_format
from heat.engine import attributes
from heat.engine import environment
from heat.engine import resource
from heat.engine import scheduler
from heat.engine import stack as parser
from heat.engine import stk_defn
from heat.engine import template
from heat.objects import raw_template
from heat.objects import stack as stack_object
from heat.objects import stack_lock
from heat.rpc import api as rpc_api
LOG = logging.getLogger(__name__)
class StackResource(resource.Resource):
"""Allows entire stack to be managed as a resource in a parent stack.
An abstract Resource subclass that allows the management of an entire Stack
as a resource in a parent stack.
"""
# Assume True as this is evaluated before the stack is created
# so there is no way to know for sure without subclass-specific
# template parsing.
requires_deferred_auth = True
def __init__(self, name, json_snippet, stack):
super(StackResource, self).__init__(name, json_snippet, stack)
self._nested = None
self._outputs = None
self.resource_info = None
def validate(self):
super(StackResource, self).validate()
# Don't redo a non-strict validation of a nested stack during the
# creation of a child stack; only validate a child stack prior to the
# creation of the root stack.
if self.stack.nested_depth == 0 or not self.stack.strict_validate:
self.validate_nested_stack()
def validate_nested_stack(self):
try:
name = "%s-%s" % (self.stack.name, self.name)
nested_stack = self._parse_nested_stack(
name,
self.child_template(),
self.child_params())
nested_stack.strict_validate = False
nested_stack.validate()
except AssertionError:
raise
except Exception as ex:
path = "%s<%s>" % (self.name, self.template_url)
raise exception.StackValidationFailed(
error=ex, path=[self.stack.t.RESOURCES, path])
@property
def template_url(self):
"""Template url for the stack resource.
When stack resource is a TemplateResource, it's the template
location. For group resources like ResourceGroup where the
template is constructed dynamically, it's just a placeholder.
"""
return "nested_stack"
def _outputs_to_attribs(self, json_snippet):
outputs = json_snippet.get('Outputs')
if not self.attributes and outputs:
self.attributes_schema = (
attributes.Attributes.schema_from_outputs(outputs))
# Note: it can be updated too and for show return dictionary
# with all available outputs
self.attributes = attributes.Attributes(
self.name, self.attributes_schema,
self._make_resolver(weakref.ref(self)))
def _needs_update(self, after, before, after_props, before_props,
prev_resource, check_init_complete=True):
# If the nested stack has not been created, use the default
# implementation to determine if we need to replace the resource. Note
# that we do *not* return the result.
if self.resource_id is None:
super(StackResource, self)._needs_update(after, before,
after_props, before_props,
prev_resource,
check_init_complete)
else:
if self.state == (self.CHECK, self.FAILED):
nested_stack = self.rpc_client().show_stack(
self.context, self.nested_identifier())[0]
nested_stack_state = (nested_stack[rpc_api.STACK_ACTION],
nested_stack[rpc_api.STACK_STATUS])
if nested_stack_state == (self.stack.CHECK, self.stack.FAILED):
# The stack-check action marked the stack resource
# CHECK_FAILED, so return True to allow the individual
# CHECK_FAILED resources decide if they need updating.
return True
# The mark-unhealthy action marked the stack resource
# CHECK_FAILED, so raise UpdateReplace to replace the
# entire failed stack.
raise resource.UpdateReplace(self)
# Always issue an update to the nested stack and let the individual
# resources in it decide if they need updating.
return True
def nested_identifier(self):
if self.resource_id is None:
return None
return identifier.HeatIdentifier(
self.context.tenant_id,
self.physical_resource_name(),
self.resource_id)
def has_nested(self):
"""Return True if the resource has an existing nested stack."""
return self.resource_id is not None or self._nested is not None
def nested(self):
"""Return a Stack object representing the nested (child) stack.
If we catch NotFound exception when loading, return None.
"""
if self._nested is None and self.resource_id is not None:
try:
self._nested = parser.Stack.load(self.context,
self.resource_id)
except exception.NotFound:
return None
return self._nested
def child_template(self):
"""Default implementation to get the child template.
Resources that inherit from StackResource should override this method
with specific details about the template used by them.
"""
raise NotImplementedError()
def child_params(self):
"""Default implementation to get the child params.
Resources that inherit from StackResource should override this method
with specific details about the parameters used by them.
"""
raise NotImplementedError()
def preview(self):
"""Preview a StackResource as resources within a Stack.
This method overrides the original Resource.preview to return a preview
of all the resources contained in this Stack. For this to be possible,
the specific resources need to override both ``child_template`` and
``child_params`` with specific information to allow the stack to be
parsed correctly. If any of these methods is missing, the entire
StackResource will be returned as if it were a regular Resource.
"""
try:
child_template = self.child_template()
params = self.child_params()
except NotImplementedError:
class_name = reflection.get_class_name(self, fully_qualified=False)
LOG.warning("Preview of '%s' not yet implemented", class_name)
return self
name = "%s-%s" % (self.stack.name, self.name)
self._nested = self._parse_nested_stack(name, child_template, params)
return self.nested().preview_resources()
def get_nested_parameters_stack(self):
"""Return a stack for schema validation.
This returns a stack to be introspected for building parameters schema.
It can be customized by subclass to return a restricted version of what
will be running.
"""
try:
child_template = self.child_template()
params = self.child_params()
except NotImplementedError:
class_name = reflection.get_class_name(self, fully_qualified=False)
LOG.warning("Nested parameters of '%s' not yet "
"implemented", class_name)
return
name = "%s-%s" % (self.stack.name, self.name)
return self._parse_nested_stack(name, child_template, params)
def _parse_child_template(self, child_template, child_env):
parsed_child_template = child_template
if isinstance(parsed_child_template, template.Template):
parsed_child_template = parsed_child_template.t
return template.Template(parsed_child_template,
files=self.child_template_files(child_env),
env=child_env)
def child_template_files(self, child_env):
"""Default implementation to get the files map for child template."""
return self.stack.t.files
def _parse_nested_stack(self, stack_name, child_template,
child_params, timeout_mins=None,
adopt_data=None):
if timeout_mins is None:
timeout_mins = self.stack.timeout_mins
stack_user_project_id = self.stack.stack_user_project_id
new_nested_depth = self._child_nested_depth()
child_env = environment.get_child_environment(
self.stack.env, child_params,
child_resource_name=self.name,
item_to_remove=self.resource_info)
parsed_template = self._child_parsed_template(child_template,
child_env)
self._validate_nested_resources(parsed_template)
# Note we disable rollback for nested stacks, since they
# should be rolled back by the parent stack on failure
nested = parser.Stack(self.context,
stack_name,
parsed_template,
timeout_mins=timeout_mins,
disable_rollback=True,
parent_resource=self.name,
owner_id=self.stack.id,
user_creds_id=self.stack.user_creds_id,
stack_user_project_id=stack_user_project_id,
adopt_stack_data=adopt_data,
nested_depth=new_nested_depth)
nested.set_parent_stack(self.stack)
return nested
def _child_nested_depth(self):
if self.stack.nested_depth >= cfg.CONF.max_nested_stack_depth:
msg = _("Recursion depth exceeds %d."
) % cfg.CONF.max_nested_stack_depth
raise exception.RequestLimitExceeded(message=msg)
return self.stack.nested_depth + 1
def _child_parsed_template(self, child_template, child_env):
parsed_template = self._parse_child_template(child_template, child_env)
# Don't overwrite the attributes_schema for subclasses that
# define their own attributes_schema.
if not hasattr(type(self), 'attributes_schema'):
self.attributes = None
self._outputs_to_attribs(parsed_template)
return parsed_template
def _validate_nested_resources(self, templ):
if cfg.CONF.max_resources_per_stack == -1:
return
total_resources = (len(templ[templ.RESOURCES]) +
self.stack.total_resources(self.root_stack_id))
identity = self.nested_identifier()
if identity is not None:
existing = self.rpc_client().list_stack_resources(self.context,
identity)
# Don't double-count existing resources during an update
total_resources -= len(existing)
if (total_resources > cfg.CONF.max_resources_per_stack):
message = exception.StackResourceLimitExceeded.msg_fmt
raise exception.RequestLimitExceeded(message=message)
def create_with_template(self, child_template, user_params=None,
timeout_mins=None, adopt_data=None):
"""Create the nested stack with the given template."""
name = self.physical_resource_name()
if timeout_mins is None:
timeout_mins = self.stack.timeout_mins
stack_user_project_id = self.stack.stack_user_project_id
kwargs = self._stack_kwargs(user_params, child_template, adopt_data)
adopt_data_str = None
if adopt_data is not None:
if 'environment' not in adopt_data:
adopt_data['environment'] = kwargs['params']
if 'template' not in adopt_data:
if isinstance(child_template, template.Template):
adopt_data['template'] = child_template.t
else:
adopt_data['template'] = child_template
adopt_data_str = json.dumps(adopt_data)
args = {rpc_api.PARAM_TIMEOUT: timeout_mins,
rpc_api.PARAM_DISABLE_ROLLBACK: True,
rpc_api.PARAM_ADOPT_STACK_DATA: adopt_data_str}
kwargs.update({
'stack_name': name,
'args': args,
'environment_files': None,
'owner_id': self.stack.id,
'user_creds_id': self.stack.user_creds_id,
'stack_user_project_id': stack_user_project_id,
'nested_depth': self._child_nested_depth(),
'parent_resource_name': self.name
})
with self.translate_remote_exceptions:
try:
result = self.rpc_client()._create_stack(self.context,
**kwargs)
except exception.HeatException:
with excutils.save_and_reraise_exception():
if adopt_data is None:
raw_template.RawTemplate.delete(self.context,
kwargs['template_id'])
self.resource_id_set(result['stack_id'])
def child_definition(self, child_template=None, user_params=None,
nested_identifier=None):
if user_params is None:
user_params = self.child_params()
if child_template is None:
child_template = self.child_template()
if nested_identifier is None:
nested_identifier = self.nested_identifier()
child_env = environment.get_child_environment(
self.stack.env,
user_params,
child_resource_name=self.name,
item_to_remove=self.resource_info)
parsed_template = self._child_parsed_template(child_template,
child_env)
return stk_defn.StackDefinition(self.context, parsed_template,
nested_identifier,
None)
def _stack_kwargs(self, user_params, child_template, adopt_data=None):
defn = self.child_definition(child_template, user_params)
parsed_template = defn.t
if adopt_data is None:
template_id = parsed_template.store(self.context)
return {
'template_id': template_id,
'template': None,
'params': None,
'files': None,
}
else:
return {
'template': parsed_template.t,
'params': defn.env.user_env_as_dict(),
'files': parsed_template.files,
}
@excutils.exception_filter
def translate_remote_exceptions(self, ex):
if (isinstance(ex, exception.ActionInProgress) and
self.stack.action == self.stack.ROLLBACK):
# The update was interrupted and the rollback is already in
# progress, so just ignore the error and wait for the rollback to
# finish
return True
class_name = reflection.get_class_name(ex, fully_qualified=False)
if not class_name.endswith('_Remote'):
return False
full_message = str(ex)
if full_message.find('\n') > -1:
message, msg_trace = full_message.split('\n', 1)
else:
message = full_message
raise exception.ResourceFailure(message, self, action=self.action)
def check_create_complete(self, cookie=None):
return self._check_status_complete(self.CREATE)
def _check_status_complete(self, expected_action, cookie=None):
try:
data = stack_object.Stack.get_status(self.context,
self.resource_id)
except exception.NotFound:
if expected_action == self.DELETE:
return True
# It's possible the engine handling the create hasn't persisted
# the stack to the DB when we first start polling for state
return False
action, status, status_reason, updated_time = data
if action != expected_action:
return False
if status == self.IN_PROGRESS:
if cookie is not None and 'fail_count' in cookie:
prev_status_reason = cookie['previous']['status_reason']
if status_reason != prev_status_reason:
# State has changed, so fail on the next failure
cookie['fail_count'] = 1
return False
elif status == self.COMPLETE:
# For operations where we do not take a resource lock
# (i.e. legacy-style), check that the stack lock has been
# released before reporting completeness.
done = (self._should_lock_on_action(expected_action) or
stack_lock.StackLock.get_engine_id(
self.context, self.resource_id) is None)
if done:
# Reset nested, to indicate we changed status
self._nested = None
return done
elif status == self.FAILED:
if cookie is not None and 'fail_count' in cookie:
cookie['fail_count'] -= 1
if cookie['fail_count'] > 0:
raise resource.PollDelay(10)
raise exception.ResourceFailure(status_reason, self,
action=action)
else:
raise exception.ResourceUnknownStatus(
resource_status=status,
status_reason=status_reason,
result=_('Stack unknown status'))
def check_adopt_complete(self, cookie=None):
return self._check_status_complete(self.ADOPT)
def _try_rollback(self):
stack_identity = self.nested_identifier()
if stack_identity is None:
return False
try:
self.rpc_client().stack_cancel_update(
self.context,
dict(stack_identity),
cancel_with_rollback=True)
except exception.NotSupported:
return False
try:
data = stack_object.Stack.get_status(self.context,
self.resource_id)
except exception.NotFound:
return False
action, status, status_reason, updated_time = data
# If nested stack is still in progress, it should eventually roll
# itself back due to stack_cancel_update(), so we just need to wait
# for that to complete
return status == self.stack.IN_PROGRESS
def update_with_template(self, child_template, user_params=None,
timeout_mins=None):
"""Update the nested stack with the new template."""
if self.id is None:
self.store()
if self.stack.action == self.stack.ROLLBACK:
if self._try_rollback():
LOG.info('Triggered nested stack %s rollback',
self.physical_resource_name())
return {'target_action': self.stack.ROLLBACK}
if self.resource_id is None:
# if the create failed for some reason and the nested
# stack was not created, we need to create an empty stack
# here so that the update will work.
def _check_for_completion():
while not self.check_create_complete():
yield
empty_temp = template_format.parse(
"heat_template_version: '2013-05-23'")
self.create_with_template(empty_temp, {})
checker = scheduler.TaskRunner(_check_for_completion)
checker(timeout=self.stack.timeout_secs())
if timeout_mins is None:
timeout_mins = self.stack.timeout_mins
try:
status_data = stack_object.Stack.get_status(self.context,
self.resource_id)
except exception.NotFound:
raise resource.UpdateReplace(self)
action, status, status_reason, updated_time = status_data
kwargs = self._stack_kwargs(user_params, child_template)
kwargs.update({
'stack_identity': dict(self.nested_identifier()),
'args': {rpc_api.PARAM_TIMEOUT: timeout_mins,
rpc_api.PARAM_CONVERGE: self.converge}
})
with self.translate_remote_exceptions:
try:
self.rpc_client()._update_stack(self.context, **kwargs)
except exception.HeatException:
with excutils.save_and_reraise_exception():
raw_template.RawTemplate.delete(self.context,
kwargs['template_id'])
def check_update_complete(self, cookie=None):
if cookie is not None and 'target_action' in cookie:
target_action = cookie['target_action']
cookie = None
else:
target_action = self.stack.UPDATE
return self._check_status_complete(target_action,
cookie=cookie)
def _handle_cancel(self):
stack_identity = self.nested_identifier()
if stack_identity is not None:
LOG.debug('Cancelling %s of %s' % (self.action, self))
try:
self.rpc_client().stack_cancel_update(
self.context,
dict(stack_identity),
cancel_with_rollback=False)
except exception.NotSupported:
LOG.debug('Nested stack %s not in cancellable state',
stack_identity.stack_name)
def handle_preempt(self):
self._handle_cancel()
def handle_update_cancel(self, cookie):
self._handle_cancel()
def handle_create_cancel(self, cookie):
return self.handle_update_cancel(cookie)
def delete_nested(self):
"""Delete the nested stack."""
stack_identity = self.nested_identifier()
if stack_identity is None:
return
cookie = None
if not self.stack.convergence:
try:
status_data = stack_object.Stack.get_status(self.context,
self.resource_id)
except exception.NotFound:
return
action, status, status_reason, updated_time = status_data
if (action, status) == (self.stack.DELETE,
self.stack.IN_PROGRESS):
cookie = {
'previous': {
'state': (action, status),
'status_reason': status_reason,
'updated_at': None,
},
'fail_count': 2,
}
with self.rpc_client().ignore_error_by_name('EntityNotFound'):
if self.abandon_in_progress:
self.rpc_client().abandon_stack(self.context, stack_identity)
else:
self.rpc_client().delete_stack(self.context, stack_identity,
cast=False)
return cookie
def handle_delete(self):
return self.delete_nested()
def check_delete_complete(self, cookie=None):
return self._check_status_complete(self.DELETE)
def handle_suspend(self):
stack_identity = self.nested_identifier()
if stack_identity is None:
raise exception.Error(_('Cannot suspend %s, stack not created')
% self.name)
self.rpc_client().stack_suspend(self.context, dict(stack_identity))
def check_suspend_complete(self, cookie=None):
return self._check_status_complete(self.SUSPEND)
def handle_resume(self):
stack_identity = self.nested_identifier()
if stack_identity is None:
raise exception.Error(_('Cannot resume %s, stack not created')
% self.name)
self.rpc_client().stack_resume(self.context, dict(stack_identity))
def check_resume_complete(self, cookie=None):
return self._check_status_complete(self.RESUME)
def handle_check(self):
stack_identity = self.nested_identifier()
if stack_identity is None:
raise exception.Error(_('Cannot check %s, stack not created')
% self.name)
self.rpc_client().stack_check(self.context, dict(stack_identity))
def check_check_complete(self, cookie=None):
return self._check_status_complete(self.CHECK)
def prepare_abandon(self):
self.abandon_in_progress = True
nested_stack = self.nested()
if nested_stack:
return nested_stack.prepare_abandon()
return {}
def get_output(self, op):
"""Return the specified Output value from the nested stack.
If the output key does not exist, raise a NotFound exception.
"""
if (self._outputs is None or
(op in self._outputs and
rpc_api.OUTPUT_ERROR not in self._outputs[op] and
self._outputs[op].get(rpc_api.OUTPUT_VALUE) is None)):
stack_identity = self.nested_identifier()
if stack_identity is None:
return
stack = self.rpc_client().show_stack(self.context,
dict(stack_identity))
if not stack:
return
outputs = stack[0].get(rpc_api.STACK_OUTPUTS) or {}
self._outputs = {o[rpc_api.OUTPUT_KEY]: o for o in outputs}
if op not in self._outputs:
raise exception.NotFound(_('Specified output key %s not '
'found.') % op)
output_data = self._outputs[op]
if rpc_api.OUTPUT_ERROR in output_data:
raise exception.TemplateOutputError(
resource=self.name,
attribute=op,
message=output_data[rpc_api.OUTPUT_ERROR])
return output_data[rpc_api.OUTPUT_VALUE]
def _resolve_attribute(self, name):
try:
return self.get_output(name)
except exception.NotFound:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=name)