deb-heat/heat/tests/test_remote_stack.py
Ethan Lynn 4cbd1431ee Always update remote_stack
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
2015-03-07 00:44:28 +08:00

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)