You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
17 KiB
386 lines
17 KiB
#
|
|
# 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 mock
|
|
from oslo_config import cfg
|
|
from oslo_messaging.rpc import dispatcher
|
|
from oslo_service import threadgroup
|
|
import six
|
|
|
|
from heat.common import environment_util as env_util
|
|
from heat.common import exception
|
|
from heat.engine.clients.os import glance
|
|
from heat.engine.clients.os import nova
|
|
from heat.engine import environment
|
|
from heat.engine import properties
|
|
from heat.engine.resources.aws.ec2 import instance as instances
|
|
from heat.engine import service
|
|
from heat.engine import stack
|
|
from heat.engine import template as templatem
|
|
from heat.objects import stack as stack_object
|
|
from heat.tests import common
|
|
from heat.tests.engine import tools
|
|
from heat.tests.openstack.nova import fakes as fakes_nova
|
|
from heat.tests import utils
|
|
|
|
|
|
class StackCreateTest(common.HeatTestCase):
|
|
def setUp(self):
|
|
super(StackCreateTest, self).setUp()
|
|
self.ctx = utils.dummy_context()
|
|
self.man = service.EngineService('a-host', 'a-topic')
|
|
self.man.create_periodic_tasks()
|
|
|
|
@mock.patch.object(threadgroup, 'ThreadGroup')
|
|
@mock.patch.object(stack.Stack, 'validate')
|
|
def _test_stack_create(self, stack_name, mock_validate, mock_tg,
|
|
environment_files=None):
|
|
mock_tg.return_value = tools.DummyThreadGroup()
|
|
|
|
params = {'foo': 'bar'}
|
|
template = '{ "Template": "data" }'
|
|
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
|
|
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
|
|
mock_env = self.patchobject(environment, 'Environment',
|
|
return_value=stk.env)
|
|
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
|
|
mock_merge = self.patchobject(env_util, 'merge_environments')
|
|
result = self.man.create_stack(self.ctx, stack_name,
|
|
template, params, None, {},
|
|
environment_files=environment_files)
|
|
self.assertEqual(stk.identifier(), result)
|
|
self.assertIsInstance(result, dict)
|
|
self.assertTrue(result['stack_id'])
|
|
|
|
mock_tmpl.assert_called_once_with(template, files=None)
|
|
mock_env.assert_called_once_with(params)
|
|
mock_stack.assert_called_once_with(
|
|
self.ctx, stack_name, stk.t, owner_id=None, nested_depth=0,
|
|
user_creds_id=None, stack_user_project_id=None,
|
|
convergence=cfg.CONF.convergence_engine, parent_resource=None)
|
|
|
|
if environment_files:
|
|
mock_merge.assert_called_once_with(environment_files, None,
|
|
params, mock.ANY)
|
|
mock_validate.assert_called_once_with(validate_resources=True)
|
|
|
|
def test_stack_create(self):
|
|
stack_name = 'service_create_test_stack'
|
|
self._test_stack_create(stack_name)
|
|
|
|
def test_stack_create_with_environment_files(self):
|
|
stack_name = 'env_files_test_stack'
|
|
environment_files = ['env_1', 'env_2']
|
|
self._test_stack_create(stack_name,
|
|
environment_files=environment_files)
|
|
|
|
def test_stack_create_equals_max_per_tenant(self):
|
|
cfg.CONF.set_override('max_stacks_per_tenant', 1, enforce_type=True)
|
|
stack_name = 'service_create_test_stack_equals_max'
|
|
self._test_stack_create(stack_name)
|
|
|
|
def test_stack_create_exceeds_max_per_tenant(self):
|
|
cfg.CONF.set_override('max_stacks_per_tenant', 0, enforce_type=True)
|
|
stack_name = 'service_create_test_stack_exceeds_max'
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self._test_stack_create, stack_name)
|
|
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
|
|
self.assertIn("You have reached the maximum stacks per tenant",
|
|
six.text_type(ex.exc_info[1]))
|
|
|
|
@mock.patch.object(stack.Stack, 'validate')
|
|
def test_stack_create_verify_err(self, mock_validate):
|
|
mock_validate.side_effect = exception.StackValidationFailed(message='')
|
|
|
|
stack_name = 'service_create_verify_err_test_stack'
|
|
params = {'foo': 'bar'}
|
|
template = '{ "Template": "data" }'
|
|
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
|
|
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
|
|
mock_env = self.patchobject(environment, 'Environment',
|
|
return_value=stk.env)
|
|
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
|
|
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
self.ctx, stack_name, template, params,
|
|
None, {})
|
|
self.assertEqual(exception.StackValidationFailed, ex.exc_info[0])
|
|
|
|
mock_tmpl.assert_called_once_with(template, files=None)
|
|
mock_env.assert_called_once_with(params)
|
|
mock_stack.assert_called_once_with(
|
|
self.ctx, stack_name, stk.t, owner_id=None, nested_depth=0,
|
|
user_creds_id=None, stack_user_project_id=None,
|
|
convergence=cfg.CONF.convergence_engine, parent_resource=None)
|
|
|
|
def test_stack_create_invalid_stack_name(self):
|
|
stack_name = 'service_create_test_stack_invalid_name'
|
|
stack = tools.get_stack('test_stack', self.ctx)
|
|
|
|
self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
self.ctx, stack_name, stack.t.t, {}, None, {})
|
|
|
|
def test_stack_create_invalid_resource_name(self):
|
|
stack_name = 'stack_create_invalid_resource_name'
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
tmpl = dict(stk.t)
|
|
tmpl['resources']['Web/Server'] = tmpl['resources']['WebServer']
|
|
del tmpl['resources']['WebServer']
|
|
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
self.ctx, stack_name,
|
|
stk.t.t, {}, None, {})
|
|
self.assertEqual(exception.StackValidationFailed, ex.exc_info[0])
|
|
|
|
@mock.patch.object(stack.Stack, 'create_stack_user_project_id')
|
|
def test_stack_create_authorization_failure(self, mock_create):
|
|
stack_name = 'stack_create_authorization_failure'
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
mock_create.side_effect = exception.AuthorizationFailure
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
self.ctx, stack_name,
|
|
stk.t.t, {}, None, {})
|
|
self.assertEqual(exception.StackValidationFailed, ex.exc_info[0])
|
|
|
|
def test_stack_create_no_credentials(self):
|
|
cfg.CONF.set_default('deferred_auth_method', 'password')
|
|
stack_name = 'test_stack_create_no_credentials'
|
|
params = {'foo': 'bar'}
|
|
template = '{ "Template": "data" }'
|
|
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
# force check for credentials on create
|
|
stk['WebServer'].requires_deferred_auth = True
|
|
|
|
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
|
|
mock_env = self.patchobject(environment, 'Environment',
|
|
return_value=stk.env)
|
|
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
|
|
|
|
# test stack create using context without password
|
|
ctx_no_pwd = utils.dummy_context(password=None)
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
ctx_no_pwd, stack_name,
|
|
template, params, None, {}, None)
|
|
self.assertEqual(exception.MissingCredentialError, ex.exc_info[0])
|
|
self.assertEqual('Missing required credential: X-Auth-Key',
|
|
six.text_type(ex.exc_info[1]))
|
|
|
|
mock_tmpl.assert_called_once_with(template, files=None)
|
|
mock_env.assert_called_once_with(params)
|
|
mock_stack.assert_called_once_with(
|
|
ctx_no_pwd, stack_name, stk.t, owner_id=None, nested_depth=0,
|
|
user_creds_id=None, stack_user_project_id=None,
|
|
convergence=cfg.CONF.convergence_engine, parent_resource=None)
|
|
mock_tmpl.reset_mock()
|
|
mock_env.reset_mock()
|
|
mock_stack.reset_mock()
|
|
|
|
# test stack create using context without user
|
|
ctx_no_pwd = utils.dummy_context(password=None)
|
|
ctx_no_user = utils.dummy_context(user=None)
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack,
|
|
ctx_no_user, stack_name,
|
|
template, params, None, {})
|
|
self.assertEqual(exception.MissingCredentialError, ex.exc_info[0])
|
|
self.assertEqual('Missing required credential: X-Auth-User',
|
|
six.text_type(ex.exc_info[1]))
|
|
|
|
mock_tmpl.assert_called_once_with(template, files=None)
|
|
mock_env.assert_called_once_with(params)
|
|
mock_stack.assert_called_once_with(
|
|
ctx_no_user, stack_name, stk.t, owner_id=None, nested_depth=0,
|
|
user_creds_id=None, stack_user_project_id=None,
|
|
convergence=cfg.CONF.convergence_engine, parent_resource=None)
|
|
|
|
@mock.patch.object(stack_object.Stack, 'count_total_resources')
|
|
def test_stack_create_total_resources_equals_max(self, ctr):
|
|
stack_name = 'stack_create_total_resources_equals_max'
|
|
params = {}
|
|
tpl = {
|
|
'heat_template_version': '2014-10-16',
|
|
'resources': {
|
|
'A': {'type': 'GenericResourceType'},
|
|
'B': {'type': 'GenericResourceType'},
|
|
'C': {'type': 'GenericResourceType'}
|
|
}
|
|
}
|
|
|
|
template = templatem.Template(tpl)
|
|
stk = stack.Stack(self.ctx, stack_name, template)
|
|
ctr.return_value = 3
|
|
|
|
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
|
|
mock_env = self.patchobject(environment, 'Environment',
|
|
return_value=stk.env)
|
|
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
|
|
|
|
cfg.CONF.set_override('max_resources_per_stack', 3, enforce_type=True)
|
|
|
|
result = self.man.create_stack(self.ctx, stack_name, template, params,
|
|
None, {})
|
|
|
|
mock_tmpl.assert_called_once_with(template, files=None)
|
|
mock_env.assert_called_once_with(params)
|
|
mock_stack.assert_called_once_with(
|
|
self.ctx, stack_name, stk.t, owner_id=None, nested_depth=0,
|
|
user_creds_id=None, stack_user_project_id=None,
|
|
convergence=cfg.CONF.convergence_engine, parent_resource=None)
|
|
|
|
self.assertEqual(stk.identifier(), result)
|
|
root_stack_id = stk.root_stack_id()
|
|
self.assertEqual(3, stk.total_resources(root_stack_id))
|
|
self.man.thread_group_mgr.groups[stk.id].wait()
|
|
stk.delete()
|
|
|
|
def test_stack_create_total_resources_exceeds_max(self):
|
|
stack_name = 'stack_create_total_resources_exceeds_max'
|
|
params = {}
|
|
tpl = {
|
|
'heat_template_version': '2014-10-16',
|
|
'resources': {
|
|
'A': {'type': 'GenericResourceType'},
|
|
'B': {'type': 'GenericResourceType'},
|
|
'C': {'type': 'GenericResourceType'}
|
|
}
|
|
}
|
|
|
|
cfg.CONF.set_override('max_resources_per_stack', 2, enforce_type=True)
|
|
ex = self.assertRaises(dispatcher.ExpectedException,
|
|
self.man.create_stack, self.ctx, stack_name,
|
|
tpl, params, None, {})
|
|
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
|
|
self.assertIn(exception.StackResourceLimitExceeded.msg_fmt,
|
|
six.text_type(ex.exc_info[1]))
|
|
|
|
@mock.patch.object(threadgroup, 'ThreadGroup')
|
|
@mock.patch.object(stack.Stack, 'validate')
|
|
def test_stack_create_nested(self, mock_validate, mock_tg):
|
|
convergence_engine = cfg.CONF.convergence_engine
|
|
stack_name = 'service_create_nested_test_stack'
|
|
parent_stack = tools.get_stack(stack_name + '_parent', self.ctx)
|
|
owner_id = parent_stack.store()
|
|
mock_tg.return_value = tools.DummyThreadGroup()
|
|
|
|
stk = tools.get_stack(stack_name, self.ctx, with_params=True,
|
|
owner_id=owner_id, nested_depth=1)
|
|
tmpl_id = stk.t.store(self.ctx)
|
|
|
|
mock_load = self.patchobject(templatem.Template, 'load',
|
|
return_value=stk.t)
|
|
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
|
|
result = self.man.create_stack(self.ctx, stack_name, None,
|
|
None, None, {},
|
|
owner_id=owner_id, nested_depth=1,
|
|
template_id=tmpl_id)
|
|
self.assertEqual(stk.identifier(), result)
|
|
self.assertIsInstance(result, dict)
|
|
self.assertTrue(result['stack_id'])
|
|
|
|
mock_load.assert_called_once_with(self.ctx, tmpl_id)
|
|
mock_stack.assert_called_once_with(self.ctx, stack_name, stk.t,
|
|
owner_id=owner_id, nested_depth=1,
|
|
user_creds_id=None,
|
|
stack_user_project_id=None,
|
|
convergence=convergence_engine,
|
|
parent_resource=None)
|
|
|
|
mock_validate.assert_called_once_with(validate_resources=False)
|
|
|
|
def test_stack_validate(self):
|
|
stack_name = 'stack_create_test_validate'
|
|
stk = tools.get_stack(stack_name, self.ctx)
|
|
|
|
fc = fakes_nova.FakeClient()
|
|
self.patchobject(nova.NovaClientPlugin, '_create', return_value=fc)
|
|
self.patchobject(glance.GlanceClientPlugin, 'find_image_by_name_or_id',
|
|
return_value=744)
|
|
|
|
resource = stk['WebServer']
|
|
resource.properties = properties.Properties(
|
|
resource.properties_schema,
|
|
{
|
|
'ImageId': 'CentOS 5.2',
|
|
'KeyName': 'test',
|
|
'InstanceType': 'm1.large'
|
|
},
|
|
context=self.ctx)
|
|
stk.validate()
|
|
|
|
resource.properties = properties.Properties(
|
|
resource.properties_schema,
|
|
{
|
|
'KeyName': 'test',
|
|
'InstanceType': 'm1.large'
|
|
},
|
|
context=self.ctx)
|
|
self.assertRaises(exception.StackValidationFailed, stk.validate)
|
|
|
|
def test_validate_deferred_auth_context_trusts(self):
|
|
stk = tools.get_stack('test_deferred_auth', self.ctx)
|
|
stk['WebServer'].requires_deferred_auth = True
|
|
ctx = utils.dummy_context(user=None, password=None)
|
|
cfg.CONF.set_default('deferred_auth_method', 'trusts')
|
|
|
|
# using trusts, no username or password required
|
|
self.man._validate_deferred_auth_context(ctx, stk)
|
|
|
|
def test_validate_deferred_auth_context_not_required(self):
|
|
stk = tools.get_stack('test_deferred_auth', self.ctx)
|
|
stk['WebServer'].requires_deferred_auth = False
|
|
ctx = utils.dummy_context(user=None, password=None)
|
|
cfg.CONF.set_default('deferred_auth_method', 'password')
|
|
|
|
# stack performs no deferred operations, so no username or
|
|
# password required
|
|
self.man._validate_deferred_auth_context(ctx, stk)
|
|
|
|
def test_validate_deferred_auth_context_missing_credentials(self):
|
|
stk = tools.get_stack('test_deferred_auth', self.ctx)
|
|
stk['WebServer'].requires_deferred_auth = True
|
|
cfg.CONF.set_default('deferred_auth_method', 'password')
|
|
|
|
# missing username
|
|
ctx = utils.dummy_context(user=None)
|
|
ex = self.assertRaises(exception.MissingCredentialError,
|
|
self.man._validate_deferred_auth_context,
|
|
ctx, stk)
|
|
self.assertEqual('Missing required credential: X-Auth-User',
|
|
six.text_type(ex))
|
|
|
|
# missing password
|
|
ctx = utils.dummy_context(password=None)
|
|
ex = self.assertRaises(exception.MissingCredentialError,
|
|
self.man._validate_deferred_auth_context,
|
|
ctx, stk)
|
|
self.assertEqual('Missing required credential: X-Auth-Key',
|
|
six.text_type(ex))
|
|
|
|
@mock.patch.object(instances.Instance, 'validate')
|
|
@mock.patch.object(stack.Stack, 'total_resources')
|
|
def test_stack_create_max_unlimited(self, total_res_mock, validate_mock):
|
|
total_res_mock.return_value = 9999
|
|
validate_mock.return_value = None
|
|
cfg.CONF.set_override('max_resources_per_stack', -1, enforce_type=True)
|
|
stack_name = 'service_create_test_max_unlimited'
|
|
self._test_stack_create(stack_name)
|