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.
1656 lines
62 KiB
1656 lines
62 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 copy |
|
import re |
|
import uuid |
|
|
|
import mock |
|
import six |
|
|
|
from oslo_serialization import jsonutils |
|
|
|
from heat.common import exception as exc |
|
from heat.common.i18n import _ |
|
from heat.common import template_format |
|
from heat.engine.clients.os import nova |
|
from heat.engine.clients.os import swift |
|
from heat.engine.clients.os import zaqar |
|
from heat.engine.resources.openstack.heat import software_deployment as sd |
|
from heat.engine import rsrc_defn |
|
from heat.engine import stack as parser |
|
from heat.engine import template |
|
from heat.tests import common |
|
from heat.tests import utils |
|
|
|
|
|
class SoftwareDeploymentTest(common.HeatTestCase): |
|
|
|
template = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar'}, |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_with_server = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': 'server', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar'}, |
|
} |
|
}, |
|
'server': { |
|
'Type': 'OS::Nova::Server', |
|
'Properties': { |
|
'image': 'fedora-amd64', |
|
'flavor': 'm1.small', |
|
'key_name': 'heat_key' |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_no_signal = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar', 'bink': 'bonk'}, |
|
'signal_transport': 'NO_SIGNAL', |
|
'name': '00_run_me_first' |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_temp_url_signal = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar', 'bink': 'bonk'}, |
|
'signal_transport': 'TEMP_URL_SIGNAL', |
|
'name': '00_run_me_first' |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_zaqar_signal = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar', 'bink': 'bonk'}, |
|
'signal_transport': 'ZAQAR_SIGNAL', |
|
'name': '00_run_me_first' |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_delete_suspend_resume = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'config': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'input_values': {'foo': 'bar'}, |
|
'actions': ['DELETE', 'SUSPEND', 'RESUME'], |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_no_config = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': { |
|
'server': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'input_values': {'foo': 'bar', 'bink': 'bonk'}, |
|
'signal_transport': 'NO_SIGNAL', |
|
} |
|
} |
|
} |
|
} |
|
|
|
template_no_server = { |
|
'HeatTemplateFormatVersion': '2012-12-12', |
|
'Resources': { |
|
'deployment_mysql': { |
|
'Type': 'OS::Heat::SoftwareDeployment', |
|
'Properties': {} |
|
} |
|
} |
|
} |
|
|
|
def setUp(self): |
|
super(SoftwareDeploymentTest, self).setUp() |
|
self.ctx = utils.dummy_context() |
|
|
|
def _create_stack(self, tmpl, cache_data=None): |
|
self.stack = parser.Stack( |
|
self.ctx, 'software_deployment_test_stack', |
|
template.Template(tmpl), |
|
stack_id='42f6f66b-631a-44e7-8d01-e22fb54574a9', |
|
stack_user_project_id='65728b74-cfe7-4f17-9c15-11d4f686e591', |
|
cache_data=cache_data |
|
) |
|
|
|
self.patchobject(nova.NovaClientPlugin, 'get_server', |
|
return_value=mock.MagicMock()) |
|
self.patchobject(sd.SoftwareDeployment, '_create_user') |
|
self.patchobject(sd.SoftwareDeployment, '_create_keypair') |
|
self.patchobject(sd.SoftwareDeployment, '_delete_user') |
|
self.patchobject(sd.SoftwareDeployment, '_delete_ec2_signed_url') |
|
get_ec2_signed_url = self.patchobject( |
|
sd.SoftwareDeployment, '_get_ec2_signed_url') |
|
get_ec2_signed_url.return_value = 'http://192.0.2.2/signed_url' |
|
|
|
self.deployment = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client = mock.MagicMock() |
|
self.deployment._rpc_client = self.rpc_client |
|
|
|
def test_validate(self): |
|
template = dict(self.template_with_server) |
|
props = template['Resources']['server']['Properties'] |
|
props['user_data_format'] = 'SOFTWARE_CONFIG' |
|
self._create_stack(self.template_with_server) |
|
mock_sd = self.deployment |
|
self.assertEqual('CFN_SIGNAL', |
|
mock_sd.properties.get('signal_transport')) |
|
mock_sd.validate() |
|
|
|
def test_validate_without_server(self): |
|
stack = utils.parse_stack(self.template_no_server) |
|
snip = stack.t.resource_definitions(stack)['deployment_mysql'] |
|
deployment = sd.SoftwareDeployment('deployment_mysql', snip, stack) |
|
err = self.assertRaises(exc.StackValidationFailed, deployment.validate) |
|
self.assertEqual("Property error: " |
|
"Resources.deployment_mysql.Properties: " |
|
"Property server not assigned", six.text_type(err)) |
|
|
|
def test_validate_failed(self): |
|
template = dict(self.template_with_server) |
|
props = template['Resources']['server']['Properties'] |
|
props['user_data_format'] = 'RAW' |
|
self._create_stack(template) |
|
mock_sd = self.deployment |
|
err = self.assertRaises(exc.StackValidationFailed, mock_sd.validate) |
|
self.assertEqual("Resource server's property " |
|
"user_data_format should be set to " |
|
"SOFTWARE_CONFIG since there are " |
|
"software deployments on it.", six.text_type(err)) |
|
|
|
def mock_software_config(self): |
|
config = { |
|
'id': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'group': 'Test::Group', |
|
'name': 'myconfig', |
|
'config': 'the config', |
|
'options': {}, |
|
'inputs': [{ |
|
'name': 'foo', |
|
'type': 'String', |
|
'default': 'baa', |
|
}, { |
|
'name': 'bar', |
|
'type': 'String', |
|
'default': 'baz', |
|
}, { |
|
'name': 'trigger_replace', |
|
'type': 'String', |
|
'default': 'default_value', |
|
'replace_on_change': True, |
|
}], |
|
'outputs': [], |
|
} |
|
|
|
def copy_config(*args, **kwargs): |
|
return config.copy() |
|
|
|
self.rpc_client.show_software_config.side_effect = copy_config |
|
|
|
return config |
|
|
|
def mock_software_component(self): |
|
config = { |
|
'id': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'group': 'component', |
|
'name': 'myconfig', |
|
'config': { |
|
'configs': [ |
|
{ |
|
'actions': ['CREATE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['DELETE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['UPDATE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['SUSPEND'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['RESUME'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
} |
|
] |
|
}, |
|
'options': {}, |
|
'inputs': [{ |
|
'name': 'foo', |
|
'type': 'String', |
|
'default': 'baa', |
|
}, { |
|
'name': 'bar', |
|
'type': 'String', |
|
'default': 'baz', |
|
}], |
|
'outputs': [], |
|
} |
|
|
|
def copy_config(*args, **kwargs): |
|
return config.copy() |
|
|
|
self.rpc_client.show_software_config.side_effect = copy_config |
|
return config |
|
|
|
def mock_derived_software_config(self): |
|
sc = {'id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2'} |
|
self.rpc_client.create_software_config.return_value = sc |
|
return sc |
|
|
|
def mock_deployment(self): |
|
mock_sd = { |
|
'config_id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2' |
|
} |
|
self.rpc_client.create_software_deployment.return_value = mock_sd |
|
return mock_sd |
|
|
|
def test_handle_create(self): |
|
self._create_stack(self.template_no_signal) |
|
|
|
self.mock_software_config() |
|
derived_sc = self.mock_derived_software_config() |
|
self.mock_deployment() |
|
|
|
self.deployment.handle_create() |
|
|
|
self.assertEqual({ |
|
'config': 'the config', |
|
'group': 'Test::Group', |
|
'name': '00_run_me_first', |
|
'inputs': [{ |
|
'default': 'baa', |
|
'name': 'foo', |
|
'type': 'String', |
|
'value': 'bar' |
|
}, { |
|
'default': 'baz', |
|
'name': 'bar', |
|
'type': 'String', |
|
'value': 'baz' |
|
}, { |
|
'default': 'default_value', |
|
'name': 'trigger_replace', |
|
'replace_on_change': True, |
|
'type': 'String', |
|
'value': 'default_value' |
|
}, { |
|
'name': 'bink', |
|
'type': 'String', |
|
'value': 'bonk' |
|
}, { |
|
'description': 'ID of the server being deployed to', |
|
'name': 'deploy_server_id', |
|
'type': 'String', |
|
'value': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0' |
|
}, { |
|
'description': 'Name of the current action being deployed', |
|
'name': 'deploy_action', |
|
'type': 'String', |
|
'value': 'CREATE' |
|
}, { |
|
'description': 'ID of the stack this deployment belongs to', |
|
'name': 'deploy_stack_id', |
|
'type': 'String', |
|
'value': ('software_deployment_test_stack' |
|
'/42f6f66b-631a-44e7-8d01-e22fb54574a9') |
|
}, { |
|
'description': 'Name of this deployment resource in the stack', |
|
'name': 'deploy_resource_name', |
|
'type': 'String', |
|
'value': 'deployment_mysql' |
|
}, { |
|
'description': ('How the server should signal to heat with ' |
|
'the deployment output values.'), |
|
'name': 'deploy_signal_transport', |
|
'type': 'String', |
|
'value': 'NO_SIGNAL' |
|
}], |
|
'options': {}, |
|
'outputs': [] |
|
}, self.rpc_client.create_software_config.call_args[1]) |
|
|
|
self.assertEqual( |
|
{'action': 'CREATE', |
|
'config_id': derived_sc['id'], |
|
'deployment_id': self.deployment.resource_id, |
|
'server_id': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'input_values': {'bink': 'bonk', 'foo': 'bar'}, |
|
'stack_user_project_id': '65728b74-cfe7-4f17-9c15-11d4f686e591', |
|
'status': 'COMPLETE', |
|
'status_reason': 'Not waiting for outputs signal'}, |
|
self.rpc_client.create_software_deployment.call_args[1]) |
|
|
|
def test_handle_create_without_config(self): |
|
self._create_stack(self.template_no_config) |
|
self.mock_deployment() |
|
derived_sc = self.mock_derived_software_config() |
|
self.deployment.handle_create() |
|
|
|
call_arg = self.rpc_client.create_software_config.call_args[1] |
|
call_arg['inputs'] = sorted( |
|
call_arg['inputs'], key=lambda k: k['name']) |
|
self.assertEqual({ |
|
'config': '', |
|
'group': 'Heat::Ungrouped', |
|
'name': self.deployment.physical_resource_name(), |
|
'inputs': [{ |
|
'name': 'bink', |
|
'type': 'String', |
|
'value': 'bonk' |
|
}, { |
|
'description': 'Name of the current action being deployed', |
|
'name': 'deploy_action', |
|
'type': 'String', |
|
'value': 'CREATE' |
|
}, { |
|
'description': 'Name of this deployment resource in the stack', |
|
'name': 'deploy_resource_name', |
|
'type': 'String', |
|
'value': 'deployment_mysql' |
|
}, { |
|
'description': 'ID of the server being deployed to', |
|
'name': 'deploy_server_id', |
|
'type': 'String', |
|
'value': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0' |
|
}, { |
|
'description': ('How the server should signal to heat with ' |
|
'the deployment output values.'), |
|
'name': 'deploy_signal_transport', |
|
'type': 'String', |
|
'value': 'NO_SIGNAL' |
|
}, { |
|
'description': 'ID of the stack this deployment belongs to', |
|
'name': 'deploy_stack_id', |
|
'type': 'String', |
|
'value': ('software_deployment_test_stack' |
|
'/42f6f66b-631a-44e7-8d01-e22fb54574a9') |
|
}, { |
|
'name': 'foo', |
|
'type': 'String', |
|
'value': 'bar' |
|
}], |
|
'options': None, |
|
'outputs': [], |
|
}, call_arg) |
|
|
|
self.assertEqual( |
|
{'action': 'CREATE', |
|
'config_id': derived_sc['id'], |
|
'deployment_id': self.deployment.resource_id, |
|
'input_values': {'bink': 'bonk', 'foo': 'bar'}, |
|
'server_id': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'stack_user_project_id': '65728b74-cfe7-4f17-9c15-11d4f686e591', |
|
'status': 'COMPLETE', |
|
'status_reason': 'Not waiting for outputs signal'}, |
|
self.rpc_client.create_software_deployment.call_args[1]) |
|
|
|
def test_handle_create_for_component(self): |
|
self._create_stack(self.template_no_signal) |
|
|
|
self.mock_software_component() |
|
derived_sc = self.mock_derived_software_config() |
|
self.mock_deployment() |
|
|
|
self.deployment.handle_create() |
|
|
|
self.assertEqual({ |
|
'config': { |
|
'configs': [ |
|
{ |
|
'actions': ['CREATE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['DELETE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['UPDATE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['SUSPEND'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
}, |
|
{ |
|
'actions': ['RESUME'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
} |
|
] |
|
}, |
|
'group': 'component', |
|
'name': '00_run_me_first', |
|
'inputs': [{ |
|
'default': 'baa', |
|
'name': 'foo', |
|
'type': 'String', |
|
'value': 'bar' |
|
}, { |
|
'default': 'baz', |
|
'name': 'bar', |
|
'type': 'String', |
|
'value': 'baz' |
|
}, { |
|
'name': 'bink', |
|
'type': 'String', |
|
'value': 'bonk' |
|
}, { |
|
'description': 'ID of the server being deployed to', |
|
'name': 'deploy_server_id', |
|
'type': 'String', |
|
'value': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0' |
|
}, { |
|
'description': 'Name of the current action being deployed', |
|
'name': 'deploy_action', |
|
'type': 'String', |
|
'value': 'CREATE' |
|
}, { |
|
'description': 'ID of the stack this deployment belongs to', |
|
'name': 'deploy_stack_id', |
|
'type': 'String', |
|
'value': ('software_deployment_test_stack' |
|
'/42f6f66b-631a-44e7-8d01-e22fb54574a9') |
|
}, { |
|
'description': 'Name of this deployment resource in the stack', |
|
'name': 'deploy_resource_name', |
|
'type': 'String', |
|
'value': 'deployment_mysql' |
|
}, { |
|
'description': ('How the server should signal to heat with ' |
|
'the deployment output values.'), |
|
'name': 'deploy_signal_transport', |
|
'type': 'String', |
|
'value': 'NO_SIGNAL' |
|
}], |
|
'options': {}, |
|
'outputs': [] |
|
}, self.rpc_client.create_software_config.call_args[1]) |
|
|
|
self.assertEqual( |
|
{'action': 'CREATE', |
|
'config_id': derived_sc['id'], |
|
'deployment_id': self.deployment.resource_id, |
|
'input_values': {'bink': 'bonk', 'foo': 'bar'}, |
|
'server_id': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'stack_user_project_id': '65728b74-cfe7-4f17-9c15-11d4f686e591', |
|
'status': 'COMPLETE', |
|
'status_reason': 'Not waiting for outputs signal'}, |
|
self.rpc_client.create_software_deployment.call_args[1]) |
|
|
|
def test_handle_create_do_not_wait(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_software_config() |
|
derived_sc = self.mock_derived_software_config() |
|
self.mock_deployment() |
|
|
|
self.deployment.handle_create() |
|
self.assertEqual( |
|
{'action': 'CREATE', |
|
'config_id': derived_sc['id'], |
|
'deployment_id': self.deployment.resource_id, |
|
'input_values': {'foo': 'bar'}, |
|
'server_id': '9f1f0e00-05d2-4ca5-8602-95021f19c9d0', |
|
'stack_user_project_id': '65728b74-cfe7-4f17-9c15-11d4f686e591', |
|
'status': 'IN_PROGRESS', |
|
'status_reason': 'Deploy data available'}, |
|
self.rpc_client.create_software_deployment.call_args[1]) |
|
|
|
def test_check_create_complete(self): |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_create_complete(mock_sd)) |
|
mock_sd['status'] = self.deployment.IN_PROGRESS |
|
self.assertFalse(self.deployment.check_create_complete(mock_sd)) |
|
|
|
def test_check_create_complete_none(self): |
|
self._create_stack(self.template) |
|
self.assertTrue(self.deployment.check_create_complete(sd=None)) |
|
|
|
def test_check_update_complete(self): |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_update_complete(mock_sd)) |
|
|
|
mock_sd['status'] = self.deployment.IN_PROGRESS |
|
self.assertFalse(self.deployment.check_update_complete(mock_sd)) |
|
|
|
def test_check_update_complete_none(self): |
|
self._create_stack(self.template) |
|
self.assertTrue(self.deployment.check_update_complete(sd=None)) |
|
|
|
def test_check_suspend_complete(self): |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_suspend_complete(mock_sd)) |
|
|
|
mock_sd['status'] = self.deployment.IN_PROGRESS |
|
self.assertFalse(self.deployment.check_suspend_complete(mock_sd)) |
|
|
|
def test_check_suspend_complete_none(self): |
|
self._create_stack(self.template) |
|
self.assertTrue(self.deployment.check_suspend_complete(sd=None)) |
|
|
|
def test_check_resume_complete(self): |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_resume_complete(mock_sd)) |
|
|
|
mock_sd['status'] = self.deployment.IN_PROGRESS |
|
self.assertFalse(self.deployment.check_resume_complete(mock_sd)) |
|
|
|
def test_check_resume_complete_none(self): |
|
self._create_stack(self.template) |
|
self.assertTrue(self.deployment.check_resume_complete(sd=None)) |
|
|
|
def test_check_create_complete_error(self): |
|
self._create_stack(self.template) |
|
mock_sd = { |
|
'status': self.deployment.FAILED, |
|
'status_reason': 'something wrong' |
|
} |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
err = self.assertRaises( |
|
exc.Error, self.deployment.check_create_complete, mock_sd) |
|
self.assertEqual( |
|
'Deployment to server failed: something wrong', six.text_type(err)) |
|
|
|
def test_handle_delete(self): |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
self.deployment.handle_delete() |
|
self.deployment.check_delete_complete() |
|
self.assertEqual( |
|
(self.ctx, self.deployment.resource_id), |
|
self.rpc_client.delete_software_deployment.call_args[0]) |
|
|
|
def test_handle_delete_resource_id_is_None(self): |
|
self._create_stack(self.template_delete_suspend_resume) |
|
self.mock_software_config() |
|
mock_sd = self.mock_deployment() |
|
self.assertEqual(mock_sd, self.deployment.handle_delete()) |
|
|
|
def test_delete_complete(self): |
|
self._create_stack(self.template_delete_suspend_resume) |
|
|
|
self.mock_software_config() |
|
derived_sc = self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
mock_sd['server_id'] = 'b509edfb-1448-4b57-8cb1-2e31acccbb8a' |
|
|
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.rpc_client.update_software_deployment.return_value = mock_sd |
|
self.assertEqual(mock_sd, self.deployment.handle_delete()) |
|
self.assertEqual({ |
|
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', |
|
'action': 'DELETE', |
|
'config_id': derived_sc['id'], |
|
'input_values': {'foo': 'bar'}, |
|
'status': 'IN_PROGRESS', |
|
'status_reason': 'Deploy data available'}, |
|
self.rpc_client.update_software_deployment.call_args[1]) |
|
|
|
mock_sd['status'] = self.deployment.IN_PROGRESS |
|
self.assertFalse(self.deployment.check_delete_complete(mock_sd)) |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_delete_complete(mock_sd)) |
|
|
|
def test_delete_complete_missing_server(self): |
|
"""Tests deleting a deployment when the server disappears""" |
|
self._create_stack(self.template_delete_suspend_resume) |
|
|
|
self.mock_software_config() |
|
mock_sd = self.mock_deployment() |
|
mock_sd['server_id'] = 'b509edfb-1448-4b57-8cb1-2e31acccbb8a' |
|
|
|
# Simulate Nova not knowing about the server |
|
mock_get_server = self.patchobject( |
|
nova.NovaClientPlugin, 'get_server', |
|
side_effect=exc.EntityNotFound) |
|
|
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.rpc_client.update_software_deployment.return_value = mock_sd |
|
|
|
mock_sd['status'] = self.deployment.COMPLETE |
|
self.assertTrue(self.deployment.check_delete_complete(mock_sd)) |
|
|
|
mock_get_server.assert_called_once_with(mock_sd['server_id']) |
|
|
|
def test_handle_delete_notfound(self): |
|
self._create_stack(self.template) |
|
deployment_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
self.deployment.resource_id = deployment_id |
|
|
|
self.mock_software_config() |
|
derived_sc = self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
mock_sd['config_id'] = derived_sc['id'] |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
nf = exc.NotFound |
|
self.rpc_client.delete_software_deployment.side_effect = nf |
|
self.rpc_client.delete_software_config.side_effect = nf |
|
self.assertIsNone(self.deployment.handle_delete()) |
|
self.assertTrue(self.deployment.check_delete_complete()) |
|
self.assertEqual( |
|
(self.ctx, derived_sc['id']), |
|
self.rpc_client.delete_software_config.call_args[0]) |
|
|
|
def test_handle_delete_none(self): |
|
self._create_stack(self.template) |
|
deployment_id = None |
|
self.deployment.resource_id = deployment_id |
|
self.assertIsNone(self.deployment.handle_delete()) |
|
|
|
def test_check_delete_complete_none(self): |
|
self._create_stack(self.template) |
|
self.assertTrue(self.deployment.check_delete_complete()) |
|
|
|
def test_check_delete_complete_delete_sd(self): |
|
# handle_delete will return None if NO_SIGNAL, |
|
# in this case also need to call the _delete_resource(), |
|
# otherwise the sd data will residue in db |
|
self._create_stack(self.template) |
|
mock_sd = self.mock_deployment() |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.assertTrue(self.deployment.check_delete_complete()) |
|
self.assertEqual( |
|
(self.ctx, self.deployment.resource_id), |
|
self.rpc_client.delete_software_deployment.call_args[0]) |
|
|
|
def test_handle_update(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
rsrc = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
config_id = '0ff2e903-78d7-4cca-829e-233af3dae705' |
|
prop_diff = { |
|
'config': config_id, |
|
'name': 'new_name' |
|
} |
|
props = copy.copy(rsrc.properties.data) |
|
props.update(prop_diff) |
|
snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) |
|
|
|
self.deployment.handle_update( |
|
json_snippet=snippet, tmpl_diff=None, prop_diff=prop_diff) |
|
self.assertEqual( |
|
(self.ctx, config_id), |
|
self.rpc_client.show_software_config.call_args[0]) |
|
|
|
self.assertEqual( |
|
(self.ctx, self.deployment.resource_id), |
|
self.rpc_client.show_software_deployment.call_args[0]) |
|
|
|
self.assertEqual( |
|
'new_name', |
|
self.rpc_client.create_software_config.call_args[1]['name']) |
|
|
|
self.assertEqual({ |
|
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', |
|
'action': 'UPDATE', |
|
'config_id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2', |
|
'input_values': {'foo': 'bar'}, |
|
'status': 'IN_PROGRESS', |
|
'status_reason': u'Deploy data available'}, |
|
self.rpc_client.update_software_deployment.call_args[1]) |
|
|
|
def test_handle_update_no_replace_on_change(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_software_config() |
|
self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
rsrc = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
prop_diff = { |
|
'input_values': {'trigger_replace': 'default_value'}, |
|
} |
|
props = copy.copy(rsrc.properties.data) |
|
props.update(prop_diff) |
|
snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) |
|
|
|
self.deployment.handle_update(snippet, None, prop_diff) |
|
|
|
self.assertEqual({ |
|
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', |
|
'action': 'UPDATE', |
|
'config_id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2', |
|
'input_values': {'trigger_replace': 'default_value'}, |
|
'status': 'IN_PROGRESS', |
|
'status_reason': u'Deploy data available'}, |
|
self.rpc_client.update_software_deployment.call_args[1]) |
|
|
|
self.assertEqual([ |
|
{ |
|
'default': 'baa', |
|
'name': 'foo', |
|
'type': 'String', |
|
'value': 'baa' |
|
}, { |
|
'default': 'baz', |
|
'name': 'bar', |
|
'type': 'String', |
|
'value': 'baz' |
|
}, { |
|
'default': 'default_value', |
|
'name': 'trigger_replace', |
|
'replace_on_change': True, |
|
'type': 'String', |
|
'value': 'default_value' |
|
}], |
|
self.rpc_client.create_software_config.call_args[1]['inputs'][:3]) |
|
|
|
def test_handle_update_replace_on_change(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_software_config() |
|
self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
rsrc = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
prop_diff = { |
|
'input_values': {'trigger_replace': 'new_value'}, |
|
} |
|
props = copy.copy(rsrc.properties.data) |
|
props.update(prop_diff) |
|
snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) |
|
|
|
self.assertRaises(exc.UpdateReplace, |
|
self.deployment.handle_update, |
|
snippet, None, prop_diff) |
|
|
|
def test_handle_suspend_resume(self): |
|
self._create_stack(self.template_delete_suspend_resume) |
|
|
|
self.mock_software_config() |
|
derived_sc = self.mock_derived_software_config() |
|
mock_sd = self.mock_deployment() |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
|
|
# first, handle the suspend |
|
self.deployment.handle_suspend() |
|
|
|
self.assertEqual({ |
|
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', |
|
'action': 'SUSPEND', |
|
'config_id': derived_sc['id'], |
|
'input_values': {'foo': 'bar'}, |
|
'status': 'IN_PROGRESS', |
|
'status_reason': 'Deploy data available'}, |
|
self.rpc_client.update_software_deployment.call_args[1]) |
|
|
|
mock_sd['status'] = 'IN_PROGRESS' |
|
self.assertFalse(self.deployment.check_suspend_complete(mock_sd)) |
|
|
|
mock_sd['status'] = 'COMPLETE' |
|
self.assertTrue(self.deployment.check_suspend_complete(mock_sd)) |
|
|
|
# now, handle the resume |
|
self.deployment.handle_resume() |
|
|
|
self.assertEqual({ |
|
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c', |
|
'action': 'RESUME', |
|
'config_id': derived_sc['id'], |
|
'input_values': {'foo': 'bar'}, |
|
'status': 'IN_PROGRESS', |
|
'status_reason': 'Deploy data available'}, |
|
self.rpc_client.update_software_deployment.call_args[1]) |
|
|
|
mock_sd['status'] = 'IN_PROGRESS' |
|
self.assertFalse(self.deployment.check_resume_complete(mock_sd)) |
|
|
|
mock_sd['status'] = 'COMPLETE' |
|
self.assertTrue(self.deployment.check_resume_complete(mock_sd)) |
|
|
|
def test_handle_signal_ok_zero(self): |
|
self._create_stack(self.template) |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = 'deployment succeeded' |
|
details = { |
|
'foo': 'bar', |
|
'deploy_status_code': 0 |
|
} |
|
ret = self.deployment.handle_signal(details) |
|
self.assertEqual('deployment succeeded', ret) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) |
|
self.assertEqual({'foo': 'bar', 'deploy_status_code': 0}, ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
def test_no_signal_action(self): |
|
self._create_stack(self.template) |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = 'deployment succeeded' |
|
details = { |
|
'foo': 'bar', |
|
'deploy_status_code': 0 |
|
} |
|
actions = [self.deployment.SUSPEND, self.deployment.DELETE] |
|
ev = self.patchobject(self.deployment, 'handle_signal') |
|
for action in actions: |
|
for status in self.deployment.STATUSES: |
|
self.deployment.state_set(action, status) |
|
self.deployment.signal(details) |
|
ev.assert_called_with(details) |
|
|
|
def test_handle_signal_ok_str_zero(self): |
|
self._create_stack(self.template) |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = 'deployment succeeded' |
|
details = { |
|
'foo': 'bar', |
|
'deploy_status_code': '0' |
|
} |
|
ret = self.deployment.handle_signal(details) |
|
self.assertEqual('deployment succeeded', ret) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) |
|
self.assertEqual({'foo': 'bar', 'deploy_status_code': '0'}, ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
def test_handle_signal_failed(self): |
|
self._create_stack(self.template) |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = 'deployment failed' |
|
|
|
details = {'failed': 'no enough memory found.'} |
|
ret = self.deployment.handle_signal(details) |
|
self.assertEqual('deployment failed', ret) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) |
|
self.assertEqual(details, ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
# Test bug 1332355, where details contains a translatable message |
|
details = {'failed': _('need more memory.')} |
|
ret = self.deployment.handle_signal(details) |
|
self.assertEqual('deployment failed', ret) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) |
|
self.assertEqual(details, ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
def test_handle_status_code_failed(self): |
|
self._create_stack(self.template) |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = 'deployment failed' |
|
|
|
details = { |
|
'deploy_stdout': 'A thing happened', |
|
'deploy_stderr': 'Then it broke', |
|
'deploy_status_code': -1 |
|
} |
|
self.deployment.handle_signal(details) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertEqual('c8a19429-7fde-47ea-a42f-40045488226c', ca[1]) |
|
self.assertEqual(details, ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
def test_handle_signal_not_waiting(self): |
|
self._create_stack(self.template) |
|
rpcc = self.rpc_client |
|
rpcc.signal_software_deployment.return_value = None |
|
details = None |
|
self.assertIsNone(self.deployment.handle_signal(details)) |
|
ca = rpcc.signal_software_deployment.call_args[0] |
|
self.assertEqual(self.ctx, ca[0]) |
|
self.assertIsNone(ca[1]) |
|
self.assertIsNone(ca[2]) |
|
self.assertIsNotNone(ca[3]) |
|
|
|
def test_fn_get_att(self): |
|
self._create_stack(self.template) |
|
mock_sd = { |
|
'outputs': [ |
|
{'name': 'failed', 'error_output': True}, |
|
{'name': 'foo'} |
|
], |
|
'output_values': { |
|
'foo': 'bar', |
|
'deploy_stdout': 'A thing happened', |
|
'deploy_stderr': 'Extraneous logging', |
|
'deploy_status_code': 0 |
|
}, |
|
'status': self.deployment.COMPLETE |
|
} |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.assertEqual('bar', self.deployment.FnGetAtt('foo')) |
|
self.assertEqual('A thing happened', |
|
self.deployment.FnGetAtt('deploy_stdout')) |
|
self.assertEqual('Extraneous logging', |
|
self.deployment.FnGetAtt('deploy_stderr')) |
|
self.assertEqual(0, self.deployment.FnGetAtt('deploy_status_code')) |
|
|
|
def test_fn_get_att_convg(self): |
|
cache_data = {'deployment_mysql': { |
|
'uuid': mock.ANY, |
|
'id': mock.ANY, |
|
'action': 'CREATE', |
|
'status': 'COMPLETE', |
|
'attrs': {'foo': 'bar'} |
|
}} |
|
self._create_stack(self.template, cache_data=cache_data) |
|
self.assertEqual('bar', self.deployment.FnGetAtt('foo')) |
|
|
|
def test_fn_get_att_error(self): |
|
self._create_stack(self.template) |
|
|
|
mock_sd = { |
|
'outputs': [], |
|
'output_values': {'foo': 'bar'}, |
|
} |
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
err = self.assertRaises( |
|
exc.InvalidTemplateAttribute, |
|
self.deployment.FnGetAtt, 'foo2') |
|
self.assertEqual( |
|
'The Referenced Attribute (deployment_mysql foo2) is incorrect.', |
|
six.text_type(err)) |
|
|
|
def test_handle_action(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_software_config() |
|
mock_sd = self.mock_deployment() |
|
rsrc = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
config_id = '0ff2e903-78d7-4cca-829e-233af3dae705' |
|
prop_diff = {'config': config_id} |
|
props = copy.copy(rsrc.properties.data) |
|
props.update(prop_diff) |
|
snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) |
|
|
|
# by default (no 'actions' property) SoftwareDeployment must only |
|
# trigger for CREATE and UPDATE |
|
self.assertIsNotNone(self.deployment.handle_create()) |
|
self.assertIsNotNone(self.deployment.handle_update( |
|
json_snippet=snippet, tmpl_diff=None, prop_diff=prop_diff)) |
|
# ... but it must not trigger for SUSPEND, RESUME and DELETE |
|
self.assertIsNone(self.deployment.handle_suspend()) |
|
self.assertIsNone(self.deployment.handle_resume()) |
|
self.assertIsNone(self.deployment.handle_delete()) |
|
|
|
def test_handle_action_for_component(self): |
|
self._create_stack(self.template) |
|
|
|
self.mock_software_component() |
|
mock_sd = self.mock_deployment() |
|
rsrc = self.stack['deployment_mysql'] |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
self.deployment.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c' |
|
config_id = '0ff2e903-78d7-4cca-829e-233af3dae705' |
|
prop_diff = {'config': config_id} |
|
props = copy.copy(rsrc.properties.data) |
|
props.update(prop_diff) |
|
snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(), props) |
|
|
|
# for a SoftwareComponent, SoftwareDeployment must always trigger |
|
self.assertIsNotNone(self.deployment.handle_create()) |
|
self.assertIsNotNone(self.deployment.handle_update( |
|
json_snippet=snippet, tmpl_diff=None, prop_diff=prop_diff)) |
|
self.assertIsNotNone(self.deployment.handle_suspend()) |
|
self.assertIsNotNone(self.deployment.handle_resume()) |
|
self.assertIsNotNone(self.deployment.handle_delete()) |
|
|
|
def test_handle_unused_action_for_component(self): |
|
self._create_stack(self.template) |
|
|
|
config = { |
|
'id': '48e8ade1-9196-42d5-89a2-f709fde42632', |
|
'group': 'component', |
|
'name': 'myconfig', |
|
'config': { |
|
'configs': [ |
|
{ |
|
'actions': ['CREATE'], |
|
'config': 'the config', |
|
'tool': 'a_tool' |
|
} |
|
] |
|
}, |
|
'options': {}, |
|
'inputs': [{ |
|
'name': 'foo', |
|
'type': 'String', |
|
'default': 'baa', |
|
}, { |
|
'name': 'bar', |
|
'type': 'String', |
|
'default': 'baz', |
|
}], |
|
'outputs': [], |
|
} |
|
self.rpc_client.show_software_config.return_value = config |
|
mock_sd = self.mock_deployment() |
|
|
|
self.rpc_client.show_software_deployment.return_value = mock_sd |
|
|
|
self.assertIsNotNone(self.deployment.handle_create()) |
|
self.assertIsNone(self.deployment.handle_delete()) |
|
|
|
def test_get_temp_url(self): |
|
dep_data = {} |
|
|
|
sc = mock.MagicMock() |
|
scc = self.patch( |
|
'heat.engine.clients.os.swift.SwiftClientPlugin._create') |
|
scc.return_value = sc |
|
sc.head_account.return_value = { |
|
'x-account-meta-temp-url-key': 'secrit' |
|
} |
|
sc.url = 'http://192.0.2.1/v1/AUTH_test_tenant_id' |
|
|
|
self._create_stack(self.template_temp_url_signal) |
|
|
|
def data_set(key, value, redact=False): |
|
dep_data[key] = value |
|
|
|
self.deployment.data_set = data_set |
|
self.deployment.data = mock.Mock( |
|
return_value=dep_data) |
|
|
|
self.deployment.id = 23 |
|
self.deployment.uuid = str(uuid.uuid4()) |
|
self.deployment.action = self.deployment.CREATE |
|
object_name = self.deployment.physical_resource_name() |
|
|
|
temp_url = self.deployment._get_swift_signal_url() |
|
temp_url_pattern = re.compile( |
|
'^http://192.0.2.1/v1/AUTH_test_tenant_id/' |
|
'(.*)/(software_deployment_test_stack-deployment_mysql-.*)' |
|
'\\?temp_url_sig=.*&temp_url_expires=\\d*$') |
|
self.assertRegex(temp_url, temp_url_pattern) |
|
m = temp_url_pattern.search(temp_url) |
|
container = m.group(1) |
|
self.assertEqual(object_name, m.group(2)) |
|
self.assertEqual(dep_data['swift_signal_object_name'], object_name) |
|
|
|
self.assertEqual(dep_data['swift_signal_url'], temp_url) |
|
|
|
self.assertEqual(temp_url, self.deployment._get_swift_signal_url()) |
|
|
|
sc.put_container.assert_called_once_with(container) |
|
sc.put_object.assert_called_once_with(container, object_name, '') |
|
|
|
def test_delete_temp_url(self): |
|
object_name = str(uuid.uuid4()) |
|
dep_data = { |
|
'swift_signal_object_name': object_name |
|
} |
|
self._create_stack(self.template_temp_url_signal) |
|
|
|
self.deployment.data_delete = mock.MagicMock() |
|
self.deployment.data = mock.Mock( |
|
return_value=dep_data) |
|
|
|
sc = mock.MagicMock() |
|
sc.get_container.return_value = ({}, [{'name': object_name}]) |
|
sc.head_container.return_value = { |
|
'x-container-object-count': 0 |
|
} |
|
scc = self.patch( |
|
'heat.engine.clients.os.swift.SwiftClientPlugin._create') |
|
scc.return_value = sc |
|
|
|
self.deployment.id = 23 |
|
self.deployment.uuid = str(uuid.uuid4()) |
|
container = self.stack.id |
|
self.deployment._delete_swift_signal_url() |
|
sc.delete_object.assert_called_once_with(container, object_name) |
|
self.assertEqual( |
|
[mock.call('swift_signal_object_name'), |
|
mock.call('swift_signal_url')], |
|
self.deployment.data_delete.mock_calls) |
|
|
|
swift_exc = swift.SwiftClientPlugin.exceptions_module |
|
sc.delete_object.side_effect = swift_exc.ClientException( |
|
'Not found', http_status=404) |
|
self.deployment._delete_swift_signal_url() |
|
self.assertEqual( |
|
[mock.call('swift_signal_object_name'), |
|
mock.call('swift_signal_url'), |
|
mock.call('swift_signal_object_name'), |
|
mock.call('swift_signal_url')], |
|
self.deployment.data_delete.mock_calls) |
|
|
|
del(dep_data['swift_signal_object_name']) |
|
self.deployment.physical_resource_name = mock.Mock() |
|
self.deployment._delete_swift_signal_url() |
|
self.assertFalse(self.deployment.physical_resource_name.called) |
|
|
|
def test_handle_action_temp_url(self): |
|
|
|
self._create_stack(self.template_temp_url_signal) |
|
dep_data = { |
|
'swift_signal_url': ( |
|
'http://192.0.2.1/v1/AUTH_a/b/c' |
|
'?temp_url_sig=ctemp_url_expires=1234') |
|
} |
|
self.deployment.data = mock.Mock( |
|
return_value=dep_data) |
|
|
|
self.mock_software_config() |
|
|
|
for action in ('DELETE', 'SUSPEND', 'RESUME'): |
|
self.assertIsNone(self.deployment._handle_action(action)) |
|
for action in ('CREATE', 'UPDATE'): |
|
self.assertIsNotNone(self.deployment._handle_action(action)) |
|
|
|
def test_get_zaqar_queue(self): |
|
dep_data = {} |
|
|
|
zc = mock.MagicMock() |
|
zcc = self.patch( |
|
'heat.engine.clients.os.zaqar.ZaqarClientPlugin.create_for_tenant') |
|
zcc.return_value = zc |
|
|
|
mock_queue = mock.MagicMock() |
|
zc.queue.return_value = mock_queue |
|
|
|
signed_data = {"signature": "hi", "expires": "later"} |
|
mock_queue.signed_url.return_value = signed_data |
|
|
|
self._create_stack(self.template_zaqar_signal) |
|
|
|
def data_set(key, value, redact=False): |
|
dep_data[key] = value |
|
|
|
self.deployment.data_set = data_set |
|
self.deployment.data = mock.Mock(return_value=dep_data) |
|
|
|
self.deployment.id = 23 |
|
self.deployment.uuid = str(uuid.uuid4()) |
|
self.deployment.action = self.deployment.CREATE |
|
|
|
queue_id = self.deployment._get_zaqar_signal_queue_id() |
|
self.assertEqual(queue_id, dep_data['zaqar_signal_queue_id']) |
|
self.assertEqual(jsonutils.dumps(signed_data), |
|
dep_data['zaqar_queue_signed_url_data']) |
|
|
|
self.assertEqual(queue_id, |
|
self.deployment._get_zaqar_signal_queue_id()) |
|
|
|
@mock.patch.object(zaqar.ZaqarClientPlugin, 'create_for_tenant') |
|
def test_delete_zaqar_queue(self, zcc): |
|
queue_id = str(uuid.uuid4()) |
|
dep_data = { |
|
'password': 'password', |
|
'zaqar_signal_queue_id': queue_id |
|
} |
|
self._create_stack(self.template_zaqar_signal) |
|
|
|
self.deployment.data_delete = mock.MagicMock() |
|
self.deployment.data = mock.Mock(return_value=dep_data) |
|
|
|
zc = mock.MagicMock() |
|
zcc.return_value = zc |
|
|
|
self.deployment.id = 23 |
|
self.deployment.uuid = str(uuid.uuid4()) |
|
self.deployment._delete_zaqar_signal_queue() |
|
zc.queue.assert_called_once_with(queue_id) |
|
self.assertTrue(zc.queue(self.deployment.uuid).delete.called) |
|
self.assertEqual( |
|
[mock.call('zaqar_signal_queue_id')], |
|
self.deployment.data_delete.mock_calls) |
|
|
|
zaqar_exc = zaqar.ZaqarClientPlugin.exceptions_module |
|
zc.queue.delete.side_effect = zaqar_exc.ResourceNotFound() |
|
self.deployment._delete_zaqar_signal_queue() |
|
self.assertEqual( |
|
[mock.call('zaqar_signal_queue_id'), |
|
mock.call('zaqar_signal_queue_id')], |
|
self.deployment.data_delete.mock_calls) |
|
|
|
dep_data.pop('zaqar_signal_queue_id') |
|
self.deployment.physical_resource_name = mock.Mock() |
|
self.deployment._delete_zaqar_signal_queue() |
|
self.assertEqual(2, len(self.deployment.data_delete.mock_calls)) |
|
|
|
def test_server_exists(self): |
|
# Setup |
|
self._create_stack(self.template_delete_suspend_resume) |
|
mock_sd = {'server_id': 'b509edfb-1448-4b57-8cb1-2e31acccbb8a'} |
|
|
|
# For a success case, this doesn't raise an exception |
|
self.patchobject(nova.NovaClientPlugin, 'get_server') |
|
|
|
# Test |
|
result = self.deployment._server_exists(mock_sd) |
|
self.assertTrue(result) |
|
|
|
def test_server_exists_no_server(self): |
|
# Setup |
|
self._create_stack(self.template_delete_suspend_resume) |
|
mock_sd = {'server_id': 'b509edfb-1448-4b57-8cb1-2e31acccbb8a'} |
|
|
|
# For a success case, this doesn't raise an exception |
|
self.patchobject(nova.NovaClientPlugin, 'get_server', |
|
side_effect=exc.EntityNotFound) |
|
|
|
# Test |
|
result = self.deployment._server_exists(mock_sd) |
|
self.assertFalse(result) |
|
|
|
|
|
class SoftwareDeploymentGroupTest(common.HeatTestCase): |
|
|
|
template = { |
|
'heat_template_version': '2013-05-23', |
|
'resources': { |
|
'deploy_mysql': { |
|
'type': 'OS::Heat::SoftwareDeploymentGroup', |
|
'properties': { |
|
'config': 'config_uuid', |
|
'servers': {'server1': 'uuid1', 'server2': 'uuid2'}, |
|
'input_values': {'foo': 'bar'}, |
|
'name': '10_config' |
|
} |
|
} |
|
} |
|
} |
|
|
|
def setUp(self): |
|
common.HeatTestCase.setUp(self) |
|
self.rpc_client = mock.MagicMock() |
|
|
|
def test_build_resource_definition(self): |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
|
|
expect = rsrc_defn.ResourceDefinition( |
|
None, |
|
"OS::Heat::SoftwareDeployment", |
|
{'actions': ['CREATE', 'UPDATE'], |
|
'config': 'config_uuid', |
|
'input_values': {'foo': 'bar'}, |
|
'name': '10_config', |
|
'server': 'uuid1', |
|
'signal_transport': 'CFN_SIGNAL'}) |
|
|
|
rdef = resg.get_resource_def() |
|
self.assertEqual( |
|
expect, resg.build_resource_definition('server1', rdef)) |
|
rdef = resg.get_resource_def(include_all=True) |
|
self.assertEqual( |
|
expect, resg.build_resource_definition('server1', rdef)) |
|
|
|
def test_resource_names(self): |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
self.assertEqual( |
|
set(('server1', 'server2')), |
|
set(resg._resource_names()) |
|
) |
|
|
|
resg.properties = {'servers': {'s1': 'u1', 's2': 'u2', 's3': 'u3'}} |
|
self.assertEqual( |
|
set(('s1', 's2', 's3')), |
|
set(resg._resource_names())) |
|
|
|
def test_assemble_nested(self): |
|
"""Tests nested stack implements group creation based on properties. |
|
|
|
Tests that the nested stack that implements the group is created |
|
appropriately based on properties. |
|
""" |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
templ = { |
|
"heat_template_version": "2015-04-30", |
|
"resources": { |
|
"server1": { |
|
'type': 'OS::Heat::SoftwareDeployment', |
|
'properties': { |
|
'server': 'uuid1', |
|
'actions': ['CREATE', 'UPDATE'], |
|
'config': 'config_uuid', |
|
'input_values': {'foo': 'bar'}, |
|
'name': '10_config', |
|
'signal_transport': 'CFN_SIGNAL' |
|
} |
|
}, |
|
"server2": { |
|
'type': 'OS::Heat::SoftwareDeployment', |
|
'properties': { |
|
'server': 'uuid2', |
|
'actions': ['CREATE', 'UPDATE'], |
|
'config': 'config_uuid', |
|
'input_values': {'foo': 'bar'}, |
|
'name': '10_config', |
|
'signal_transport': 'CFN_SIGNAL' |
|
} |
|
} |
|
} |
|
} |
|
|
|
self.assertEqual(templ, resg._assemble_nested(['server1', |
|
'server2']).t) |
|
|
|
def test_attributes(self): |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
nested = self.patchobject(resg, 'nested') |
|
server1 = mock.MagicMock() |
|
server2 = mock.MagicMock() |
|
nested.return_value = { |
|
'server1': server1, |
|
'server2': server2 |
|
} |
|
|
|
server1.FnGetAtt.return_value = 'Thing happened on server1' |
|
server2.FnGetAtt.return_value = 'ouch' |
|
self.assertEqual({ |
|
'server1': 'Thing happened on server1', |
|
'server2': 'ouch' |
|
}, resg.FnGetAtt('deploy_stdouts')) |
|
|
|
server1.FnGetAtt.return_value = '' |
|
server2.FnGetAtt.return_value = 'Its gone Pete Tong' |
|
self.assertEqual({ |
|
'server1': '', |
|
'server2': 'Its gone Pete Tong' |
|
}, resg.FnGetAtt('deploy_stderrs')) |
|
|
|
server1.FnGetAtt.return_value = 0 |
|
server2.FnGetAtt.return_value = 1 |
|
self.assertEqual({ |
|
'server1': 0, |
|
'server2': 1 |
|
}, resg.FnGetAtt('deploy_status_codes')) |
|
|
|
server1.FnGetAtt.assert_has_calls([ |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_status_code'), |
|
]) |
|
|
|
server2.FnGetAtt.assert_has_calls([ |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_status_code'), |
|
]) |
|
|
|
def test_attributes_path(self): |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
nested = self.patchobject(resg, 'nested') |
|
server1 = mock.MagicMock() |
|
server2 = mock.MagicMock() |
|
nested.return_value = { |
|
'server1': server1, |
|
'server2': server2 |
|
} |
|
|
|
server1.FnGetAtt.return_value = 'Thing happened on server1' |
|
server2.FnGetAtt.return_value = 'ouch' |
|
self.assertEqual('Thing happened on server1', |
|
resg.FnGetAtt('deploy_stdouts', 'server1')) |
|
self.assertEqual('ouch', |
|
resg.FnGetAtt('deploy_stdouts', 'server2')) |
|
|
|
server1.FnGetAtt.return_value = '' |
|
server2.FnGetAtt.return_value = 'Its gone Pete Tong' |
|
self.assertEqual('', resg.FnGetAtt('deploy_stderrs', 'server1')) |
|
self.assertEqual('Its gone Pete Tong', |
|
resg.FnGetAtt('deploy_stderrs', 'server2')) |
|
|
|
server1.FnGetAtt.return_value = 0 |
|
server2.FnGetAtt.return_value = 1 |
|
self.assertEqual(0, resg.FnGetAtt('deploy_status_codes', 'server1')) |
|
self.assertEqual(1, resg.FnGetAtt('deploy_status_codes', 'server2')) |
|
|
|
server1.FnGetAtt.assert_has_calls([ |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_status_code'), |
|
mock.call('deploy_status_code'), |
|
]) |
|
|
|
server2.FnGetAtt.assert_has_calls([ |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stdout'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_stderr'), |
|
mock.call('deploy_status_code'), |
|
mock.call('deploy_status_code'), |
|
]) |
|
|
|
def test_attributes_passthrough_key(self): |
|
'''Prove attributes not in the schema pass-through.''' |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('test', snip, stack) |
|
nested = self.patchobject(resg, 'nested') |
|
server1 = mock.MagicMock() |
|
server2 = mock.MagicMock() |
|
nested.return_value = { |
|
'server1': server1, |
|
'server2': server2 |
|
} |
|
|
|
server1.FnGetAtt.return_value = 'attr1' |
|
server2.FnGetAtt.return_value = 'attr2' |
|
self.assertEqual({ |
|
'server1': 'attr1', |
|
'server2': 'attr2' |
|
}, resg.FnGetAtt('some_attr')) |
|
|
|
server1.FnGetAtt.assert_has_calls([ |
|
mock.call('some_attr'), |
|
]) |
|
|
|
server2.FnGetAtt.assert_has_calls([ |
|
mock.call('some_attr'), |
|
]) |
|
|
|
def test_validate(self): |
|
stack = utils.parse_stack(self.template) |
|
snip = stack.t.resource_definitions(stack)['deploy_mysql'] |
|
resg = sd.SoftwareDeploymentGroup('deploy_mysql', snip, stack) |
|
self.assertIsNone(resg.validate()) |
|
|
|
|
|
class SDGReplaceTest(common.HeatTestCase): |
|
template = { |
|
'heat_template_version': '2013-05-23', |
|
'resources': { |
|
'deploy_mysql': { |
|
'type': 'OS::Heat::SoftwareDeploymentGroup', |
|
'properties': { |
|
'config': 'config_uuid', |
|
'servers': {'server1': 'uuid1', 'server2': 'uuid2'}, |
|
'input_values': {'foo': 'bar'}, |
|
'name': '10_config' |
|
} |
|
} |
|
} |
|
} |
|
|
|
# 1. existing > batch_size |
|
# 2. existing < batch_size |
|
# 3. count > existing |
|
# 4. count < exiting |
|
# 5. with pause_sec |
|
|
|
scenarios = [ |
|
('1', dict(count=2, |
|
existing=['0', '1'], batch_size=1, |
|
pause_sec=0, tasks=2)), |
|
('2', dict(count=4, |
|
existing=['0', '1'], batch_size=3, |
|
pause_sec=0, tasks=2)), |
|
('3', dict(count=3, |
|
existing=['0', '1'], batch_size=2, |
|
pause_sec=0, tasks=2)), |
|
('4', dict(count=2, |
|
existing=['0', '1', '2'], batch_size=2, |
|
pause_sec=0, tasks=1)), |
|
('5', dict(count=2, |
|
existing=['0', '1'], batch_size=1, |
|
pause_sec=1, tasks=3))] |
|
|
|
def get_fake_nested_stack(self, names): |
|
nested_t = ''' |
|
heat_template_version: 2015-04-30 |
|
description: Resource Group |
|
resources: |
|
''' |
|
resource_snip = ''' |
|
'%s': |
|
type: SoftwareDeployment |
|
properties: |
|
foo: bar |
|
''' |
|
resources = [nested_t] |
|
for res_name in names: |
|
resources.extend([resource_snip % res_name]) |
|
|
|
nested_t = ''.join(resources) |
|
return utils.parse_stack(template_format.parse(nested_t)) |
|
|
|
def setUp(self): |
|
super(SDGReplaceTest, self).setUp() |
|
self.stack = utils.parse_stack(self.template) |
|
snip = self.stack.t.resource_definitions(self.stack)['deploy_mysql'] |
|
self.group = sd.SoftwareDeploymentGroup('deploy_mysql', |
|
snip, self.stack) |
|
self.group.update_with_template = mock.Mock() |
|
self.group.check_update_complete = mock.Mock() |
|
|
|
def test_rolling_updates(self): |
|
self.group._nested = self.get_fake_nested_stack(self.existing) |
|
self.group.get_size = mock.Mock(return_value=self.count) |
|
tasks = self.group._replace(0, self.batch_size, |
|
self.pause_sec) |
|
self.assertEqual(self.tasks, |
|
len(tasks))
|
|
|