StackResource convert operations to use RPC
Modifies create/update/delete to use an RPC call, so it can potentially be handled by a different engine in a scaled-out heat deployment with multiple heat-engine processes or workers. The current introspection interface via nested() is maintained, so we minimize the impact on existing StackResource subclasses. A future optimisation would be to remove the DB polling from the check_create_complete and instead rely on an RPC notification from the engine handling the create when it's done. Remove test_{create,update}_with_template_validates as this is the normal behaviour of stack create/update. Change-Id: I7b88a329ddb5df4005b0bc10810bc5e3cdd077ab blueprint: decouple-nested Co-Authored-by: Angus Salkeld <asalkeld@mirantis.com> Co-Authored-by: Steven Hardy <shardy@redhat.com>
This commit is contained in:
parent
e1aa606741
commit
2c2ae11456
|
@ -11,7 +11,9 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
@ -20,8 +22,8 @@ import six
|
|||
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common.i18n import _LI
|
||||
from heat.common.i18n import _LW
|
||||
from heat.common import identifier
|
||||
from heat.common import template_format
|
||||
from heat.engine import attributes
|
||||
from heat.engine import environment
|
||||
|
@ -29,8 +31,7 @@ from heat.engine import resource
|
|||
from heat.engine import scheduler
|
||||
from heat.engine import stack as parser
|
||||
from heat.engine import template
|
||||
|
||||
cfg.CONF.import_opt('error_wait_time', 'heat.common.config')
|
||||
from heat.rpc import api as rpc_api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -155,34 +156,20 @@ class StackResource(resource.Resource):
|
|||
files=self.stack.t.files, env=child_env)
|
||||
|
||||
def _parse_nested_stack(self, stack_name, child_template,
|
||||
child_params=None, timeout_mins=None,
|
||||
child_params, timeout_mins=None,
|
||||
adopt_data=None):
|
||||
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)
|
||||
if timeout_mins is None:
|
||||
timeout_mins = self.stack.timeout_mins
|
||||
|
||||
if child_params is None:
|
||||
child_params = self.child_params()
|
||||
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,
|
||||
item_to_remove=self.resource_info)
|
||||
|
||||
parsed_template = self._parse_child_template(child_template, child_env)
|
||||
self._validate_nested_resources(parsed_template)
|
||||
|
||||
# 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)
|
||||
|
||||
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.stack.nested_depth + 1
|
||||
parsed_template = self._child_parsed_template(child_template,
|
||||
child_env)
|
||||
|
||||
# Note we disable rollback for nested stacks, since they
|
||||
# should be rolled back by the parent stack on failure
|
||||
|
@ -199,6 +186,24 @@ class StackResource(resource.Resource):
|
|||
nested_depth=new_nested_depth)
|
||||
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)
|
||||
self._validate_nested_resources(parsed_template)
|
||||
|
||||
# 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):
|
||||
total_resources = (len(templ[templ.RESOURCES]) +
|
||||
self.stack.root_stack.total_resources())
|
||||
|
@ -215,156 +220,226 @@ class StackResource(resource.Resource):
|
|||
timeout_mins=None, adopt_data=None):
|
||||
"""Create the nested stack with the given template."""
|
||||
name = self.physical_resource_name()
|
||||
self._nested = self._parse_nested_stack(name, child_template,
|
||||
user_params, timeout_mins,
|
||||
adopt_data)
|
||||
self._nested.validate()
|
||||
nested_id = self._nested.store()
|
||||
self.resource_id_set(nested_id)
|
||||
if timeout_mins is None:
|
||||
timeout_mins = self.stack.timeout_mins
|
||||
stack_user_project_id = self.stack.stack_user_project_id
|
||||
|
||||
action = self._nested.CREATE
|
||||
error_wait_time = cfg.CONF.error_wait_time
|
||||
if adopt_data:
|
||||
action = self._nested.ADOPT
|
||||
error_wait_time = None
|
||||
if user_params is None:
|
||||
user_params = self.child_params()
|
||||
child_env = environment.get_child_environment(
|
||||
self.stack.env,
|
||||
user_params,
|
||||
item_to_remove=self.resource_info)
|
||||
|
||||
stack_creator = scheduler.TaskRunner(self._nested.stack_task,
|
||||
action=action,
|
||||
error_wait_time=error_wait_time)
|
||||
stack_creator.start(timeout=self._nested.timeout_secs())
|
||||
return stack_creator
|
||||
new_nested_depth = self._child_nested_depth()
|
||||
parsed_template = self._child_parsed_template(child_template,
|
||||
child_env)
|
||||
|
||||
def check_create_complete(self, stack_creator):
|
||||
if stack_creator is None:
|
||||
adopt_data_str = None
|
||||
if adopt_data is not None:
|
||||
if 'environment' not in adopt_data:
|
||||
adopt_data['environment'] = child_env.user_env_as_dict()
|
||||
if 'template' not in adopt_data:
|
||||
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}
|
||||
try:
|
||||
result = self.rpc_client()._create_stack(
|
||||
self.context,
|
||||
name,
|
||||
parsed_template.t,
|
||||
child_env.user_env_as_dict(),
|
||||
parsed_template.files,
|
||||
args,
|
||||
owner_id=self.stack.id,
|
||||
user_creds_id=self.stack.user_creds_id,
|
||||
stack_user_project_id=stack_user_project_id,
|
||||
nested_depth=new_nested_depth)
|
||||
except Exception as ex:
|
||||
self.raise_local_exception(ex)
|
||||
|
||||
self.resource_id_set(result['stack_id'])
|
||||
|
||||
def raise_local_exception(self, ex):
|
||||
ex_type = ex.__class__.__name__
|
||||
|
||||
is_remote = ex_type.endswith('_Remote')
|
||||
if is_remote:
|
||||
ex_type = ex_type[:-len('_Remote')]
|
||||
|
||||
full_message = six.text_type(ex)
|
||||
if full_message.find('\n') > -1 and is_remote:
|
||||
message, msg_trace = full_message.split('\n', 1)
|
||||
else:
|
||||
message = full_message
|
||||
|
||||
if isinstance(ex, exception.HeatException):
|
||||
message = ex.message
|
||||
local_ex = copy.copy(getattr(exception, ex_type))
|
||||
local_ex.msg_fmt = "%(message)s"
|
||||
raise local_ex(message=message)
|
||||
|
||||
def check_create_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.CREATE)
|
||||
|
||||
def _check_status_complete(self, action, show_deleted=False,
|
||||
cookie=None):
|
||||
try:
|
||||
nested = self.nested(force_reload=True, show_deleted=show_deleted)
|
||||
except exception.NotFound:
|
||||
if action == resource.Resource.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
|
||||
|
||||
if nested is None:
|
||||
return True
|
||||
done = stack_creator.step()
|
||||
if done:
|
||||
if self._nested.state != (self._nested.CREATE,
|
||||
self._nested.COMPLETE):
|
||||
raise exception.Error(self._nested.status_reason)
|
||||
|
||||
return done
|
||||
# Has the action really started?
|
||||
#
|
||||
# The rpc call to update does not guarantee that the stack will be
|
||||
# placed into IN_PROGRESS by the time it returns (it runs stack.update
|
||||
# in a thread) so you could also have a situation where we get into
|
||||
# this method and the update hasn't even started.
|
||||
#
|
||||
# So we are using a mixture of state (action+status) and updated_at
|
||||
# to see if the action has actually progressed.
|
||||
# - very fast updates (like something with one RandomString) we will
|
||||
# probably miss the state change, but we should catch the updated_at.
|
||||
# - very slow updates we won't see the updated_at for quite a while,
|
||||
# but should see the state change.
|
||||
if cookie is not None:
|
||||
prev_state = cookie['previous']['state']
|
||||
prev_updated_at = cookie['previous']['updated_at']
|
||||
if (prev_updated_at == nested.updated_time and
|
||||
prev_state == nested.state):
|
||||
return False
|
||||
|
||||
def check_adopt_complete(self, stack_creator):
|
||||
if stack_creator is None:
|
||||
if nested.status == resource.Resource.IN_PROGRESS:
|
||||
return False
|
||||
elif nested.status == resource.Resource.COMPLETE:
|
||||
return True
|
||||
done = stack_creator.step()
|
||||
if done:
|
||||
if self._nested.state != (self._nested.ADOPT,
|
||||
self._nested.COMPLETE):
|
||||
raise exception.Error(self._nested.status_reason)
|
||||
elif nested.status == resource.Resource.FAILED:
|
||||
raise resource.ResourceUnknownStatus(
|
||||
resource_status=nested.status,
|
||||
status_reason=nested.status_reason)
|
||||
else:
|
||||
raise resource.ResourceUnknownStatus(
|
||||
resource_status=nested.status,
|
||||
result=_('Stack unknown status'))
|
||||
|
||||
return done
|
||||
def check_adopt_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.ADOPT)
|
||||
|
||||
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()
|
||||
name = self.physical_resource_name()
|
||||
|
||||
nested_stack = self.nested()
|
||||
if nested_stack 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(creator_fn):
|
||||
while not self.check_create_complete(creator_fn):
|
||||
yield
|
||||
|
||||
empty_temp = template_format.parse(
|
||||
"heat_template_version: '2013-05-23'")
|
||||
stack_creator = self.create_with_template(empty_temp, {})
|
||||
stack_creator.run_to_completion()
|
||||
checker = scheduler.TaskRunner(_check_for_completion,
|
||||
stack_creator)
|
||||
checker(timeout=self.stack.timeout_secs())
|
||||
|
||||
if stack_creator is not None:
|
||||
stack_creator.run_to_completion()
|
||||
nested_stack = self.nested()
|
||||
|
||||
stack = self._parse_nested_stack(name, child_template, user_params,
|
||||
timeout_mins)
|
||||
stack.validate()
|
||||
stack.parameters.set_stack_id(nested_stack.identifier())
|
||||
nested_stack.updated_time = self.updated_time
|
||||
updater = scheduler.TaskRunner(nested_stack.update_task, stack)
|
||||
updater.start()
|
||||
return updater
|
||||
if timeout_mins is None:
|
||||
timeout_mins = self.stack.timeout_mins
|
||||
|
||||
def check_update_complete(self, updater):
|
||||
if updater is not None:
|
||||
if not updater.step():
|
||||
return False
|
||||
if user_params is None:
|
||||
user_params = self.child_params()
|
||||
|
||||
nested_stack = self.nested()
|
||||
if nested_stack.state != (nested_stack.UPDATE,
|
||||
nested_stack.COMPLETE):
|
||||
raise exception.Error(_("Nested stack UPDATE failed: %s") %
|
||||
nested_stack.status_reason)
|
||||
return True
|
||||
child_env = environment.get_child_environment(
|
||||
self.stack.env,
|
||||
user_params, item_to_remove=self.resource_info)
|
||||
parsed_template = self._child_parsed_template(child_template,
|
||||
child_env)
|
||||
|
||||
cookie = {'previous': {
|
||||
'updated_at': nested_stack.updated_time,
|
||||
'state': nested_stack.state}}
|
||||
|
||||
args = {rpc_api.PARAM_TIMEOUT: timeout_mins}
|
||||
try:
|
||||
self.rpc_client().update_stack(
|
||||
self.context,
|
||||
nested_stack.identifier(),
|
||||
parsed_template.t,
|
||||
child_env.user_env_as_dict(),
|
||||
parsed_template.files,
|
||||
args)
|
||||
except Exception as ex:
|
||||
LOG.exception('update_stack')
|
||||
self.raise_local_exception(ex)
|
||||
return cookie
|
||||
|
||||
def check_update_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.UPDATE,
|
||||
cookie=cookie)
|
||||
|
||||
def delete_nested(self):
|
||||
'''
|
||||
Delete the nested stack.
|
||||
'''
|
||||
stack_identity = identifier.HeatIdentifier(
|
||||
self.context.tenant_id,
|
||||
self.physical_resource_name(),
|
||||
self.resource_id)
|
||||
|
||||
try:
|
||||
stack = self.nested()
|
||||
except exception.NotFound:
|
||||
LOG.info(_LI("Stack not found to delete"))
|
||||
else:
|
||||
if stack is not None:
|
||||
delete_task = scheduler.TaskRunner(stack.delete)
|
||||
delete_task.start()
|
||||
return delete_task
|
||||
self.rpc_client().delete_stack(self.context, stack_identity)
|
||||
except Exception as ex:
|
||||
self.rpc_client().ignore_error_named(ex, 'NotFound')
|
||||
|
||||
def check_delete_complete(self, delete_task):
|
||||
if delete_task is None:
|
||||
return True
|
||||
|
||||
done = delete_task.step()
|
||||
if done:
|
||||
nested_stack = self.nested()
|
||||
if nested_stack.state != (nested_stack.DELETE,
|
||||
nested_stack.COMPLETE):
|
||||
raise exception.Error(nested_stack.status_reason)
|
||||
|
||||
return done
|
||||
def check_delete_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.DELETE,
|
||||
show_deleted=True)
|
||||
|
||||
def handle_suspend(self):
|
||||
stack = self.nested()
|
||||
if stack is None:
|
||||
raise exception.Error(_('Cannot suspend %s, stack not created')
|
||||
% self.name)
|
||||
stack_identity = identifier.HeatIdentifier(
|
||||
self.context.tenant_id,
|
||||
self.physical_resource_name(),
|
||||
self.resource_id)
|
||||
self.rpc_client().stack_suspend(self.context, stack_identity)
|
||||
|
||||
suspend_task = scheduler.TaskRunner(self._nested.stack_task,
|
||||
action=self._nested.SUSPEND,
|
||||
reverse=True)
|
||||
|
||||
suspend_task.start(timeout=self._nested.timeout_secs())
|
||||
return suspend_task
|
||||
|
||||
def check_suspend_complete(self, suspend_task):
|
||||
done = suspend_task.step()
|
||||
if done:
|
||||
if self._nested.state != (self._nested.SUSPEND,
|
||||
self._nested.COMPLETE):
|
||||
raise exception.Error(self._nested.status_reason)
|
||||
|
||||
return done
|
||||
def check_suspend_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.SUSPEND)
|
||||
|
||||
def handle_resume(self):
|
||||
stack = self.nested()
|
||||
if stack is None:
|
||||
raise exception.Error(_('Cannot resume %s, stack not created')
|
||||
% self.name)
|
||||
stack_identity = identifier.HeatIdentifier(
|
||||
self.context.tenant_id,
|
||||
self.physical_resource_name(),
|
||||
self.resource_id)
|
||||
self.rpc_client().stack_resume(self.context, stack_identity)
|
||||
|
||||
resume_task = scheduler.TaskRunner(self._nested.stack_task,
|
||||
action=self._nested.RESUME,
|
||||
reverse=False)
|
||||
|
||||
resume_task.start(timeout=self._nested.timeout_secs())
|
||||
return resume_task
|
||||
|
||||
def check_resume_complete(self, resume_task):
|
||||
done = resume_task.step()
|
||||
if done:
|
||||
if self._nested.state != (self._nested.RESUME,
|
||||
self._nested.COMPLETE):
|
||||
raise exception.Error(self._nested.status_reason)
|
||||
|
||||
return done
|
||||
def check_resume_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.RESUME)
|
||||
|
||||
def handle_check(self):
|
||||
stack = self.nested()
|
||||
|
@ -372,15 +447,14 @@ class StackResource(resource.Resource):
|
|||
raise exception.Error(_('Cannot check %s, stack not created')
|
||||
% self.name)
|
||||
|
||||
check_task = scheduler.TaskRunner(self._nested.stack_task,
|
||||
action=self._nested.CHECK,
|
||||
aggregate_exceptions=True)
|
||||
stack_identity = identifier.HeatIdentifier(
|
||||
self.context.tenant_id,
|
||||
self.physical_resource_name(),
|
||||
self.resource_id)
|
||||
self.rpc_client().stack_check(self.context, stack_identity)
|
||||
|
||||
check_task.start(timeout=self._nested.timeout_secs())
|
||||
return check_task
|
||||
|
||||
def check_check_complete(self, check_task):
|
||||
return check_task.step()
|
||||
def check_check_complete(self, cookie=None):
|
||||
return self._check_status_complete(resource.Resource.CHECK)
|
||||
|
||||
def prepare_abandon(self):
|
||||
return self.nested().prepare_abandon()
|
||||
|
|
|
@ -19,6 +19,7 @@ import six
|
|||
import yaml
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import identifier
|
||||
from heat.common import template_format
|
||||
from heat.common import urlfetch
|
||||
from heat.engine import parser
|
||||
|
@ -307,9 +308,9 @@ Outputs:
|
|||
def setUp(self):
|
||||
super(NestedStackCrudTest, self).setUp()
|
||||
|
||||
ctx = utils.dummy_context('test_username', 'aaaa', 'password')
|
||||
self.ctx = utils.dummy_context('test_username', 'aaaa', 'password')
|
||||
empty_template = {"HeatTemplateFormatVersion": "2012-12-12"}
|
||||
stack = parser.Stack(ctx, 'test', parser.Template(empty_template))
|
||||
stack = parser.Stack(self.ctx, 'test', parser.Template(empty_template))
|
||||
stack.store()
|
||||
|
||||
self.patchobject(urlfetch, 'get', return_value=self.nested_template)
|
||||
|
@ -323,6 +324,7 @@ Outputs:
|
|||
self.res = stack_res.NestedStack('test_t_res',
|
||||
self.defn, stack)
|
||||
self.assertIsNone(self.res.validate())
|
||||
self.res._store()
|
||||
|
||||
def test_handle_create(self):
|
||||
self.res.create_with_template = mock.Mock(return_value=None)
|
||||
|
@ -350,7 +352,12 @@ Outputs:
|
|||
self.nested_parsed, self.nested_params, None)
|
||||
|
||||
def test_handle_delete(self):
|
||||
self.res.nested = mock.MagicMock()
|
||||
self.res.nested.return_value.delete.return_value = None
|
||||
self.res.rpc_client = mock.MagicMock()
|
||||
stack_identity = identifier.HeatIdentifier(
|
||||
self.ctx.tenant_id,
|
||||
self.res.physical_resource_name(),
|
||||
self.res.resource_id)
|
||||
|
||||
self.res.handle_delete()
|
||||
self.res.nested.return_value.delete.assert_called_once_with()
|
||||
self.res.rpc_client.return_value.delete_stack.assert_called_once_with(
|
||||
self.ctx, stack_identity)
|
||||
|
|
|
@ -20,6 +20,7 @@ import six
|
|||
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common import identifier
|
||||
from heat.common import template_format
|
||||
from heat.common import urlfetch
|
||||
from heat.engine import attributes
|
||||
|
@ -823,6 +824,7 @@ class TemplateResourceCrudTest(common.HeatTestCase):
|
|||
def setUp(self):
|
||||
super(TemplateResourceCrudTest, self).setUp()
|
||||
files = {'test_resource.template': json.dumps(self.provider)}
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
class DummyResource(object):
|
||||
support_status = support.SupportStatus()
|
||||
|
@ -835,7 +837,7 @@ class TemplateResourceCrudTest(common.HeatTestCase):
|
|||
resource._register_class('DummyResource', DummyResource)
|
||||
env.load({'resource_registry':
|
||||
{'DummyResource': 'test_resource.template'}})
|
||||
stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
||||
stack = parser.Stack(self.ctx, 'test_stack',
|
||||
parser.Template(empty_template, files=files,
|
||||
env=env),
|
||||
stack_id=str(uuid.uuid4()))
|
||||
|
@ -872,7 +874,13 @@ class TemplateResourceCrudTest(common.HeatTestCase):
|
|||
self.provider, {'Foo': 'bar'})
|
||||
|
||||
def test_handle_delete(self):
|
||||
self.res.nested = mock.MagicMock()
|
||||
self.res.nested.return_value.delete.return_value = None
|
||||
self.res.rpc_client = mock.MagicMock()
|
||||
self.res.id = 55
|
||||
self.res.uuid = six.text_type(uuid.uuid4())
|
||||
self.res.resource_id = six.text_type(uuid.uuid4())
|
||||
ident = identifier.HeatIdentifier(self.ctx.tenant_id,
|
||||
self.res.physical_resource_name(),
|
||||
self.res.resource_id)
|
||||
self.res.handle_delete()
|
||||
self.res.nested.return_value.delete.assert_called_once_with()
|
||||
rpcc = self.res.rpc_client.return_value
|
||||
rpcc.delete_stack.assert_called_once_with(self.ctx, ident)
|
||||
|
|
|
@ -21,7 +21,6 @@ from heat.common import exception
|
|||
from heat.common import template_format
|
||||
from heat.engine import resource
|
||||
from heat.engine.resources import stack_resource
|
||||
from heat.engine import scheduler
|
||||
from heat.engine import stack as parser
|
||||
from heat.engine import template as templatem
|
||||
from heat.tests import common
|
||||
|
@ -419,36 +418,7 @@ class StackResourceTest(common.HeatTestCase):
|
|||
self.parent_resource._validate_nested_resources,
|
||||
template)
|
||||
|
||||
def test_create_with_template_validates(self):
|
||||
"""
|
||||
Creating a stack with a template validates the created stack, so that
|
||||
an invalid template will cause an error to be raised.
|
||||
"""
|
||||
# Make a parameter key with the same name as the resource to cause a
|
||||
# simple validation error
|
||||
template = self.simple_template.copy()
|
||||
template['Parameters']['WebServer'] = {'Type': 'String'}
|
||||
self.assertRaises(
|
||||
exception.StackValidationFailed,
|
||||
self.parent_resource.create_with_template,
|
||||
template, {'WebServer': 'foo'})
|
||||
|
||||
def test_update_with_template_validates(self):
|
||||
"""Updating a stack with a template validates the created stack."""
|
||||
self.parent_resource._nested = mock.MagicMock()
|
||||
|
||||
template = self.simple_template.copy()
|
||||
template['Parameters']['WebServer'] = {'Type': 'String'}
|
||||
self.assertRaises(
|
||||
exception.StackValidationFailed,
|
||||
self.parent_resource.update_with_template,
|
||||
template, {'WebServer': 'foo'})
|
||||
|
||||
def test_load_nested_ok(self):
|
||||
self.parent_resource.create_with_template(self.templ,
|
||||
{"KeyName": "key"})
|
||||
self.stack = self.parent_resource.nested()
|
||||
|
||||
self.parent_resource._nested = None
|
||||
self.parent_resource.resource_id = 319
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
|
@ -458,59 +428,55 @@ class StackResourceTest(common.HeatTestCase):
|
|||
show_deleted=False,
|
||||
force_reload=False).AndReturn('s')
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.parent_resource.nested()
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_load_nested_force_reload(self):
|
||||
create_creator = self.parent_resource.create_with_template(
|
||||
self.templ, {"KeyName": "key"})
|
||||
create_creator.run_to_completion()
|
||||
expected_state = (parser.Stack.CREATE, parser.Stack.COMPLETE)
|
||||
self.assertEqual(expected_state, self.parent_resource.nested().state)
|
||||
|
||||
stack = parser.Stack.load(
|
||||
self.parent_resource.context,
|
||||
self.parent_resource.resource_id,
|
||||
parent_resource=self.parent_resource.name,
|
||||
show_deleted=False)
|
||||
stack.state_set(parser.Stack.CREATE, parser.Stack.FAILED, "foo")
|
||||
self.assertEqual(expected_state, self.parent_resource.nested().state)
|
||||
expected_state = (parser.Stack.CREATE, parser.Stack.FAILED)
|
||||
self.assertEqual(expected_state,
|
||||
self.parent_resource.nested(force_reload=True).state)
|
||||
|
||||
def test_load_nested_deleted(self):
|
||||
create_creator = self.parent_resource.create_with_template(
|
||||
self.templ, {"KeyName": "key"})
|
||||
create_creator.run_to_completion()
|
||||
expected_state = (parser.Stack.CREATE, parser.Stack.COMPLETE)
|
||||
self.assertEqual(expected_state, self.parent_resource.nested().state)
|
||||
|
||||
delete_deletor = self.parent_resource.delete_nested()
|
||||
delete_deletor.run_to_completion()
|
||||
expected_state = (parser.Stack.DELETE, parser.Stack.COMPLETE)
|
||||
self.assertEqual(expected_state,
|
||||
self.parent_resource.nested(force_reload=True,
|
||||
show_deleted=True).state)
|
||||
|
||||
def test_load_nested_non_exist(self):
|
||||
self.parent_resource.create_with_template(self.templ,
|
||||
{"KeyName": "key"})
|
||||
self.stack = self.parent_resource.nested()
|
||||
|
||||
self.parent_resource._nested = None
|
||||
self.parent_resource._nested = 'write-over-me'
|
||||
self.parent_resource.resource_id = 319
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
parser.Stack.load(self.parent_resource.context,
|
||||
self.parent_resource.resource_id,
|
||||
parent_resource=self.parent_resource.name,
|
||||
show_deleted=False,
|
||||
force_reload=False)
|
||||
force_reload=True).AndReturn('ok')
|
||||
self.m.ReplayAll()
|
||||
self.parent_resource.nested(force_reload=True)
|
||||
self.assertEqual('ok', self.parent_resource._nested)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_load_nested_non_exist(self):
|
||||
self.parent_resource._nested = None
|
||||
self.parent_resource.resource_id = '90-8'
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
parser.Stack.load(self.parent_resource.context,
|
||||
self.parent_resource.resource_id,
|
||||
parent_resource=self.parent_resource.name,
|
||||
show_deleted=False,
|
||||
force_reload=False).AndReturn(None)
|
||||
self.m.ReplayAll()
|
||||
|
||||
self.assertRaises(exception.NotFound, self.parent_resource.nested)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_load_nested_cached(self):
|
||||
self.parent_resource._nested = 'gotthis'
|
||||
self.assertEqual('gotthis', self.parent_resource.nested())
|
||||
|
||||
def test_load_nested_force_reload_ok(self):
|
||||
self.parent_resource._nested = mock.MagicMock()
|
||||
self.parent_resource.resource_id = '90-8'
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
parser.Stack.load(self.parent_resource.context,
|
||||
self.parent_resource.resource_id,
|
||||
parent_resource=self.parent_resource.name,
|
||||
show_deleted=False,
|
||||
force_reload=True).AndReturn('s')
|
||||
self.m.ReplayAll()
|
||||
st = self.parent_resource.nested(force_reload=True)
|
||||
self.assertEqual('s', st)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_load_nested_force_reload_none(self):
|
||||
self.parent_resource._nested = mock.MagicMock()
|
||||
self.parent_resource.resource_id = '90-8'
|
||||
|
@ -526,20 +492,12 @@ class StackResourceTest(common.HeatTestCase):
|
|||
self.m.VerifyAll()
|
||||
|
||||
def test_delete_nested_not_found_nested_stack(self):
|
||||
self.parent_resource.create_with_template(self.templ,
|
||||
{"KeyName": "key"})
|
||||
self.stack = self.parent_resource.nested()
|
||||
|
||||
self.parent_resource._nested = None
|
||||
self.m.StubOutWithMock(parser.Stack, 'load')
|
||||
parser.Stack.load(
|
||||
self.parent_resource.context,
|
||||
self.parent_resource.resource_id,
|
||||
parent_resource=self.parent_resource.name,
|
||||
show_deleted=False, force_reload=False
|
||||
).AndRaise(exception.NotFound(''))
|
||||
self.m.ReplayAll()
|
||||
|
||||
rpcc = mock.Mock()
|
||||
self.parent_resource.rpc_client = rpcc
|
||||
rpcc.return_value.delete_stack = mock.Mock(
|
||||
side_effect=exception.NotFound())
|
||||
self.assertIsNone(self.parent_resource.delete_nested())
|
||||
|
||||
def test_need_update_in_failed_state_for_nested_resource(self):
|
||||
|
@ -556,18 +514,6 @@ class StackResourceTest(common.HeatTestCase):
|
|||
|
||||
self.assertEqual(True, need_update)
|
||||
|
||||
def test_check_nested_resources(self):
|
||||
def _mock_check(res):
|
||||
res.handle_check = mock.Mock()
|
||||
|
||||
self.parent_resource.create_with_template(self.templ, {"KeyName": "k"})
|
||||
nested = self.parent_resource.nested()
|
||||
[_mock_check(res) for res in nested.resources.values()]
|
||||
|
||||
scheduler.TaskRunner(self.parent_resource.check)()
|
||||
[self.assertTrue(res.handle_check.called)
|
||||
for res in nested.resources.values()]
|
||||
|
||||
|
||||
class StackResourceLimitTest(common.HeatTestCase):
|
||||
scenarios = [
|
||||
|
@ -719,11 +665,11 @@ class StackResourceAttrTest(common.HeatTestCase):
|
|||
|
||||
class StackResourceCheckCompleteTest(common.HeatTestCase):
|
||||
scenarios = [
|
||||
('create', dict(action='create')),
|
||||
('update', dict(action='update')),
|
||||
('suspend', dict(action='suspend')),
|
||||
('resume', dict(action='resume')),
|
||||
('delete', dict(action='delete')),
|
||||
('create', dict(action='create', show_deleted=False)),
|
||||
('update', dict(action='update', show_deleted=False)),
|
||||
('suspend', dict(action='suspend', show_deleted=False)),
|
||||
('resume', dict(action='resume', show_deleted=False)),
|
||||
('delete', dict(action='delete', show_deleted=True)),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
@ -744,8 +690,10 @@ class StackResourceCheckCompleteTest(common.HeatTestCase):
|
|||
self.parent_stack)
|
||||
|
||||
self.nested = mock.MagicMock()
|
||||
self.parent_resource.nested = mock.MagicMock(return_value=self.nested)
|
||||
self.parent_resource._nested = self.nested
|
||||
setattr(self.nested, self.action.upper(), self.action.upper())
|
||||
self.nested.action = self.action.upper()
|
||||
self.nested.COMPLETE = 'COMPLETE'
|
||||
|
||||
def test_state_ok(self):
|
||||
|
@ -753,33 +701,43 @@ class StackResourceCheckCompleteTest(common.HeatTestCase):
|
|||
check_create_complete should return True create task is
|
||||
done and the nested stack is in (<action>,COMPLETE) state.
|
||||
"""
|
||||
self.nested.state = (self.action.upper(), 'COMPLETE')
|
||||
task = mock.MagicMock()
|
||||
task.step.return_value = True
|
||||
|
||||
self.nested.status = 'COMPLETE'
|
||||
complete = getattr(self.parent_resource,
|
||||
'check_%s_complete' % self.action)
|
||||
self.assertIs(True, complete(task))
|
||||
self.assertIs(True, complete(None))
|
||||
self.parent_resource.nested.assert_called_once_with(
|
||||
show_deleted=self.show_deleted, force_reload=True)
|
||||
|
||||
def test_state_err(self):
|
||||
"""
|
||||
check_create_complete should raise error when create task is
|
||||
done but the nested stack is not in (<action>,COMPLETE) state
|
||||
"""
|
||||
|
||||
self.nested.state = (self.action.upper(), 'FAILED')
|
||||
self.nested.status = 'FAILED'
|
||||
self.nested.status_reason = 'broken on purpose'
|
||||
task = mock.MagicMock()
|
||||
task.step.return_value = True
|
||||
|
||||
complete = getattr(self.parent_resource,
|
||||
'check_%s_complete' % self.action)
|
||||
self.assertRaises(exception.Error, complete, task)
|
||||
|
||||
def test_step_false(self):
|
||||
task = mock.MagicMock()
|
||||
task.step.return_value = False
|
||||
self.assertRaises(resource.ResourceUnknownStatus, complete, None)
|
||||
self.parent_resource.nested.assert_called_once_with(
|
||||
show_deleted=self.show_deleted, force_reload=True)
|
||||
|
||||
def test_state_unknown(self):
|
||||
"""
|
||||
check_create_complete should raise error when create task is
|
||||
done but the nested stack is not in (<action>,COMPLETE) state
|
||||
"""
|
||||
self.nested.status = 'WTF'
|
||||
self.nested.status_reason = 'broken on purpose'
|
||||
complete = getattr(self.parent_resource,
|
||||
'check_%s_complete' % self.action)
|
||||
self.assertIs(False, complete(task))
|
||||
self.assertRaises(resource.ResourceUnknownStatus, complete, None)
|
||||
self.parent_resource.nested.assert_called_once_with(
|
||||
show_deleted=self.show_deleted, force_reload=True)
|
||||
|
||||
def test_in_progress(self):
|
||||
self.nested.status = 'IN_PROGRESS'
|
||||
complete = getattr(self.parent_resource,
|
||||
'check_%s_complete' % self.action)
|
||||
self.assertFalse(complete(None))
|
||||
self.parent_resource.nested.assert_called_once_with(
|
||||
show_deleted=self.show_deleted, force_reload=True)
|
||||
|
|
Loading…
Reference in New Issue