
It is convenient to have all exceptions in exception module. Also it is reduces namespace cluttering of resource module and decreases the number of dependencies in other modules (we do not need to import resource in some cases for now). UpdateReplace exception is moved in this patch. Change-Id: Ief441ca2022a0d50e88d709d1a062631479715b7
662 lines
26 KiB
Python
662 lines
26 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 collections
|
|
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.resources.openstack.heat import remote_stack
|
|
from heat.engine import rsrc_defn
|
|
from heat.engine import scheduler
|
|
from heat.engine import stack
|
|
from heat.engine import template
|
|
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 client(self, name):
|
|
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.client("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 = template.Template(snippet, files=self.files)
|
|
parent = stack.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')]
|
|
|
|
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=defaults)
|
|
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.client("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')]
|
|
|
|
# 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=returns)
|
|
|
|
rsrc = self.parent['remote_stack']
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.create))
|
|
error_msg = ('ResourceInError: resources.remote_stack: '
|
|
'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')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=returns)
|
|
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):
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.delete = mock.MagicMock(
|
|
side_effect=exc.HTTPNotFound())
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=exc.HTTPNotFound())
|
|
|
|
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')]
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=returns)
|
|
self.heat.stacks.delete = mock.MagicMock()
|
|
|
|
remote_stack_id = rsrc.resource_id
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.delete))
|
|
error_msg = ('ResourceInError: resources.remote_stack: '
|
|
'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_snapshot(self):
|
|
stacks = [get_stack(stack_status='SNAPSHOT_IN_PROGRESS'),
|
|
get_stack(stack_status='SNAPSHOT_COMPLETE')]
|
|
snapshot = {
|
|
'id': 'a29bc9e25aa44f99a9a3d59cd5b0e263',
|
|
'status': 'IN_PROGRESS'
|
|
}
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=stacks)
|
|
self.heat.stacks.snapshot = mock.MagicMock(return_value=snapshot)
|
|
scheduler.TaskRunner(rsrc.snapshot)()
|
|
|
|
self.assertEqual((rsrc.SNAPSHOT, rsrc.COMPLETE), rsrc.state)
|
|
self.assertEqual('a29bc9e25aa44f99a9a3d59cd5b0e263',
|
|
rsrc.data().get('snapshot_id'))
|
|
self.heat.stacks.snapshot.assert_called_with(
|
|
stack_id=rsrc.resource_id)
|
|
|
|
def test_restore(self):
|
|
snapshot = {
|
|
'id': 'a29bc9e25aa44f99a9a3d59cd5b0e263',
|
|
'status': 'IN_PROGRESS'
|
|
}
|
|
remote_stack = mock.MagicMock()
|
|
remote_stack.action = 'SNAPSHOT'
|
|
remote_stack.status = 'COMPLETE'
|
|
|
|
parent, rsrc = self.create_parent_stack()
|
|
rsrc.action = rsrc.SNAPSHOT
|
|
|
|
heat = rsrc._context().clients.client("heat")
|
|
heat.stacks.snapshot = mock.MagicMock(return_value=snapshot)
|
|
heat.stacks.get = mock.MagicMock(return_value=remote_stack)
|
|
scheduler.TaskRunner(parent.snapshot, None)()
|
|
self.assertEqual((parent.SNAPSHOT, parent.COMPLETE), parent.state)
|
|
|
|
data = parent.prepare_abandon()
|
|
remote_stack_snapshot = {
|
|
'snapshot': {
|
|
'id': 'a29bc9e25aa44f99a9a3d59cd5b0e263',
|
|
'status': 'COMPLETE',
|
|
'data': {
|
|
'files': data['files'],
|
|
'environment': data['environment'],
|
|
'template': template_format.parse(
|
|
data['files']['remote_template.yaml'])
|
|
}
|
|
}
|
|
}
|
|
fake_snapshot = collections.namedtuple(
|
|
'Snapshot', ('data', 'stack_id'))(data, parent.id)
|
|
heat.stacks.snapshot_show = mock.MagicMock(
|
|
return_value=remote_stack_snapshot)
|
|
self.patchobject(rsrc, 'update').return_value = None
|
|
rsrc.action = rsrc.UPDATE
|
|
rsrc.status = rsrc.COMPLETE
|
|
remote_stack.action = 'UPDATE'
|
|
|
|
parent.restore(fake_snapshot)
|
|
|
|
self.assertEqual((parent.RESTORE, parent.COMPLETE), parent.state)
|
|
|
|
def test_check(self):
|
|
stacks = [get_stack(stack_status='CHECK_IN_PROGRESS'),
|
|
get_stack(stack_status='CHECK_COMPLETE')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=stacks)
|
|
self.heat.actions.check = mock.MagicMock()
|
|
scheduler.TaskRunner(rsrc.check)()
|
|
|
|
self.assertEqual((rsrc.CHECK, rsrc.COMPLETE), rsrc.state)
|
|
self.heat.actions.check.assert_called_with(stack_id=rsrc.resource_id)
|
|
|
|
def test_check_failed(self):
|
|
returns = [get_stack(stack_status='CHECK_IN_PROGRESS'),
|
|
get_stack(stack_status='CHECK_FAILED',
|
|
stack_status_reason='Remote stack check failed')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=returns)
|
|
self.heat.actions.resume = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.check))
|
|
error_msg = ('ResourceInError: resources.remote_stack: '
|
|
'Went to status CHECK_FAILED due to '
|
|
'"Remote stack check failed"')
|
|
self.assertEqual(error_msg, six.text_type(error))
|
|
self.assertEqual((rsrc.CHECK, rsrc.FAILED), rsrc.state)
|
|
self.heat.actions.check.assert_called_with(stack_id=rsrc.resource_id)
|
|
|
|
def test_resume(self):
|
|
stacks = [get_stack(stack_status='RESUME_IN_PROGRESS'),
|
|
get_stack(stack_status='RESUME_COMPLETE')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
rsrc.action = rsrc.SUSPEND
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=stacks)
|
|
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')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
rsrc.action = rsrc.SUSPEND
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=returns)
|
|
self.heat.actions.resume = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.resume))
|
|
error_msg = ('ResourceInError: resources.remote_stack: '
|
|
'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: resources.remote_stack: '
|
|
'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')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=stacks)
|
|
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')]
|
|
|
|
rsrc = self.create_remote_stack()
|
|
|
|
self.heat.stacks.get = mock.MagicMock(side_effect=stacks)
|
|
self.heat.actions.suspend = mock.MagicMock()
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.suspend))
|
|
error_msg = ('ResourceInError: resources.remote_stack: '
|
|
'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: resources.remote_stack: '
|
|
'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')]
|
|
|
|
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=stacks)
|
|
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(exception.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')]
|
|
|
|
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=stacks)
|
|
error = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(rsrc.update,
|
|
update_snippet))
|
|
error_msg = _('ResourceInError: resources.remote_stack: '
|
|
'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')]
|
|
|
|
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=stacks)
|
|
scheduler.TaskRunner(rsrc.update, update_snippet)()
|
|
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
|