Adds multi-region support for stack resource
Adds new resource type called OS::Heat::Stack which allows creating new stack in specified region using heatclient. Implements: blueprint multi-region-support Co-Authored-By: Qiming Teng <tengqim@cn.ibm.com> Change-Id: I62cb07877702c57dd0e77dbcb0e123a3ec2c885d
This commit is contained in:
284
heat/engine/resources/remote_stack.py
Normal file
284
heat/engine/resources/remote_stack.py
Normal file
@@ -0,0 +1,284 @@
|
||||
#
|
||||
# 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 six
|
||||
|
||||
from heat.common import context
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common.i18n import _LE
|
||||
from heat.common import template_format
|
||||
from heat.engine import attributes
|
||||
from heat.engine import environment
|
||||
from heat.engine import function
|
||||
from heat.engine import properties
|
||||
from heat.engine import resource
|
||||
|
||||
from heat.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RemoteStack(resource.Resource):
|
||||
"""
|
||||
A Resource representing a stack which can be created using specified
|
||||
context.
|
||||
"""
|
||||
|
||||
PROPERTIES = (
|
||||
CONTEXT, TEMPLATE, TIMEOUT, PARAMETERS,
|
||||
) = (
|
||||
'context', 'template', 'timeout', 'parameters',
|
||||
)
|
||||
|
||||
ATTRIBUTES = (
|
||||
NAME_ATTR, OUTPUTS,
|
||||
) = (
|
||||
'stack_name', 'outputs',
|
||||
)
|
||||
|
||||
_CONTEXT_KEYS = (
|
||||
REGION_NAME
|
||||
) = (
|
||||
'region_name'
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
CONTEXT: properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Context for this stack.'),
|
||||
schema={
|
||||
REGION_NAME: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Region name in which this stack will be created.'),
|
||||
required=True,
|
||||
)
|
||||
}
|
||||
),
|
||||
TEMPLATE: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Template that specifies the stack to be created as '
|
||||
'a resource.'),
|
||||
required=True,
|
||||
update_allowed=True
|
||||
),
|
||||
TIMEOUT: properties.Schema(
|
||||
properties.Schema.NUMBER,
|
||||
_('Number of minutes to wait for this stack creation.'),
|
||||
update_allowed=True
|
||||
),
|
||||
PARAMETERS: properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Set of parameters passed to this stack.'),
|
||||
default={},
|
||||
update_allowed=True
|
||||
),
|
||||
}
|
||||
|
||||
attributes_schema = {
|
||||
NAME_ATTR: attributes.Schema(
|
||||
_('Name of the stack.')
|
||||
),
|
||||
OUTPUTS: attributes.Schema(
|
||||
_('A dict of key-value pairs output from the stack.')
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, name, definition, stack):
|
||||
super(RemoteStack, self).__init__(name, definition, stack)
|
||||
self._region_name = None
|
||||
self._local_context = None
|
||||
|
||||
def _context(self):
|
||||
if self._local_context:
|
||||
return self._local_context
|
||||
|
||||
ctx_props = self.properties.get(self.CONTEXT)
|
||||
if ctx_props:
|
||||
self._region_name = ctx_props[self.REGION_NAME]
|
||||
else:
|
||||
self._region_name = self.context.region_name
|
||||
|
||||
# Build RequestContext from existing one
|
||||
dict_ctxt = self.context.to_dict()
|
||||
dict_ctxt.update({'region_name': self._region_name})
|
||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
||||
return self._local_context
|
||||
|
||||
def heat(self):
|
||||
# A convenience method overriding Resource.heat()
|
||||
return self._context().clients.heat()
|
||||
|
||||
def client_plugin(self):
|
||||
# A convenience method overriding Resource.client_plugin()
|
||||
return self._context().clients.client_plugin('heat')
|
||||
|
||||
def validate(self):
|
||||
super(RemoteStack, self).validate()
|
||||
|
||||
try:
|
||||
self.heat()
|
||||
except Exception as ex:
|
||||
exc_info = dict(region=self._region_name, exc=six.text_type(ex))
|
||||
msg = _('Cannot establish connection to Heat endpoint at region '
|
||||
'"%(region)s" due to "%(exc)s"') % exc_info
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
try:
|
||||
registry = self.stack.env.registry
|
||||
params = self.properties[self.PARAMETERS]
|
||||
env = environment.get_custom_environment(registry, params)
|
||||
tmpl = template_format.parse(self.properties[self.TEMPLATE])
|
||||
args = {
|
||||
'template': tmpl,
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
self.heat().stacks.validate(**args)
|
||||
except Exception as ex:
|
||||
exc_info = dict(region=self._region_name, exc=six.text_type(ex))
|
||||
LOG.error(_LE('exception: %s'), type(ex))
|
||||
msg = _('Failed validating stack template using Heat endpoint at '
|
||||
'region "%(region)s" due to "%(exc)s"') % exc_info
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
def handle_create(self):
|
||||
registry = self.stack.env.registry
|
||||
params = self.properties[self.PARAMETERS]
|
||||
env = environment.get_custom_environment(registry, params)
|
||||
tmpl = template_format.parse(self.properties[self.TEMPLATE])
|
||||
args = {
|
||||
'stack_name': self.physical_resource_name_or_FnGetRefId(),
|
||||
'template': tmpl,
|
||||
'timeout_mins': self.properties[self.TIMEOUT],
|
||||
'disable_rollback': True,
|
||||
'parameters': params,
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
remote_stack_id = self.heat().stacks.create(**args)['stack']['id']
|
||||
self.resource_id_set(remote_stack_id)
|
||||
|
||||
def handle_delete(self):
|
||||
if self.resource_id is not None:
|
||||
try:
|
||||
self.heat().stacks.delete(stack_id=self.resource_id)
|
||||
except Exception as e:
|
||||
self.client_plugin().ignore_not_found(e)
|
||||
|
||||
def handle_resume(self):
|
||||
if self.resource_id is None:
|
||||
raise exception.Error(_('Cannot resume %s, resource not found')
|
||||
% self.name)
|
||||
self.heat().stacks.resume(stack_id=self.resource_id)
|
||||
|
||||
def handle_suspend(self):
|
||||
if self.resource_id is None:
|
||||
raise exception.Error(_('Cannot suspend %s, resource not found')
|
||||
% self.name)
|
||||
self.heat().stacks.suspend(stack_id=self.resource_id)
|
||||
|
||||
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
||||
if self.resource_id and prop_diff:
|
||||
snippet = json_snippet.get('Properties', {})
|
||||
self.properties = properties.Properties(self.properties_schema,
|
||||
snippet,
|
||||
function.resolve,
|
||||
self.name)
|
||||
|
||||
params = self.properties[self.PARAMETERS]
|
||||
registry = self.stack.env.registry
|
||||
env = environment.get_custom_environment(registry, params)
|
||||
tmpl = template_format.parse(self.properties[self.TEMPLATE])
|
||||
fields = {
|
||||
'stack_id': self.resource_id,
|
||||
'parameters': params,
|
||||
'template': tmpl,
|
||||
'timeout_mins': self.properties[self.TIMEOUT],
|
||||
'disable_rollback': self.stack.disable_rollback,
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
self.heat().stacks.update(**fields)
|
||||
|
||||
def _check_action_complete(self, action):
|
||||
stack = self.heat().stacks.get(stack_id=self.resource_id)
|
||||
if stack.action == action:
|
||||
if stack.status == self.IN_PROGRESS:
|
||||
return False
|
||||
elif stack.status == self.COMPLETE:
|
||||
return True
|
||||
elif stack.status == self.FAILED:
|
||||
raise resource.ResourceInError(
|
||||
resource_status=stack.stack_status,
|
||||
status_reason=stack.stack_status_reason)
|
||||
else:
|
||||
# Note: this should never happen, so it really means that
|
||||
# the resource/engine is in serious problem if it happens.
|
||||
raise resource.ResourceUnknownStatus(
|
||||
resource_status=stack.stack_status,
|
||||
status_reason=stack.stack_status_reason)
|
||||
else:
|
||||
msg = _('Resource action misatch detected: expected=%(expected)s '
|
||||
'actual=%(actual)s') % dict(expected=action,
|
||||
actual=stack.action)
|
||||
raise resource.ResourceUnknownStatus(
|
||||
resource_status=stack.stack_status,
|
||||
status_reason=msg)
|
||||
|
||||
def check_create_complete(self, *args):
|
||||
return self._check_action_complete(action=self.CREATE)
|
||||
|
||||
def check_delete_complete(self, *args):
|
||||
if self.resource_id is None:
|
||||
return True
|
||||
|
||||
try:
|
||||
return self._check_action_complete(action=self.DELETE)
|
||||
except Exception as ex:
|
||||
self.client_plugin().ignore_not_found(ex)
|
||||
return True
|
||||
|
||||
def check_resume_complete(self, *args):
|
||||
return self._check_action_complete(action=self.RESUME)
|
||||
|
||||
def check_suspend_complete(self, *args):
|
||||
return self._check_action_complete(action=self.SUSPEND)
|
||||
|
||||
def check_update_complete(self, *args):
|
||||
return self._check_action_complete(action=self.UPDATE)
|
||||
|
||||
def _resolve_attribute(self, name):
|
||||
try:
|
||||
stack = self.heat().stacks.get(stack_id=self.resource_id)
|
||||
except Exception as e:
|
||||
self.client_plugin().ignore_not_found(e)
|
||||
return None
|
||||
|
||||
if name == self.NAME_ATTR:
|
||||
value = getattr(stack, name, None)
|
||||
return value or self.physical_resource_name_or_FnGetRefId()
|
||||
|
||||
if name == self.OUTPUTS:
|
||||
outputs = stack.outputs
|
||||
return dict((output['output_key'], output['output_value'])
|
||||
for output in outputs)
|
||||
|
||||
def FnGetRefId(self):
|
||||
return self.resource_id
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Heat::Stack': RemoteStack,
|
||||
}
|
||||
591
heat/tests/test_remote_stack.py
Normal file
591
heat/tests/test_remote_stack.py
Normal file
@@ -0,0 +1,591 @@
|
||||
#
|
||||
# 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
|
||||
import mock
|
||||
import six
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import stacks
|
||||
from oslo.config import cfg
|
||||
|
||||
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 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)
|
||||
registry = rsrc.stack.env.registry
|
||||
env = environment.get_custom_environment(registry, {'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.stacks.resume = mock.MagicMock()
|
||||
scheduler.TaskRunner(rsrc.resume)()
|
||||
|
||||
self.assertEqual((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
|
||||
self.heat.stacks.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.stacks.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.stacks.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.stacks.suspend = mock.MagicMock()
|
||||
scheduler.TaskRunner(rsrc.suspend)()
|
||||
|
||||
self.assertEqual((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
|
||||
self.heat.stacks.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.stacks.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)
|
||||
self.heat.stacks.suspend.assert_called_with(stack_id=rsrc.resource_id)
|
||||
|
||||
def test_suspend_failed_not_created(self):
|
||||
self.initialize()
|
||||
rsrc = self.parent['remote_stack']
|
||||
# Note: the resource is not created so far
|
||||
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)
|
||||
|
||||
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'])
|
||||
registry = rsrc.stack.env.registry
|
||||
env = environment.get_custom_environment(registry, {'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_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))
|
||||
error_msg = _('ResourceUnknownStatus: Resource failed - Unknown '
|
||||
'status UPDATE_COMPLETE')
|
||||
self.assertEqual(error_msg, six.text_type(error))
|
||||
self.heat.stacks.delete.assert_called_with(stack_id=remote_stack_id)
|
||||
@@ -85,16 +85,16 @@ def dummy_context(user='test_username', tenant_id='test_tenant_id',
|
||||
})
|
||||
|
||||
|
||||
def parse_stack(t, params=None, stack_name='test_stack', stack_id=None,
|
||||
timeout_mins=None):
|
||||
def parse_stack(t, params=None, files=None, stack_name='test_stack',
|
||||
stack_id=None, timeout_mins=None):
|
||||
params = params or {}
|
||||
files = files or {}
|
||||
ctx = dummy_context()
|
||||
templ = template.Template(t)
|
||||
templ = template.Template(t, files=files)
|
||||
stk = stack.Stack(ctx, stack_name, templ,
|
||||
environment.Environment(params), stack_id,
|
||||
timeout_mins=timeout_mins)
|
||||
stk.store()
|
||||
|
||||
return stk
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user