heat/heat/tests/engine/service/test_stack_update.py

1280 lines
55 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.
from unittest import mock
import uuid
import eventlet.queue
from oslo_config import cfg
from oslo_messaging import conffixture
from oslo_messaging.rpc import dispatcher
from heat.common import context
from heat.common import environment_util as env_util
from heat.common import exception
from heat.common import messaging
from heat.common import service_utils
from heat.common import template_format
from heat.db import api as db_api
from heat.engine.clients.os import glance
from heat.engine.clients.os import nova
from heat.engine.clients.os import swift
from heat.engine import environment
from heat.engine import resource
from heat.engine import service
from heat.engine import stack
from heat.engine import stack_lock
from heat.engine import template as templatem
from heat.objects import stack as stack_object
from heat.rpc import api as rpc_api
from heat.tests import common
from heat.tests.engine import tools
from heat.tests import utils
class ServiceStackUpdateTest(common.HeatTestCase):
def setUp(self):
super(ServiceStackUpdateTest, self).setUp()
self.useFixture(conffixture.ConfFixture(cfg.CONF))
self.patchobject(context, 'StoredContext')
self.ctx = utils.dummy_context()
self.man = service.EngineService('a-host', 'a-topic')
self.man.thread_group_mgr = tools.DummyThreadGroupManager()
def test_stack_update(self):
stack_name = 'service_update_test_stack'
params = {'foo': 'bar'}
template = '{ "Template": "data" }'
old_stack = tools.get_stack(stack_name, self.ctx)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
mock_validate = self.patchobject(stk, 'validate', return_value=None)
msgq_mock = mock.Mock()
self.patchobject(eventlet.queue, 'LightQueue',
side_effect=[msgq_mock, eventlet.queue.LightQueue()])
# do update
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: True}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
template, params, None, api_args)
# assertions
self.assertEqual(old_stack.identifier(), result)
self.assertIsInstance(result, dict)
self.assertTrue(result['stack_id'])
self.assertEqual([msgq_mock], self.man.thread_group_mgr.msg_queues)
mock_tmpl.assert_called_once_with(template, files=None)
mock_env.assert_called_once_with(params)
mock_stack.assert_called_once_with(
self.ctx, stk.name, stk.t,
convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None,
current_deps=None,
disable_rollback=True,
nested_depth=0,
owner_id=None,
parent_resource=None,
stack_user_project_id='1234',
strict_validate=True,
tenant_id='test_tenant_id',
timeout_mins=60,
user_creds_id=u'1',
username='test_username',
converge=True
)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
mock_validate.assert_called_once_with()
def _test_stack_update_with_environment_files(self, stack_name,
files_container=None):
# Setup
params = {}
template = '{ "Template": "data" }'
old_stack = tools.get_stack(stack_name, self.ctx)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx)
# prepare mocks
self.patchobject(stack, 'Stack', return_value=stk)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=stk.t)
self.patchobject(environment, 'Environment', return_value=stk.env)
self.patchobject(stk, 'validate', return_value=None)
self.patchobject(eventlet.queue, 'LightQueue',
side_effect=[mock.Mock(),
eventlet.queue.LightQueue()])
mock_merge = self.patchobject(env_util, 'merge_environments')
files = None
if files_container:
files = {'/env/test.yaml': "{'resource_registry': {}}"}
# Test
environment_files = ['env_1']
self.man.update_stack(self.ctx, old_stack.identifier(),
template, params, None,
{rpc_api.PARAM_CONVERGE: False},
environment_files=environment_files,
files_container=files_container)
# Verify
mock_merge.assert_called_once_with(environment_files, files,
params, mock.ANY)
def test_stack_update_with_environment_files(self):
stack_name = 'service_update_env_files_stack'
self._test_stack_update_with_environment_files(stack_name)
def test_stack_update_with_files_container(self):
stack_name = 'env_files_test_stack'
files_container = 'test_container'
fake_get_object = (None, "{'resource_registry': {}}")
fake_get_container = ({'x-container-bytes-used': 100},
[{'name': '/env/test.yaml'}])
mock_client = mock.Mock()
mock_client.get_object.return_value = fake_get_object
mock_client.get_container.return_value = fake_get_container
self.patchobject(swift.SwiftClientPlugin, '_create',
return_value=mock_client)
self._test_stack_update_with_environment_files(
stack_name, files_container=files_container)
mock_client.get_container.assert_called_with(files_container)
mock_client.get_object.assert_called_with(files_container,
'/env/test.yaml')
def test_stack_update_nested(self):
stack_name = 'service_update_nested_test_stack'
parent_stack = tools.get_stack(stack_name + '_parent', self.ctx)
owner_id = parent_stack.store()
old_stack = tools.get_stack(stack_name, self.ctx,
owner_id=owner_id, nested_depth=1,
user_creds_id=parent_stack.user_creds_id)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx)
tmpl_id = stk.t.store(self.ctx)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem.Template, 'load',
return_value=stk.t)
mock_validate = self.patchobject(stk, 'validate', return_value=None)
msgq_mock = mock.Mock()
self.patchobject(eventlet.queue, 'LightQueue',
side_effect=[msgq_mock, eventlet.queue.LightQueue()])
# do update
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
None, None, None, api_args,
template_id=tmpl_id)
# assertions
self.assertEqual(old_stack.identifier(), result)
self.assertIsInstance(result, dict)
self.assertTrue(result['stack_id'])
self.assertEqual([msgq_mock], self.man.thread_group_mgr.msg_queues)
mock_tmpl.assert_called_once_with(self.ctx, tmpl_id)
mock_stack.assert_called_once_with(
self.ctx, stk.name, stk.t,
convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None,
current_deps=None,
disable_rollback=True,
nested_depth=1,
owner_id=owner_id,
parent_resource=None,
stack_user_project_id='1234',
strict_validate=True,
tenant_id='test_tenant_id',
timeout_mins=60,
user_creds_id=u'1',
username='test_username',
converge=False
)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
mock_validate.assert_called_once_with()
def test_stack_update_existing_parameters(self):
# Use a template with existing parameters, then update the stack
# with a template containing additional parameters and ensure all
# are preserved.
stack_name = 'service_update_test_stack_existing_parameters'
update_params = {'encrypted_param_names': [],
'parameter_defaults': {},
'event_sinks': [],
'parameters': {'newparam': 123},
'resource_registry': {'resources': {}}}
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
stk = tools.get_stack(stack_name, self.ctx, with_params=True)
stk.store()
stk.set_stack_user_project_id('1234')
self.assertEqual({'KeyName': 'test'}, stk.t.env.params)
t['parameters']['newparam'] = {'type': 'number'}
with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = stk
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(),
t,
update_params,
None, api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual({'KeyName': 'test', 'newparam': 123},
tmpl.env.params)
self.assertEqual(stk.identifier(), result)
def test_stack_update_existing_encrypted_parameters(self):
# Create the stack with encryption enabled
# On update encrypted_param_names should be used from existing stack
hidden_param_template = u'''
heat_template_version: 2013-05-23
parameters:
param2:
type: string
description: value2.
hidden: true
resources:
a_resource:
type: GenericResourceType
'''
cfg.CONF.set_override('encrypt_parameters_and_properties', True)
stack_name = 'service_update_test_stack_encrypted_parameters'
t = template_format.parse(hidden_param_template)
env1 = environment.Environment({'param2': 'bar'})
stk = stack.Stack(self.ctx, stack_name,
templatem.Template(t, env=env1))
stk.store()
stk.set_stack_user_project_id('1234')
# Verify that hidden parameters are stored encrypted
db_tpl = db_api.raw_template_get(self.ctx, stk.t.id)
db_params = db_tpl.environment['parameters']
self.assertEqual('cryptography_decrypt_v1', db_params['param2'][0])
self.assertNotEqual("foo", db_params['param2'][1])
# Verify that loaded stack has decrypted paramters
loaded_stack = stack.Stack.load(self.ctx, stack_id=stk.id)
params = loaded_stack.t.env.params
self.assertEqual('bar', params.get('param2'))
update_params = {'encrypted_param_names': [],
'parameter_defaults': {},
'event_sinks': [],
'parameters': {},
'resource_registry': {'resources': {}}}
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
with mock.patch('heat.engine.stack.Stack') as mock_stack:
loaded_stack.update = mock.Mock()
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = loaded_stack
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(),
t,
update_params,
None, api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual({u'param2': u'bar'}, tmpl.env.params)
# encrypted_param_names must be passed from existing to new
# stack otherwise the updated stack won't decrypt the params
self.assertEqual([u'param2'], tmpl.env.encrypted_param_names)
self.assertEqual(stk.identifier(), result)
def test_stack_update_existing_parameters_remove(self):
"""Test case for updating stack with changed parameters.
Use a template with existing parameters, then update with a
template containing additional parameters and a list of
parameters to be removed.
"""
stack_name = 'service_update_test_stack_existing_parameters_remove'
update_params = {'encrypted_param_names': [],
'parameter_defaults': {},
'event_sinks': [],
'parameters': {'newparam': 123},
'resource_registry': {'resources': {}}}
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CLEAR_PARAMETERS: ['removeme'],
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
t['parameters']['removeme'] = {'type': 'string'}
stk = utils.parse_stack(t, stack_name=stack_name,
params={'KeyName': 'test', 'removeme': 'foo'})
stk.set_stack_user_project_id('1234')
self.assertEqual({'KeyName': 'test', 'removeme': 'foo'},
stk.t.env.params)
t['parameters']['newparam'] = {'type': 'number'}
with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = stk
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(),
t,
update_params,
None, api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual({'KeyName': 'test', 'newparam': 123},
tmpl.env.params)
self.assertEqual(stk.identifier(), result)
def test_stack_update_with_tags(self):
"""Test case for updating stack with tags.
Create a stack with tags, then update with/without
rpc_api.PARAM_EXISTING.
"""
stack_name = 'service_update_test_stack_existing_tags'
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True}
t = template_format.parse(tools.wp_template)
stk = utils.parse_stack(t, stack_name=stack_name, tags=['tag1'])
stk.set_stack_user_project_id('1234')
self.assertEqual(['tag1'], stk.tags)
self.patchobject(stack.Stack, 'validate')
# update keep old tags
_, _, updated_stack = self.man._prepare_stack_updates(
self.ctx, stk, t, {}, None, None, None, api_args, None)
self.assertEqual(['tag1'], updated_stack.tags)
# update clear old tags
api_args[rpc_api.STACK_TAGS] = []
_, _, updated_stack = self.man._prepare_stack_updates(
self.ctx, stk, t, {}, None, None, None, api_args, None)
self.assertEqual([], updated_stack.tags)
# with new tags
api_args[rpc_api.STACK_TAGS] = ['tag2']
_, _, updated_stack = self.man._prepare_stack_updates(
self.ctx, stk, t, {}, None, None, None, api_args, None)
self.assertEqual(['tag2'], updated_stack.tags)
api_args[rpc_api.STACK_TAGS] = ['tag3']
_, _, updated_stack = self.man._prepare_stack_updates(
self.ctx, stk, t, {}, None, None, None, api_args, None)
self.assertEqual(['tag3'], updated_stack.tags)
# with no PARAM_EXISTING flag and no tags
del api_args[rpc_api.PARAM_EXISTING]
del api_args[rpc_api.STACK_TAGS]
_, _, updated_stack = self.man._prepare_stack_updates(
self.ctx, stk, t, {}, None, None, None, api_args, None)
self.assertEqual([], updated_stack.tags)
def test_stack_update_existing_registry(self):
# Use a template with existing flag and ensure the
# environment registry is preserved.
stack_name = 'service_update_test_stack_existing_registry'
intital_registry = {'OS::Foo': 'foo.yaml',
'OS::Foo2': 'foo2.yaml',
'resources': {
'myserver': {'OS::Server': 'myserver.yaml'}}}
intial_params = {'encrypted_param_names': [],
'parameter_defaults': {},
'parameters': {},
'event_sinks': [],
'resource_registry': intital_registry}
initial_files = {'foo.yaml': 'foo',
'foo2.yaml': 'foo2',
'myserver.yaml': 'myserver'}
update_registry = {'OS::Foo2': 'newfoo2.yaml',
'resources': {
'myother': {'OS::Other': 'myother.yaml'}}}
update_params = {'encrypted_param_names': [],
'parameter_defaults': {},
'parameters': {},
'resource_registry': update_registry}
update_files = {'newfoo2.yaml': 'newfoo',
'myother.yaml': 'myother'}
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
stk = utils.parse_stack(t, stack_name=stack_name, params=intial_params,
files=initial_files)
stk.set_stack_user_project_id('1234')
self.assertEqual(intial_params, stk.t.env.env_as_dict())
expected_reg = {'OS::Foo': 'foo.yaml',
'OS::Foo2': 'newfoo2.yaml',
'resources': {
'myother': {'OS::Other': 'myother.yaml'},
'myserver': {'OS::Server': 'myserver.yaml'}}}
expected_env = {'encrypted_param_names': [],
'parameter_defaults': {},
'parameters': {},
'event_sinks': [],
'resource_registry': expected_reg}
# FIXME(shardy): Currently we don't prune unused old files
expected_files = {'foo.yaml': 'foo',
'foo2.yaml': 'foo2',
'myserver.yaml': 'myserver',
'newfoo2.yaml': 'newfoo',
'myother.yaml': 'myother'}
with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = stk
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(),
t,
update_params,
update_files,
api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual(expected_env,
tmpl.env.env_as_dict())
self.assertEqual(expected_files,
tmpl.files.files)
self.assertEqual(stk.identifier(), result)
def test_stack_update_existing_parameter_defaults(self):
"""Ensure the environment parameter_defaults are preserved.
Use a template with existing flag and ensure the environment
parameter_defaults are preserved.
"""
stack_name = 'service_update_test_stack_existing_param_defaults'
intial_params = {'encrypted_param_names': [],
'parameter_defaults': {'mydefault': 123},
'parameters': {},
'resource_registry': {}}
update_params = {'encrypted_param_names': [],
'parameter_defaults': {'default2': 456},
'parameters': {},
'resource_registry': {}}
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
stk = utils.parse_stack(t, stack_name=stack_name, params=intial_params)
stk.set_stack_user_project_id('1234')
expected_env = {'encrypted_param_names': [],
'parameter_defaults': {
'mydefault': 123,
'default2': 456},
'parameters': {},
'event_sinks': [],
'resource_registry': {'resources': {}}}
with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = stk
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(),
t,
update_params,
None, api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual(expected_env,
tmpl.env.env_as_dict())
self.assertEqual(stk.identifier(), result)
def test_stack_update_reuses_api_params(self):
stack_name = 'service_update_stack_reuses_api_params'
params = {'foo': 'bar'}
template = '{ "Template": "data" }'
old_stack = tools.get_stack(stack_name, self.ctx)
old_stack.timeout_mins = 1
old_stack.disable_rollback = False
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
mock_validate = self.patchobject(stk, 'validate', return_value=None)
# do update
result = self.man.update_stack(self.ctx, old_stack.identifier(),
template, params, None,
{rpc_api.PARAM_CONVERGE: False})
# assertions
self.assertEqual(old_stack.identifier(), result)
self.assertIsInstance(result, dict)
self.assertTrue(result['stack_id'])
mock_validate.assert_called_once_with()
mock_tmpl.assert_called_once_with(template, files=None)
mock_env.assert_called_once_with(params)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
mock_stack.assert_called_once_with(
self.ctx, stk.name, stk.t,
convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None, current_deps=None,
disable_rollback=False, nested_depth=0,
owner_id=None, parent_resource=None,
stack_user_project_id='1234',
strict_validate=True,
tenant_id='test_tenant_id', timeout_mins=1,
user_creds_id=u'1',
username='test_username',
converge=False
)
def test_stack_cancel_update_same_engine(self):
stack_name = 'service_update_stack_test_cancel_same_engine'
stk = tools.get_stack(stack_name, self.ctx)
stk.state_set(stk.UPDATE, stk.IN_PROGRESS, 'test_override')
stk.disable_rollback = False
stk.store()
self.man.engine_id = service_utils.generate_engine_id()
self.patchobject(stack.Stack, 'load', return_value=stk)
self.patchobject(stack_lock.StackLock, 'get_engine_id',
return_value=self.man.engine_id)
self.patchobject(self.man.thread_group_mgr, 'send')
self.man.stack_cancel_update(self.ctx, stk.identifier(),
cancel_with_rollback=False)
self.man.thread_group_mgr.send.assert_called_once_with(stk.id,
'cancel')
def test_stack_cancel_update_different_engine(self):
stack_name = 'service_update_stack_test_cancel_different_engine'
stk = tools.get_stack(stack_name, self.ctx)
stk.state_set(stk.UPDATE, stk.IN_PROGRESS, 'test_override')
stk.disable_rollback = False
stk.store()
self.patchobject(stack.Stack, 'load', return_value=stk)
self.patchobject(stack_lock.StackLock, 'get_engine_id',
return_value=str(uuid.uuid4()))
self.patchobject(service_utils, 'engine_alive',
return_value=True)
self.man.listener = mock.Mock()
self.man.listener.SEND = 'send'
self.man._client = messaging.get_rpc_client(
version=self.man.RPC_API_VERSION)
# In fact the another engine is not alive, so the call will timeout
self.assertRaises(dispatcher.ExpectedException,
self.man.stack_cancel_update,
self.ctx, stk.identifier())
def test_stack_cancel_update_no_lock(self):
stack_name = 'service_update_stack_test_cancel_same_engine'
stk = tools.get_stack(stack_name, self.ctx)
stk.state_set(stk.UPDATE, stk.IN_PROGRESS, 'test_override')
stk.disable_rollback = False
stk.store()
self.patchobject(stack.Stack, 'load', return_value=stk)
self.patchobject(stack_lock.StackLock, 'get_engine_id',
return_value=None)
self.patchobject(self.man.thread_group_mgr, 'send')
self.man.stack_cancel_update(self.ctx, stk.identifier(),
cancel_with_rollback=False)
self.assertFalse(self.man.thread_group_mgr.send.called)
def test_stack_cancel_update_wrong_state_fails(self):
stack_name = 'service_update_cancel_test_stack'
stk = tools.get_stack(stack_name, self.ctx)
stk.state_set(stk.UPDATE, stk.COMPLETE, 'test_override')
stk.store()
self.patchobject(stack.Stack, 'load', return_value=stk)
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.stack_cancel_update,
self.ctx, stk.identifier())
self.assertEqual(exception.NotSupported, ex.exc_info[0])
self.assertIn("Cancelling update when stack is "
"UPDATE_COMPLETE",
str(ex.exc_info[1]))
@mock.patch.object(stack_object.Stack, 'count_total_resources')
def test_stack_update_equals(self, ctr):
stack_name = 'test_stack_update_equals_resource_limit'
params = {}
tpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {
'A': {'Type': 'GenericResourceType'},
'B': {'Type': 'GenericResourceType'},
'C': {'Type': 'GenericResourceType'}}}
template = templatem.Template(tpl)
old_stack = stack.Stack(self.ctx, stack_name, template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
ctr.return_value = 3
stk = stack.Stack(self.ctx, stack_name, template)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
mock_validate = self.patchobject(stk, 'validate', return_value=None)
# do update
cfg.CONF.set_override('max_resources_per_stack', 3)
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
template, params, None, api_args)
# assertions
self.assertEqual(old_stack.identifier(), result)
self.assertIsInstance(result, dict)
self.assertTrue(result['stack_id'])
root_stack_id = old_stack.root_stack_id()
self.assertEqual(3, old_stack.total_resources(root_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, stk.name, stk.t,
convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None, current_deps=None,
disable_rollback=True, nested_depth=0,
owner_id=None, parent_resource=None,
stack_user_project_id='1234', strict_validate=True,
tenant_id='test_tenant_id',
timeout_mins=60, user_creds_id=u'1',
username='test_username',
converge=False
)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
mock_validate.assert_called_once_with()
def test_stack_update_stack_id_equal(self):
stack_name = 'test_stack_update_stack_id_equal'
tpl = {
'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {
'A': {
'Type': 'ResourceWithPropsType',
'Properties': {
'Foo': {'Ref': 'AWS::StackId'}
}
}
}
}
template = templatem.Template(tpl)
create_stack = stack.Stack(self.ctx, stack_name, template)
sid = create_stack.store()
create_stack.create()
self.assertEqual((create_stack.CREATE, create_stack.COMPLETE),
create_stack.state)
create_stack._persist_state()
s = stack_object.Stack.get_by_id(self.ctx, sid)
old_stack = stack.Stack.load(self.ctx, stack=s)
self.assertEqual((old_stack.CREATE, old_stack.COMPLETE),
old_stack.state)
self.assertEqual(create_stack.identifier().arn(),
old_stack['A'].properties['Foo'])
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
result = self.man.update_stack(self.ctx, create_stack.identifier(),
tpl, {}, None,
{rpc_api.PARAM_CONVERGE: False})
old_stack._persist_state()
self.assertEqual((old_stack.UPDATE, old_stack.COMPLETE),
old_stack.state)
self.assertEqual(create_stack.identifier(), result)
self.assertIsNotNone(create_stack.identifier().stack_id)
self.assertEqual(create_stack.identifier().arn(),
old_stack['A'].properties['Foo'])
self.assertEqual(create_stack['A'].id, old_stack['A'].id)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
def test_stack_update_exceeds_resource_limit(self):
self.patchobject(context, 'StoredContext')
stack_name = 'test_stack_update_exceeds_resource_limit'
params = {}
tpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {
'A': {'Type': 'GenericResourceType'},
'B': {'Type': 'GenericResourceType'},
'C': {'Type': 'GenericResourceType'}}}
template = templatem.Template(tpl)
old_stack = stack.Stack(self.ctx, stack_name, template)
sid = old_stack.store()
self.assertIsNotNone(sid)
cfg.CONF.set_override('max_resources_per_stack', 2)
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack, self.ctx,
old_stack.identifier(), tpl, params,
None, {rpc_api.PARAM_CONVERGE: False})
self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
self.assertIn(exception.StackResourceLimitExceeded.msg_fmt,
str(ex.exc_info[1]))
def test_stack_update_verify_err(self):
stack_name = 'service_update_verify_err_test_stack'
params = {'foo': 'bar'}
template = '{ "Template": "data" }'
old_stack = tools.get_stack(stack_name, self.ctx)
old_stack.store()
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
ex_expected = exception.StackValidationFailed(message='fubar')
mock_validate = self.patchobject(stk, 'validate',
side_effect=ex_expected)
# do update
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False}
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack,
self.ctx, old_stack.identifier(),
template, params, None, api_args)
# assertions
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, stk.name, stk.t,
convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None, current_deps=None,
disable_rollback=True, nested_depth=0,
owner_id=None, parent_resource=None,
stack_user_project_id='1234', strict_validate=True,
tenant_id='test_tenant_id',
timeout_mins=60, user_creds_id=u'1',
username='test_username',
converge=False
)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
mock_validate.assert_called_once_with()
def test_stack_update_nonexist(self):
stack_name = 'service_update_nonexist_test_stack'
params = {'foo': 'bar'}
template = '{ "Template": "data" }'
stk = tools.get_stack(stack_name, self.ctx)
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack,
self.ctx, stk.identifier(), template,
params, None, {rpc_api.PARAM_CONVERGE: False})
self.assertEqual(exception.EntityNotFound, ex.exc_info[0])
def test_stack_update_no_credentials(self):
cfg.CONF.set_default('deferred_auth_method', 'password')
stack_name = 'test_stack_update_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
sid = stk.store()
stk.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
self.ctx = utils.dummy_context(password=None)
# prepare mocks
mock_get = self.patchobject(self.man, '_get_stack', return_value=s)
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load', return_value=stk)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False}
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack, self.ctx,
stk.identifier(),
template, params, None, api_args)
self.assertEqual(exception.MissingCredentialError, ex.exc_info[0])
self.assertEqual('Missing required credential: X-Auth-Key',
str(ex.exc_info[1]))
mock_get.assert_called_once_with(self.ctx, stk.identifier())
mock_tmpl.assert_called_once_with(template, files=None)
mock_env.assert_called_once_with(params)
mock_stack.assert_called_once_with(
self.ctx, stk.name, stk.t,
convergence=False, current_traversal=stk.current_traversal,
prev_raw_template_id=None, current_deps=None,
disable_rollback=True, nested_depth=0,
owner_id=None, parent_resource=None,
stack_user_project_id='1234',
strict_validate=True,
tenant_id='test_tenant_id', timeout_mins=60,
user_creds_id=u'1', username='test_username',
converge=False
)
mock_load.assert_called_once_with(self.ctx, stack=s,
check_refresh_cred=True)
def test_stack_update_existing_template(self):
'''Update a stack using the same template.'''
stack_name = 'service_update_test_stack_existing_template'
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
# Don't actually run the update as the mocking breaks it, instead
# we just ensure the expected template is passed in to the updated
# template, and that the update task is scheduled.
self.man.thread_group_mgr = tools.DummyThreadGroupMgrLogStart()
params = {}
stack = utils.parse_stack(t, stack_name=stack_name,
params=params)
stack.set_stack_user_project_id('1234')
self.assertEqual(t, stack.t.t)
stack.action = stack.CREATE
stack.status = stack.COMPLETE
with mock.patch('heat.engine.stack.Stack') as mock_stack:
self.patchobject(service, 'NotifyEvent')
mock_stack.load.return_value = stack
mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stack.identifier(),
None,
params,
None, api_args)
tmpl = mock_stack.call_args[0][2]
self.assertEqual(t,
tmpl.t)
self.assertEqual(stack.identifier(), result)
self.assertEqual(1, len(self.man.thread_group_mgr.started))
def test_stack_update_existing_failed(self):
'''Update a stack using the same template doesn't work when FAILED.'''
stack_name = 'service_update_test_stack_existing_template'
api_args = {rpc_api.PARAM_TIMEOUT: 60,
rpc_api.PARAM_EXISTING: True,
rpc_api.PARAM_CONVERGE: False}
t = template_format.parse(tools.wp_template)
# Don't actually run the update as the mocking breaks it, instead
# we just ensure the expected template is passed in to the updated
# template, and that the update task is scheduled.
self.man.thread_group_mgr = tools.DummyThreadGroupMgrLogStart()
params = {}
stack = utils.parse_stack(t, stack_name=stack_name,
params=params)
stack.set_stack_user_project_id('1234')
self.assertEqual(t, stack.t.t)
stack.action = stack.UPDATE
stack.status = stack.FAILED
ex = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack,
self.ctx, stack.identifier(),
None, params, None, api_args)
self.assertEqual(exception.NotSupported, ex.exc_info[0])
self.assertIn("PATCH update to non-COMPLETE stack",
str(ex.exc_info[1]))
def test_update_immutable_parameter_disallowed(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: true
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
params = {'param1': 'bar'}
exc = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack,
self.ctx, old_stack.identifier(),
old_stack.t.t, params,
None, {rpc_api.PARAM_CONVERGE: False})
self.assertEqual(exception.ImmutableParameterModified, exc.exc_info[0])
self.assertEqual('The following parameters are immutable and may not '
'be updated: param1', exc.exc_info[1].message)
def test_update_mutable_parameter_allowed(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: false
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
params = {}
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=old_stack.t)
self.patchobject(environment, 'Environment',
return_value=old_stack.env)
params = {'param1': 'bar'}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
templatem.Template(template), params,
None, {rpc_api.PARAM_CONVERGE: False})
self.assertEqual(s.id, result['stack_id'])
def test_update_immutable_parameter_same_value(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: true
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
params = {}
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=old_stack.t)
self.patchobject(environment, 'Environment',
return_value=old_stack.env)
params = {'param1': 'foo'}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
templatem.Template(template), params,
None, {rpc_api.PARAM_CONVERGE: False})
self.assertEqual(s.id, result['stack_id'])
class ServiceStackUpdatePreviewTest(common.HeatTestCase):
old_tmpl = """
heat_template_version: 2014-10-16
resources:
web_server:
type: OS::Nova::Server
properties:
image: F17-x86_64-gold
flavor: m1.large
key_name: test
user_data: wordpress
"""
new_tmpl = """
heat_template_version: 2014-10-16
resources:
web_server:
type: OS::Nova::Server
properties:
image: F17-x86_64-gold
flavor: m1.large
key_name: test
user_data: wordpress
password:
type: OS::Heat::RandomString
properties:
length: 8
"""
def setUp(self):
super(ServiceStackUpdatePreviewTest, self).setUp()
self.ctx = utils.dummy_context()
self.man = service.EngineService('a-host', 'a-topic')
self.man.thread_group_mgr = tools.DummyThreadGroupManager()
def _test_stack_update_preview(self, orig_template, new_template,
environment_files=None):
stack_name = 'service_update_test_stack_preview'
params = {'foo': 'bar'}
def side_effect(*args):
return 2 if args[0] == 'm1.small' else 1
self.patchobject(nova.NovaClientPlugin, 'find_flavor_by_name_or_id',
side_effect=side_effect)
self.patchobject(glance.GlanceClientPlugin, 'find_image_by_name_or_id',
return_value=1)
old_stack = tools.get_stack(stack_name, self.ctx,
template=orig_template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
stk = tools.get_stack(stack_name, self.ctx, template=new_template)
# prepare mocks
mock_stack = self.patchobject(stack, 'Stack', return_value=stk)
mock_load = self.patchobject(stack.Stack, 'load',
return_value=old_stack)
mock_tmpl = self.patchobject(templatem, 'Template', return_value=stk.t)
mock_env = self.patchobject(environment, 'Environment',
return_value=stk.env)
mock_validate = self.patchobject(stk, 'validate', return_value=None)
mock_merge = self.patchobject(env_util, 'merge_environments')
# Patch _resolve_any_attribute or it tries to call novaclient
self.patchobject(resource.Resource, '_resolve_any_attribute',
return_value=None)
# do preview_update_stack
api_args = {'timeout_mins': 60, rpc_api.PARAM_CONVERGE: False}
result = self.man.preview_update_stack(
self.ctx,
old_stack.identifier(),
new_template, params, None,
api_args,
environment_files=environment_files)
# assertions
mock_stack.assert_called_once_with(
self.ctx, stk.name, stk.t, convergence=False,
current_traversal=old_stack.current_traversal,
prev_raw_template_id=None, current_deps=None,
disable_rollback=True, nested_depth=0, owner_id=None,
parent_resource=None, stack_user_project_id='1234',
strict_validate=True, tenant_id='test_tenant_id', timeout_mins=60,
user_creds_id=u'1', username='test_username',
converge=False
)
mock_load.assert_called_once_with(self.ctx, stack=s)
mock_tmpl.assert_called_once_with(new_template, files=None)
mock_env.assert_called_once_with(params)
mock_validate.assert_called_once_with()
if environment_files:
mock_merge.assert_called_once_with(environment_files, None,
params, mock.ANY)
return result
def test_stack_update_preview_added_unchanged(self):
result = self._test_stack_update_preview(self.old_tmpl, self.new_tmpl)
added = [x for x in result['added']][0]
self.assertEqual('password', added['resource_name'])
unchanged = [x for x in result['unchanged']][0]
self.assertEqual('web_server', unchanged['resource_name'])
self.assertNotEqual('None', unchanged['resource_identity']['stack_id'])
empty_sections = ('deleted', 'replaced', 'updated')
for section in empty_sections:
section_contents = [x for x in result[section]]
self.assertEqual([], section_contents)
def test_stack_update_preview_replaced(self):
# new template with a different key_name
new_tmpl = self.old_tmpl.replace('test', 'test2')
result = self._test_stack_update_preview(self.old_tmpl, new_tmpl)
replaced = [x for x in result['replaced']][0]
self.assertEqual('web_server', replaced['resource_name'])
empty_sections = ('added', 'deleted', 'unchanged', 'updated')
for section in empty_sections:
section_contents = [x for x in result[section]]
self.assertEqual([], section_contents)
def test_stack_update_preview_replaced_type(self):
# new template with a different type for web_server
new_tmpl = self.old_tmpl.replace('OS::Nova::Server', 'OS::Heat::None')
result = self._test_stack_update_preview(self.old_tmpl, new_tmpl)
replaced = [x for x in result['replaced']][0]
self.assertEqual('web_server', replaced['resource_name'])
empty_sections = ('added', 'deleted', 'unchanged', 'updated')
for section in empty_sections:
section_contents = [x for x in result[section]]
self.assertEqual([], section_contents)
def test_stack_update_preview_updated(self):
# new template changes to flavor of server
new_tmpl = self.old_tmpl.replace('m1.large', 'm1.small')
result = self._test_stack_update_preview(self.old_tmpl, new_tmpl)
updated = [x for x in result['updated']][0]
self.assertEqual('web_server', updated['resource_name'])
empty_sections = ('added', 'deleted', 'unchanged', 'replaced')
for section in empty_sections:
section_contents = [x for x in result[section]]
self.assertEqual([], section_contents)
def test_stack_update_preview_deleted(self):
# do the reverse direction, i.e. delete resources
result = self._test_stack_update_preview(self.new_tmpl, self.old_tmpl)
deleted = [x for x in result['deleted']][0]
self.assertEqual('password', deleted['resource_name'])
unchanged = [x for x in result['unchanged']][0]
self.assertEqual('web_server', unchanged['resource_name'])
empty_sections = ('added', 'updated', 'replaced')
for section in empty_sections:
section_contents = [x for x in result[section]]
self.assertEqual([], section_contents)
def test_stack_update_preview_with_environment_files(self):
# Setup
environment_files = ['env_1']
# Test
self._test_stack_update_preview(self.old_tmpl, self.new_tmpl,
environment_files=environment_files)
# Assertions done in _test_stack_update_preview
def test_reset_stack_and_resources_in_progress(self):
def mock_stack_resource(name, action, status):
rs = mock.MagicMock()
rs.name = name
rs.action = action
rs.status = status
rs.IN_PROGRESS = 'IN_PROGRESS'
rs.FAILED = 'FAILED'
def mock_resource_state_set(a, s, reason='engine_down'):
rs.status = s
rs.action = a
rs.status_reason = reason
rs.state_set = mock_resource_state_set
return rs
stk_name = 'test_stack'
stk = tools.get_stack(stk_name, self.ctx)
stk.action = 'CREATE'
stk.status = 'IN_PROGRESS'
resources = {'r1': mock_stack_resource('r1', 'UPDATE', 'COMPLETE'),
'r2': mock_stack_resource('r2', 'UPDATE', 'IN_PROGRESS'),
'r3': mock_stack_resource('r3', 'UPDATE', 'FAILED')}
stk._resources = resources
reason = 'Test resetting stack and resources in progress'
stk.reset_stack_and_resources_in_progress(reason)
self.assertEqual('FAILED', stk.status)
self.assertEqual('COMPLETE', stk.resources.get('r1').status)
self.assertEqual('FAILED', stk.resources.get('r2').status)
self.assertEqual('FAILED', stk.resources.get('r3').status)