deb-heat/heat/tests/test_software_deployment.py
Steven Hardy a2c3e00297 SoftwareDeploymentGroup allow arbitrary keys for attributes
Since SoftwareDeploymentGroup is really a ResourceGroup, it's capable
of resolving any attribute supported by SoftwareDeployment, including
arbitrary outputs mapped to attributes.  Exposing these via the
SoftwareDeploymentGroup resource provides much better flexibility than
forcing users to mangle everything via stdout.

This has the side-effect of making the existing attributes somewhat
redundant, e.g:

get_attr: [a_sdg, deploy_stdouts]

is exactly equivalent to this:

get_attr: [a_sdg, deploy_stdout]

The deploy_stdout attribute should be transparently reflected from the
SoftwareDeployment resources via the normal ResourceGroup interfaces,
so we could consider deprecating the existing attributes at some point.

Change-Id: Ie3b89155d2be0050394eb7f7d5000331cde9aae0
Closes-Bug: #1488921
2015-09-01 15:43:28 +01:00

1369 lines
51 KiB
Python

#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import re
import uuid
import mock
import six
from heat.common import exception as exc
from heat.common.i18n import _
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):
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'
)
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)
sd = self.deployment
self.assertEqual('CFN_SIGNAL', sd.properties.get('signal_transport'))
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)
sd = self.deployment
err = self.assertRaises(exc.StackValidationFailed, 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 test_resource_mapping(self):
self._create_stack(self.template)
self.assertIsInstance(self.deployment, sd.SoftwareDeployment)
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',
}],
'outputs': [],
}
self.rpc_client.show_software_config.return_value = 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': [],
}
self.rpc_client.show_software_config.return_value = 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):
sd = {
'id': 'c8a19429-7fde-47ea-a42f-40045488226c',
'config_id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2'
}
self.rpc_client.create_software_deployment.return_value = sd
return sd
def test_handle_create(self):
self._create_stack(self.template_no_signal)
self.mock_software_config()
derived_sc = self.mock_derived_software_config()
sd = self.mock_deployment()
self.deployment.handle_create()
self.assertEqual(sd['id'], self.deployment.resource_id)
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'
}, {
'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'],
'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_without_config(self):
self._create_stack(self.template_no_config)
sd = self.mock_deployment()
derived_sc = self.mock_derived_software_config()
self.deployment.handle_create()
self.assertEqual(sd['id'], self.deployment.resource_id)
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': None
}, call_arg)
self.assertEqual(
{'action': 'CREATE',
'config_id': derived_sc['id'],
'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()
sd = self.mock_deployment()
self.deployment.handle_create()
self.assertEqual(sd['id'], self.deployment.resource_id)
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'],
'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()
sd = self.mock_deployment()
self.deployment.handle_create()
self.assertEqual(sd['id'], self.deployment.resource_id)
self.assertEqual(
{'action': 'CREATE',
'config_id': derived_sc['id'],
'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)
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
sd['status'] = self.deployment.COMPLETE
self.assertTrue(self.deployment.check_create_complete(sd))
sd['status'] = self.deployment.IN_PROGRESS
self.assertFalse(self.deployment.check_create_complete(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)
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
sd['status'] = self.deployment.COMPLETE
self.assertTrue(self.deployment.check_update_complete(sd))
sd['status'] = self.deployment.IN_PROGRESS
self.assertFalse(self.deployment.check_update_complete(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)
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
sd['status'] = self.deployment.COMPLETE
self.assertTrue(self.deployment.check_suspend_complete(sd))
sd['status'] = self.deployment.IN_PROGRESS
self.assertFalse(self.deployment.check_suspend_complete(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)
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
sd['status'] = self.deployment.COMPLETE
self.assertTrue(self.deployment.check_resume_complete(sd))
sd['status'] = self.deployment.IN_PROGRESS
self.assertFalse(self.deployment.check_resume_complete(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)
sd = {
'status': self.deployment.FAILED,
'status_reason': 'something wrong'
}
self.rpc_client.show_software_deployment.return_value = sd
err = self.assertRaises(
exc.Error, self.deployment.check_create_complete, sd)
self.assertEqual(
'Deployment to server failed: something wrong', six.text_type(err))
def test_handle_delete(self):
self._create_stack(self.template)
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
self.deployment.resource_id = sd['id']
self.deployment.handle_delete()
self.deployment.check_delete_complete()
self.assertEqual(
(self.ctx, sd['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()
sd = self.mock_deployment()
self.assertEqual(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()
sd = self.mock_deployment()
self.deployment.resource_id = sd['id']
self.rpc_client.show_software_deployment.return_value = sd
self.rpc_client.update_software_deployment.return_value = sd
self.assertEqual(sd, self.deployment.handle_delete())
self.assertEqual({
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c',
'action': 'DELETE',
'config_id': derived_sc['id'],
'status': 'IN_PROGRESS',
'status_reason': 'Deploy data available'},
self.rpc_client.update_software_deployment.call_args[1])
sd['status'] = self.deployment.IN_PROGRESS
self.assertFalse(self.deployment.check_delete_complete(sd))
sd['status'] = self.deployment.COMPLETE
self.assertTrue(self.deployment.check_delete_complete(sd))
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()
sd = self.mock_deployment()
sd['config_id'] = derived_sc['id']
self.rpc_client.show_software_deployment.return_value = 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)
sd = self.mock_deployment()
self.deployment.resource_id = sd['id']
self.rpc_client.show_software_deployment.return_value = sd
self.assertTrue(self.deployment.check_delete_complete())
self.assertEqual(
(self.ctx, sd['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()
sd = self.mock_deployment()
rsrc = self.stack['deployment_mysql']
self.rpc_client.show_software_deployment.return_value = sd
self.deployment.resource_id = sd['id']
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)
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, sd['id']),
self.rpc_client.show_software_deployment.call_args[0])
self.assertEqual({
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c',
'action': 'UPDATE',
'config_id': '9966c8e7-bc9c-42de-aa7d-f2447a952cb2',
'status': 'IN_PROGRESS',
'status_reason': u'Deploy data available'},
self.rpc_client.update_software_deployment.call_args[1])
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()
sd = self.mock_deployment()
self.rpc_client.show_software_deployment.return_value = sd
self.deployment.resource_id = sd['id']
# first, handle the suspend
self.deployment.handle_suspend()
self.assertEqual({
'deployment_id': 'c8a19429-7fde-47ea-a42f-40045488226c',
'action': 'SUSPEND',
'config_id': derived_sc['id'],
'status': 'IN_PROGRESS',
'status_reason': 'Deploy data available'},
self.rpc_client.update_software_deployment.call_args[1])
sd['status'] = 'IN_PROGRESS'
self.assertFalse(self.deployment.check_suspend_complete(sd))
sd['status'] = 'COMPLETE'
self.assertTrue(self.deployment.check_suspend_complete(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'],
'status': 'IN_PROGRESS',
'status_reason': 'Deploy data available'},
self.rpc_client.update_software_deployment.call_args[1])
sd['status'] = 'IN_PROGRESS'
self.assertFalse(self.deployment.check_resume_complete(sd))
sd['status'] = 'COMPLETE'
self.assertTrue(self.deployment.check_resume_complete(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 translateable 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)
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 = 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_error(self):
self._create_stack(self.template)
sd = {
'outputs': [],
'output_values': {'foo': 'bar'},
}
self.rpc_client.show_software_deployment.return_value = 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()
sd = self.mock_deployment()
rsrc = self.stack['deployment_mysql']
self.rpc_client.show_software_deployment.return_value = sd
self.deployment.resource_id = sd['id']
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()
sd = self.mock_deployment()
rsrc = self.stack['deployment_mysql']
self.rpc_client.show_software_deployment.return_value = sd
self.deployment.resource_id = sd['id']
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_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.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.deployment.physical_resource_name()
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')
zcc.return_value = zc
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(2, len(zc.queue.mock_calls))
self.assertEqual(queue_id, zc.queue.mock_calls[0][1][0])
self.assertEqual(queue_id, dep_data['zaqar_signal_queue_id'])
self.assertEqual(queue_id,
self.deployment._get_zaqar_signal_queue_id())
def test_delete_zaqar_queue(self):
queue_id = str(uuid.uuid4())
dep_data = {
'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 = self.patch(
'heat.engine.clients.os.zaqar.ZaqarClientPlugin._create')
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))
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 = {
'type': 'OS::Heat::SoftwareDeployment',
'properties': {
'actions': ['CREATE', 'UPDATE'],
'config': 'config_uuid',
'input_values': {'foo': 'bar'},
'name': '10_config',
'signal_transport': 'CFN_SIGNAL'
}
}
self.assertEqual(
expect, resg._build_resource_definition())
self.assertEqual(
expect, resg._build_resource_definition(include_all=True))
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 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']))
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())