murano/murano/tests/unit/test_heat_stack.py

731 lines
26 KiB
Python

# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# Copyright (c) 2016 AT&T Corp
#
# 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 heatclient.v1 import stacks
import mock
from oslo_config import cfg
from murano.engine.system import heat_stack
from murano.tests.unit import base
CLS_NAME = 'murano.engine.system.heat_stack.HeatStack'
CONF = cfg.CONF
class TestHeatStack(base.MuranoTestCase):
def setUp(self):
super(TestHeatStack, self).setUp()
self.heat_client_mock = mock.Mock()
self.heat_client_mock.stacks = mock.MagicMock(spec=stacks.StackManager)
self.override_config('stack_tags', ['test-murano'], 'heat')
self.mock_tag = ','.join(CONF.heat.stack_tags)
self._patch_get_client()
def tearDown(self):
super(TestHeatStack, self).tearDown()
self.addCleanup(mock.patch.stopall)
def _patch_get_client(self):
self.get_client_patcher = mock.patch(
'murano.engine.system.heat_stack.HeatStack._get_client',
return_value=self.heat_client_mock)
self.get_token_client_patcher = mock.patch.object(
heat_stack.HeatStack, '_get_token_client',
return_value=self.heat_client_mock)
self.get_client_patcher.start()
self.get_token_client_patcher.start()
def _unpatch_get_client(self):
self.get_client_patcher.stop()
self.get_token_client_patcher.stop()
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_push_adds_version(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = {}
hs._parameters = {}
hs._applied = False
hs.push()
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'description': 'Generated by TestHeatStack',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment={},
tags=self.mock_tag
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_description_is_optional(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=self.mock_tag
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_heat_files_are_sent(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"},
environment='',
tags=self.mock_tag
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_heat_environments_are_sent(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = 'environments'
hs._parameters = {}
hs._applied = False
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"},
environment='environments',
tags=self.mock_tag
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_heat_async_push(self, status_get, wait_st):
"""Assert that if heat_template_version is omitted, it's added."""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = 'environments'
hs._parameters = {}
hs._applied = False
with mock.patch('murano.dsl.dsl.get_execution_session'):
hs.push(is_async=True)
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_not_called()
hs.output()
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={"heatFile": "file"},
environment='environments',
tags=self.mock_tag
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
@mock.patch.object(heat_stack, 'LOG')
def test_push_except_http_conflict(self, mock_log, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {"heatFile": "file"}
hs._hot_environment = 'environments'
hs._parameters = {}
hs._applied = False
hs._get_token_client().stacks.create.side_effect = [
heat_stack.heat_exc.HTTPConflict('test_error_msg'),
None
]
hs.push()
mock_log.warning.assert_called_with(
'Conflicting operation: ERROR: test_error_msg')
@mock.patch(CLS_NAME + '.current')
def test_update_wrong_template_version(self, current):
"""Template version other than expected should cause error."""
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = {'resources': {'test': 1}}
invalid_template = {
'heat_template_version': 'something else'
}
current.return_value = {}
e = self.assertRaises(heat_stack.HeatStackError,
hs.update_template,
invalid_template)
err_msg = "Currently only heat_template_version 2013-05-23 "\
"is supported."
self.assertEqual(err_msg, str(e))
# Check it's ok without a version
hs.update_template({})
expected = {'resources': {'test': 1}}
self.assertEqual(expected, hs._template)
# .. or with a good version
hs.update_template({'heat_template_version': '2013-05-23'})
expected['heat_template_version'] = '2013-05-23'
self.assertEqual(expected, hs._template)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_heat_stack_tags_are_sent(self, status_get, wait_st):
"""Assert heat_stack tags are sent
Assert that heat_stack `tags` parameter get push & with
value from config parameter `stack_tags`.
"""
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_parameters(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
self.assertEqual(hs.parameters(), hs._parameters)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_reload(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
hs.reload()
stack_info = self.heat_client_mock.stacks.get(stack_id=hs._name)
self.assertEqual(hs._template, hs._client.stacks.template(
stack_id='{0}/{1}'.format(
stack_info.stack_name,
stack_info.id)))
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_delete(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
hs.delete()
self.assertEqual({}, hs._template)
self.assertTrue(hs._applied)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
@mock.patch.object(heat_stack, 'LOG')
def test_delete_except_not_found(self, mock_log, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
hs._client.stacks.delete.side_effect =\
heat_stack.heat_exc.NotFound
hs.delete()
self.assertTrue(hs._applied)
self.assertEqual({}, hs._template)
mock_log.warning.assert_called_with(
'Stack test-stack already deleted?')
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
@mock.patch.object(heat_stack, 'LOG')
def test_delete_except_http_conflict(self, mock_log, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
hs = heat_stack.HeatStack('test-stack', None)
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs.push()
hs._client.stacks.delete.side_effect = [
heat_stack.heat_exc.HTTPConflict('test_error_msg'),
None
]
hs.delete()
self.assertTrue(hs._applied)
self.assertEqual({}, hs._template)
mock_log.warning.assert_called_with('Conflicting operation: '
'ERROR: test_error_msg')
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_set_template_and_params(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
new_template = {'resources': {'test': 2}}
new_parameters = {'parameters': {'test': 1}}
hs.set_template(new_template)
self.assertEqual(new_template, hs._template)
hs.set_parameters(new_parameters)
self.assertEqual(new_parameters, hs._parameters)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_set_hot_env_and_files(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
expected_template = {
'heat_template_version': '2013-05-23',
'resources': {'test': 1}
}
self.heat_client_mock.stacks.create.assert_called_with(
stack_name='test-stack',
disable_rollback=True,
parameters={},
template=expected_template,
files={},
environment='',
tags=','.join(CONF.heat.stack_tags)
)
new_hot_env = 'test'
new_files = {'files': {'test': 1}}
hs.set_hot_environment(new_hot_env)
self.assertEqual(new_hot_env, hs._hot_environment)
hs.set_files(new_files)
self.assertEqual(new_files, hs._files)
@mock.patch(CLS_NAME + '._wait_state')
@mock.patch(CLS_NAME + '._get_status')
def test_none_template(self, status_get, wait_st):
status_get.return_value = 'NOT_FOUND'
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = None
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = True
hs._tags = ','.join(CONF.heat.stack_tags)
self.assertIsNone(hs.push())
@mock.patch(CLS_NAME + '._wait_state')
def test_get_hot_status(self, wait_st):
wait_st.return_value = {}
self.override_config('stack_tags', ['test-murano', 'murano-tag'],
'heat')
hs = heat_stack.HeatStack('test-stack', None)
hs._description = None
hs._template = {'resources': {'test': 1}}
hs._files = {}
hs._hot_environment = ''
hs._parameters = {}
hs._applied = False
hs._tags = ','.join(CONF.heat.stack_tags)
hs.push()
self.assertIsNone(hs._get_status())
self.assertTrue(wait_st.called)
self.assertEqual({}, hs.output())
def test_current_except_http_notfound(self):
hs = heat_stack.HeatStack(
'test-stack', 'Generated by TestHeatStack')
hs._template = None
hs._applied = False
hs._parameters = {'param1': 'val1', 'param2': 'val2'}
hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound
current = hs.current()
self.assertEqual({}, current)
self.assertEqual(True, hs._applied)
self.assertEqual({}, hs._template)
self.assertEqual({}, hs._parameters)
@mock.patch.object(heat_stack, 'auth_utils')
def test_get_client(self, mock_auth_utils):
self._unpatch_get_client()
mock_auth_utils.get_session_client_parameters.return_value =\
{'endpoint': 'test_endpoint/v1'}
client = heat_stack.HeatStack._get_client('test_region_name')
self.assertIsNotNone(client)
self.assertEqual("<class 'heatclient.v1.client.Client'>",
str(client.__class__))
mock_auth_utils.get_client_session.assert_called_with(
conf='heat')
@mock.patch.object(heat_stack, 'auth_utils')
def test_get_token_client(self, mock_auth_utils):
self._unpatch_get_client()
mock_auth_utils.get_session_client_parameters.return_value =\
{'endpoint': 'test_endpoint/v1'}
hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack')
token_client = hs._get_token_client()
self.assertIsNotNone(token_client)
self.assertEqual("<class 'heatclient.v1.client.Client'>",
str(token_client.__class__))
mock_auth_utils.get_token_client_session.assert_called_with(
conf='heat')
def test_wait_state(self):
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.return_value =\
mock.Mock(stack_status='CREATE_COMPLETE')
result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
self.assertEqual({}, result)
def test_wait_state_with_outputs(self):
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.side_effect = [
mock.Mock(stack_status='IN_PROGRESS'),
mock.Mock(stack_status='CREATE_COMPLETE',
outputs=[{'output_key': 'key1', 'output_value': 'val1'},
{'output_key': 'key2', 'output_value': 'val2'}])
]
result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
self.assertNotEqual({}, result)
self.assertEqual({'key1': 'val1', 'key2': 'val2'}, result)
def test_wait_state_with_multiple_states(self):
"""Test that only the first state is checked."""
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.side_effect = [
mock.Mock(stack_status=['IN_PROGRESS', 'NOT_FOUND']),
mock.Mock(stack_status='CREATE_COMPLETE')
]
result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
self.assertEqual({}, result)
@mock.patch.object(heat_stack, 'eventlet')
def test_wait_state_with_wait_progress_true(self, mock_eventlet):
hs = heat_stack.HeatStack('test-stack', None)
hs._last_stack_timestamps = ('creation_time', 'updated_time')
hs._client.stacks.get.side_effect = [
mock.Mock(stack_status='TEST_STATUS',
creation_time='creation_time',
updated_time='updated_time'),
mock.Mock(stack_status='TEST_STATUS',
creation_time='creation_time',
updated_time='updated_time'),
mock.Mock(stack_status='CREATE_COMPLETE')
]
result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE',
wait_progress=True)
self.assertEqual({}, result)
self.assertEqual(3, hs._client.stacks.get.call_count)
self.assertEqual(2, mock_eventlet.sleep.call_count)
expected_calls = [mock.call.sleep(2), mock.call.sleep(2)]
self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls)
def test_wait_state_except_http_not_found(self):
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound
# If NOT FOUND is the expected status, then should run successfully.
result = hs._wait_state(lambda status: status == 'NOT_FOUND')
self.assertEqual({}, result)
# Else EnvironmentError should be thrown.
expected_error_msg = "Unexpected stack state {0}"\
.format('NOT_FOUND')
with self.assertRaisesRegex(EnvironmentError,
expected_error_msg):
hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
@mock.patch.object(heat_stack, 'eventlet')
def test_wait_state_except_general_exception(self, mock_eventlet):
"""Test whether 4 tries are executed before exception raised."""
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.side_effect = Exception('test_exception_msg')
with self.assertRaisesRegex(Exception,
'test_exception_msg'):
hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
expected_calls = [mock.call.sleep(2), mock.call.sleep(4),
mock.call.sleep(8)]
self.assertEqual(4, hs._client.stacks.get.call_count)
self.assertEqual(3, mock_eventlet.sleep.call_count)
self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls)
def test_wait_state_except_environment_error(self):
hs = heat_stack.HeatStack('test-stack', None)
hs._client.stacks.get.side_effect = [
mock.Mock(stack_status='IN_PROGRESS'),
mock.Mock(stack_status='UNEXPECTED_STATUS',
stack_status_reason='test_reason')
]
expected_error_msg = "Unexpected stack state {0}: {1}"\
.format('UNEXPECTED_STATUS', 'test_reason')
with self.assertRaisesRegex(EnvironmentError,
expected_error_msg):
hs._wait_state(lambda status: status == 'CREATE_COMPLETE')