4cbd1431ee
Always update remote_stack even though properties didn't change, and let the let the individual resources in it decide if they need updating Closes-Bug: #1428979 Change-Id: Id0898683b86e3a72e539f2432f521c7ee70afe26
616 lines
24 KiB
Python
616 lines
24 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 copy
|
|
|
|
from heatclient import exc
|
|
from heatclient.v1 import stacks
|
|
import mock
|
|
from oslo_config import cfg
|
|
import six
|
|
|
|
from heat.common import exception
|
|
from heat.common.i18n import _
|
|
from heat.common import template_format
|
|
from heat.engine import environment
|
|
from heat.engine import parser
|
|
from heat.engine import resource
|
|
from heat.engine.resources.openstack.heat import remote_stack
|
|
from heat.engine import rsrc_defn
|
|
from heat.engine import scheduler
|
|
from heat.tests import common as tests_common
|
|
from heat.tests import utils
|
|
|
|
|
|
cfg.CONF.import_opt('action_retry_limit', 'heat.common.config')
|
|
|
|
parent_stack_template = '''
|
|
heat_template_version: 2013-05-23
|
|
resources:
|
|
remote_stack:
|
|
type: OS::Heat::Stack
|
|
properties:
|
|
context:
|
|
region_name: RegionOne
|
|
template: { get_file: remote_template.yaml }
|
|
timeout: 60
|
|
parameters:
|
|
name: foo
|
|
'''
|
|
|
|
remote_template = '''
|
|
heat_template_version: 2013-05-23
|
|
parameters:
|
|
name:
|
|
type: string
|
|
resources:
|
|
resource1:
|
|
type: GenericResourceType
|
|
outputs:
|
|
foo:
|
|
value: bar
|
|
'''
|
|
|
|
bad_template = '''
|
|
heat_template_version: 2013-05-26
|
|
parameters:
|
|
name:
|
|
type: string
|
|
resources:
|
|
resource1:
|
|
type: UnknownResourceType
|
|
outputs:
|
|
foo:
|
|
value: bar
|
|
'''
|
|
|
|
|
|
def get_stack(stack_id='c8a19429-7fde-47ea-a42f-40045488226c',
|
|
stack_name='teststack', description='No description',
|
|
creation_time='2013-08-04T20:57:55Z',
|
|
updated_time='2013-08-04T20:57:55Z',
|
|
stack_status='CREATE_COMPLETE',
|
|
stack_status_reason='',
|
|
outputs=None):
|
|
action = stack_status[:stack_status.index('_')]
|
|
status = stack_status[stack_status.index('_') + 1:]
|
|
data = {
|
|
'id': stack_id,
|
|
'stack_name': stack_name,
|
|
'description': description,
|
|
'creation_time': creation_time,
|
|
'updated_time': updated_time,
|
|
'stack_status': stack_status,
|
|
'stack_status_reason': stack_status_reason,
|
|
'action': action,
|
|
'status': status,
|
|
'outputs': outputs or None,
|
|
}
|
|
return stacks.Stack(mock.MagicMock(), data)
|
|
|
|
|
|
class FakeClients(object):
|
|
def __init__(self, region_name=None):
|
|
self.region_name = region_name or 'RegionOne'
|
|
self.hc = None
|
|
self.plugin = None
|
|
|
|
def heat(self):
|
|
if self.region_name in ['RegionOne', 'RegionTwo']:
|
|
if self.hc is None:
|
|
self.hc = mock.MagicMock()
|
|
return self.hc
|
|
else:
|
|
raise Exception('Failed connecting to Heat')
|
|
|
|
def client_plugin(self, name):
|
|
def examine_exception(ex):
|
|
if not isinstance(ex, exc.HTTPNotFound):
|
|
raise ex
|
|
if self.plugin is None:
|
|
self.plugin = mock.MagicMock()
|
|
self.plugin.ignore_not_found.side_effect = examine_exception
|
|
return self.plugin
|
|
|
|
|
|
class RemoteStackTest(tests_common.HeatTestCase):
|
|
|
|
def setUp(self):
|
|
super(RemoteStackTest, self).setUp()
|
|
self.this_region = 'RegionOne'
|
|
self.that_region = 'RegionTwo'
|
|
self.bad_region = 'RegionNone'
|
|
|
|
cfg.CONF.set_override('action_retry_limit', 0)
|
|
self.parent = None
|
|
self.heat = None
|
|
self.client_plugin = None
|
|
self.this_context = None
|
|
self.old_clients = None
|
|
|
|
def unset_clients_property():
|
|
type(self.this_context).clients = self.old_clients
|
|
|
|
self.addCleanup(unset_clients_property)
|
|
|
|
def initialize(self):
|
|
parent, rsrc = self.create_parent_stack(remote_region='RegionTwo')
|
|
self.parent = parent
|
|
self.heat = rsrc._context().clients.heat()
|
|
self.client_plugin = rsrc._context().clients.client_plugin('heat')
|
|
|
|
def create_parent_stack(self, remote_region=None, custom_template=None):
|
|
snippet = template_format.parse(parent_stack_template)
|
|
self.files = {
|
|
'remote_template.yaml': custom_template or remote_template
|
|
}
|
|
|
|
region_name = remote_region or self.this_region
|
|
props = snippet['resources']['remote_stack']['properties']
|
|
|
|
# context property is not required, default to current region
|
|
if remote_region is None:
|
|
del props['context']
|
|
else:
|
|
props['context']['region_name'] = region_name
|
|
|
|
if self.this_context is None:
|
|
self.this_context = utils.dummy_context(
|
|
region_name=self.this_region)
|
|
|
|
tmpl = parser.Template(snippet, files=self.files)
|
|
parent = parser.Stack(self.this_context, 'parent_stack', tmpl)
|
|
|
|
# parent context checking
|
|
ctx = parent.context.to_dict()
|
|
self.assertEqual(self.this_region, ctx['region_name'])
|
|
self.assertEqual(self.this_context.to_dict(), ctx)
|
|
|
|
parent.store()
|
|
|
|
resource_defns = parent.t.resource_definitions(parent)
|
|
rsrc = remote_stack.RemoteStack(
|
|
'remote_stack_res',
|
|
resource_defns['remote_stack'],
|
|
parent)
|
|
|
|
# remote stack resource checking
|
|
self.assertEqual(60, rsrc.properties.get('timeout'))
|
|
|
|
remote_context = rsrc._context()
|
|
hc = FakeClients(rsrc._region_name)
|
|
if self.old_clients is None:
|
|
self.old_clients = type(remote_context).clients
|
|
type(remote_context).clients = mock.PropertyMock(return_value=hc)
|
|
|
|
return parent, rsrc
|
|
|
|
def create_remote_stack(self):
|
|
# This method default creates a stack on RegionTwo (self.other_region)
|
|
defaults = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
|
get_stack(stack_status='CREATE_COMPLETE')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return defaults.pop(0)
|
|
|
|
if self.parent is None:
|
|
self.initialize()
|
|
|
|
# prepare clients to return status
|
|
self.heat.stacks.create.return_value = {'stack': get_stack().to_dict()}
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
rsrc = self.parent['remote_stack']
|
|
scheduler.TaskRunner(rsrc.create)()
|
|
|
|
return rsrc
|
|
|
|
def test_create_remote_stack_default_region(self):
|
|
parent, rsrc = self.create_parent_stack()
|
|
|
|
self.assertEqual((rsrc.INIT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual(self.this_region, rsrc._region_name)
|
|
ctx = rsrc.properties.get('context')
|
|
self.assertIsNone(ctx)
|
|
|
|
self.assertIsNone(rsrc.validate())
|
|
|
|
def test_create_remote_stack_this_region(self):
|
|
parent, rsrc = self.create_parent_stack(remote_region=self.this_region)
|
|
|
|
self.assertEqual((rsrc.INIT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual(self.this_region, rsrc._region_name)
|
|
ctx = rsrc.properties.get('context')
|
|
self.assertEqual(self.this_region, ctx['region_name'])
|
|
|
|
self.assertIsNone(rsrc.validate())
|
|
|
|
def test_create_remote_stack_that_region(self):
|
|
parent, rsrc = self.create_parent_stack(remote_region=self.that_region)
|
|
|
|
self.assertEqual((rsrc.INIT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual(self.that_region, rsrc._region_name)
|
|
ctx = rsrc.properties.get('context')
|
|
self.assertEqual(self.that_region, ctx['region_name'])
|
|
|
|
self.assertIsNone(rsrc.validate())
|
|
|
|
def test_create_remote_stack_bad_region(self):
|
|
parent, rsrc = self.create_parent_stack(remote_region=self.bad_region)
|
|
|
|
self.assertEqual((rsrc.INIT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual(self.bad_region, rsrc._region_name)
|
|
ctx = rsrc.properties.get('context')
|
|
self.assertEqual(self.bad_region, ctx['region_name'])
|
|
|
|
ex = self.assertRaises(exception.StackValidationFailed,
|
|
rsrc.validate)
|
|
msg = ('Cannot establish connection to Heat endpoint '
|
|
'at region "%s"' % self.bad_region)
|
|
self.assertIn(msg, six.text_type(ex))
|
|
|
|
def test_remote_validation_failed(self):
|
|
parent, rsrc = self.create_parent_stack(remote_region=self.that_region,
|
|
custom_template=bad_template)
|
|
|
|
self.assertEqual((rsrc.INIT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual(self.that_region, rsrc._region_name)
|
|
ctx = rsrc.properties.get('context')
|
|
self.assertEqual(self.that_region, ctx['region_name'])
|
|
|
|
# not setting or using self.heat because this test case is a special
|
|
# one with the RemoteStack resource initialized but not created.
|
|
heat = rsrc._context().clients.heat()
|
|
|
|
# heatclient.exc.BadRequest is the exception returned by a failed
|
|
# validation
|
|
heat.stacks.validate = mock.MagicMock(side_effect=exc.HTTPBadRequest)
|
|
ex = self.assertRaises(exception.StackValidationFailed, rsrc.validate)
|
|
msg = ('Failed validating stack template using Heat endpoint at region'
|
|
' "%s"') % self.that_region
|
|
self.assertIn(msg, six.text_type(ex))
|
|
|
|
def test_create(self):
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c',
|
|
rsrc.resource_id)
|
|
env = environment.get_child_environment(rsrc.stack.env,
|
|
{'name': 'foo'})
|
|
args = {
|
|
'stack_name': rsrc.physical_resource_name(),
|
|
'template': template_format.parse(remote_template),
|
|
'timeout_mins': 60,
|
|
'disable_rollback': True,
|
|
'parameters': {'name': 'foo'},
|
|
'files': self.files,
|
|
'environment': env.user_env_as_dict(),
|
|
}
|
|
self.heat.stacks.create.assert_called_with(**args)
|
|
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
|
|
|
def test_create_failed(self):
|
|
returns = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
|
get_stack(stack_status='CREATE_FAILED',
|
|
stack_status_reason='Remote stack creation '
|
|
'failed')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return returns.pop(0)
|
|
|
|
# Note: only this test case does a out-of-band intialization, most of
|
|
# the other test cases will have self.parent initialized.
|
|
if self.parent is None:
|
|
self.initialize()
|
|
|
|
self.heat.stacks.create.return_value = {'stack': get_stack().to_dict()}
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
|
|
rsrc = self.parent['remote_stack']
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.create))
|
|
error_msg = ('ResourceInError: Went to status CREATE_FAILED due to '
|
|
'"Remote stack creation failed"')
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
|
|
|
|
def test_delete(self):
|
|
returns = [get_stack(stack_status='DELETE_IN_PROGRESS'),
|
|
get_stack(stack_status='DELETE_COMPLETE')]
|
|
|
|
def side_effect_d(*args, **kwargs):
|
|
return returns.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect_d)
|
|
self.heat.stacks.delete = mock.MagicMock()
|
|
remote_stack_id = rsrc.resource_id
|
|
scheduler.TaskRunner(rsrc.delete)()
|
|
|
|
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
|
|
self.heat.stacks.delete.assert_called_with(stack_id=remote_stack_id)
|
|
|
|
def test_delete_already_gone(self):
|
|
def side_effect(*args, **kwargs):
|
|
raise exc.HTTPNotFound()
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.delete = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
|
|
remote_stack_id = rsrc.resource_id
|
|
scheduler.TaskRunner(rsrc.delete)()
|
|
|
|
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
|
|
self.heat.stacks.delete.assert_called_with(stack_id=remote_stack_id)
|
|
|
|
def test_delete_failed(self):
|
|
returns = [get_stack(stack_status='DELETE_IN_PROGRESS'),
|
|
get_stack(stack_status='DELETE_FAILED',
|
|
stack_status_reason='Remote stack deletion '
|
|
'failed')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return returns.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.stacks.delete = mock.MagicMock()
|
|
|
|
remote_stack_id = rsrc.resource_id
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.delete))
|
|
error_msg = ('ResourceInError: Went to status DELETE_FAILED due to '
|
|
'"Remote stack deletion failed"')
|
|
self.assertIn(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state)
|
|
self.heat.stacks.delete.assert_called_with(stack_id=remote_stack_id)
|
|
self.assertEqual(rsrc.resource_id, remote_stack_id)
|
|
|
|
def test_attribute(self):
|
|
rsrc = self.create_remote_stack()
|
|
|
|
outputs = [
|
|
{
|
|
'output_key': 'foo',
|
|
'output_value': 'bar'
|
|
}
|
|
]
|
|
created_stack = get_stack(stack_name='stack1', outputs=outputs)
|
|
self.heat.stacks.get = mock.MagicMock(return_value=created_stack)
|
|
self.assertEqual('stack1', rsrc.FnGetAtt('stack_name'))
|
|
self.assertEqual('bar', rsrc.FnGetAtt('outputs')['foo'])
|
|
self.heat.stacks.get.assert_called_with(
|
|
stack_id='c8a19429-7fde-47ea-a42f-40045488226c')
|
|
|
|
def test_attribute_failed(self):
|
|
rsrc = self.create_remote_stack()
|
|
|
|
error = self.assertRaises(exception.InvalidTemplateAttribute,
|
|
rsrc.FnGetAtt, 'non-existent_property')
|
|
self.assertEqual(
|
|
'The Referenced Attribute (remote_stack non-existent_property) is '
|
|
'incorrect.',
|
|
six.text_type(error))
|
|
|
|
def test_resume(self):
|
|
stacks = [get_stack(stack_status='RESUME_IN_PROGRESS'),
|
|
get_stack(stack_status='RESUME_COMPLETE')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
rsrc.action = rsrc.SUSPEND
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.actions.resume = mock.MagicMock()
|
|
scheduler.TaskRunner(rsrc.resume)()
|
|
|
|
self.assertEqual((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
|
|
self.heat.actions.resume.assert_called_with(stack_id=rsrc.resource_id)
|
|
|
|
def test_resume_failed(self):
|
|
returns = [get_stack(stack_status='RESUME_IN_PROGRESS'),
|
|
get_stack(stack_status='RESUME_FAILED',
|
|
stack_status_reason='Remote stack resume failed')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return returns.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
rsrc.action = rsrc.SUSPEND
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.actions.resume = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.resume))
|
|
error_msg = ('ResourceInError: Went to status RESUME_FAILED due to '
|
|
'"Remote stack resume failed"')
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.RESUME, rsrc.FAILED), rsrc.state)
|
|
self.heat.actions.resume.assert_called_with(stack_id=rsrc.resource_id)
|
|
|
|
def test_resume_failed_not_created(self):
|
|
self.initialize()
|
|
rsrc = self.parent['remote_stack']
|
|
rsrc.action = rsrc.SUSPEND
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.resume))
|
|
error_msg = 'Error: Cannot resume remote_stack, resource not found'
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.RESUME, rsrc.FAILED), rsrc.state)
|
|
|
|
def test_suspend(self):
|
|
stacks = [get_stack(stack_status='SUSPEND_IN_PROGRESS'),
|
|
get_stack(stack_status='SUSPEND_COMPLETE')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.actions.suspend = mock.MagicMock()
|
|
scheduler.TaskRunner(rsrc.suspend)()
|
|
|
|
self.assertEqual((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
|
|
self.heat.actions.suspend.assert_called_with(stack_id=rsrc.resource_id)
|
|
|
|
def test_suspend_failed(self):
|
|
stacks = [get_stack(stack_status='SUSPEND_IN_PROGRESS'),
|
|
get_stack(stack_status='SUSPEND_FAILED',
|
|
stack_status_reason='Remote stack suspend failed')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
self.heat.actions.suspend = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.suspend))
|
|
error_msg = ('ResourceInError: Went to status SUSPEND_FAILED due to '
|
|
'"Remote stack suspend failed"')
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.SUSPEND, rsrc.FAILED), rsrc.state)
|
|
# assert suspend was not called
|
|
self.heat.actions.suspend.assert_has_calls([])
|
|
|
|
def test_suspend_failed_not_created(self):
|
|
self.initialize()
|
|
rsrc = self.parent['remote_stack']
|
|
# Note: the resource is not created so far
|
|
self.heat.actions.suspend = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.suspend))
|
|
error_msg = 'Error: Cannot suspend remote_stack, resource not found'
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.SUSPEND, rsrc.FAILED), rsrc.state)
|
|
# assert suspend was not called
|
|
self.heat.actions.suspend.assert_has_calls([])
|
|
|
|
def test_update(self):
|
|
stacks = [get_stack(stack_status='UPDATE_IN_PROGRESS'),
|
|
get_stack(stack_status='UPDATE_COMPLETE')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
props = copy.deepcopy(rsrc.parsed_template()['Properties'])
|
|
props['parameters']['name'] = 'bar'
|
|
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name,
|
|
rsrc.type(),
|
|
props)
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
scheduler.TaskRunner(rsrc.update, update_snippet)()
|
|
|
|
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual('bar', rsrc.properties.get('parameters')['name'])
|
|
env = environment.get_child_environment(rsrc.stack.env,
|
|
{'name': 'bar'})
|
|
fields = {
|
|
'stack_id': rsrc.resource_id,
|
|
'template': template_format.parse(remote_template),
|
|
'timeout_mins': 60,
|
|
'disable_rollback': True,
|
|
'parameters': {'name': 'bar'},
|
|
'files': self.files,
|
|
'environment': env.user_env_as_dict(),
|
|
}
|
|
self.heat.stacks.update.assert_called_with(**fields)
|
|
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
|
|
|
def test_update_with_replace(self):
|
|
rsrc = self.create_remote_stack()
|
|
|
|
props = copy.deepcopy(rsrc.parsed_template()['Properties'])
|
|
props['context']['region_name'] = 'RegionOne'
|
|
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name,
|
|
rsrc.type(),
|
|
props)
|
|
self.assertRaises(resource.UpdateReplace,
|
|
scheduler.TaskRunner(rsrc.update, update_snippet))
|
|
|
|
def test_update_failed(self):
|
|
stacks = [get_stack(stack_status='UPDATE_IN_PROGRESS'),
|
|
get_stack(stack_status='UPDATE_FAILED',
|
|
stack_status_reason='Remote stack update failed')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
props = copy.deepcopy(rsrc.parsed_template()['Properties'])
|
|
props['parameters']['name'] = 'bar'
|
|
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name,
|
|
rsrc.type(),
|
|
props)
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.update,
|
|
update_snippet))
|
|
error_msg = _('ResourceInError: Went to status UPDATE_FAILED due to '
|
|
'"Remote stack update failed"')
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.UPDATE, rsrc.FAILED), rsrc.state)
|
|
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
|
|
|
def test_update_no_change(self):
|
|
stacks = [get_stack(stack_status='UPDATE_IN_PROGRESS'),
|
|
get_stack(stack_status='UPDATE_COMPLETE')]
|
|
|
|
def side_effect(*args, **kwargs):
|
|
return stacks.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
props = copy.deepcopy(rsrc.parsed_template()['Properties'])
|
|
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name,
|
|
rsrc.type(),
|
|
props)
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect)
|
|
scheduler.TaskRunner(rsrc.update, update_snippet)()
|
|
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
|
|
|
|
def test_stack_status_error(self):
|
|
returns = [get_stack(stack_status='DELETE_IN_PROGRESS'),
|
|
get_stack(stack_status='UPDATE_COMPLETE')]
|
|
|
|
def side_effect_d(*args, **kwargs):
|
|
return returns.pop(0)
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=side_effect_d)
|
|
self.heat.stacks.delete = mock.MagicMock()
|
|
remote_stack_id = rsrc.resource_id
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.delete))
|
|
reason = ('Resource action mismatch detected: expected=DELETE '
|
|
'actual=UPDATE')
|
|
error_msg = ('ResourceUnknownStatus: Resource failed - Unknown '
|
|
'status UPDATE_COMPLETE due to "%s"') % reason
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.heat.stacks.delete.assert_called_with(stack_id=remote_stack_id)
|