# 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(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("", str(client.__class__)) mock_auth_utils.get_client_session.assert_called_with( conf=heat_stack.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("", str(token_client.__class__)) mock_auth_utils.get_token_client_session.assert_called_with( conf=heat_stack.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')