diff --git a/heat/engine/resources/openstack/heat/software_deployment.py b/heat/engine/resources/openstack/heat/software_deployment.py index d1a13a74bb..451507d139 100644 --- a/heat/engine/resources/openstack/heat/software_deployment.py +++ b/heat/engine/resources/openstack/heat/software_deployment.py @@ -12,6 +12,7 @@ # under the License. import copy +import six import uuid from oslo_config import cfg @@ -223,7 +224,7 @@ class SoftwareDeployment(signal_responder.SignalResponder): self.context, **derived_params) return derived_config[rpc_api.SOFTWARE_CONFIG_ID] - def _handle_action(self, action): + def _load_config(self): if self.properties.get(self.CONFIG): config = self.rpc_client().show_software_config( self.context, self.properties.get(self.CONFIG)) @@ -239,6 +240,12 @@ class SoftwareDeployment(signal_responder.SignalResponder): for o in config.get(rpc_api.SOFTWARE_CONFIG_OUTPUTS, []) ] + return config + + def _handle_action(self, action, config=None): + if config is None: + config = self._load_config() + if config.get(rpc_api.SOFTWARE_CONFIG_GROUP) == 'component': valid_actions = set() for conf in config[rpc_api.SOFTWARE_CONFIG_CONFIG]['configs']: @@ -423,11 +430,28 @@ class SoftwareDeployment(signal_responder.SignalResponder): return self._check_complete() def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if prop_diff: - self.properties = json_snippet.properties(self.properties_schema, - self.context) + old_config_id = self.properties.get(self.CONFIG) + config = self._load_config() + old_inputs = {i.name(): i + for i in self._build_derived_inputs(self.UPDATE, config)} - return self._handle_action(self.UPDATE) + self.properties = json_snippet.properties(self.properties_schema, + self.context) + + new_config_id = self.properties.get(self.CONFIG) + if old_config_id != new_config_id: + config = self._load_config() + new_inputs = {i.name(): i + for i in self._build_derived_inputs(self.UPDATE, config)} + + for name, inp in six.iteritems(new_inputs): + if inp.replace_on_change() and name in old_inputs: + if inp.input_data() != old_inputs[name].input_data(): + LOG.debug('Replacing SW Deployment due to change in ' + 'input "%s"', name) + raise exception.UpdateReplace + + return self._handle_action(self.UPDATE, config=config) def check_update_complete(self, sd): if not sd: diff --git a/heat/engine/software_config_io.py b/heat/engine/software_config_io.py index 0bac6a8c04..a77c4da3a6 100644 --- a/heat/engine/software_config_io.py +++ b/heat/engine/software_config_io.py @@ -27,11 +27,11 @@ from heat.engine import properties ( IO_NAME, DESCRIPTION, TYPE, - DEFAULT, VALUE, + DEFAULT, REPLACE_ON_CHANGE, VALUE, ERROR_OUTPUT, ) = ( 'name', 'description', 'type', - 'default', 'value', + 'default', 'replace_on_change', 'value', 'error_output', ) @@ -62,6 +62,12 @@ input_config_schema = { properties.Schema.STRING, _('Default value for the input if none is specified.'), ), + REPLACE_ON_CHANGE: properties.Schema( + properties.Schema.BOOLEAN, + _('Replace the deployment instead of updating it when the input ' + 'value changes.'), + default=False, + ), } output_config_schema = { @@ -127,9 +133,14 @@ class InputConfig(IOConfig): """Return the default value of the input.""" return self._props[DEFAULT] + def replace_on_change(self): + return self._props[REPLACE_ON_CHANGE] + def as_dict(self): """Return a dict representation suitable for persisting.""" d = super(InputConfig, self).as_dict() + if not self._props[REPLACE_ON_CHANGE]: + del d[REPLACE_ON_CHANGE] if self._value is not _no_value: d[VALUE] = self._value return d diff --git a/heat/tests/engine/service/test_software_config.py b/heat/tests/engine/service/test_software_config.py index c6979c9d94..6e27d31767 100644 --- a/heat/tests/engine/service/test_software_config.py +++ b/heat/tests/engine/service/test_software_config.py @@ -921,6 +921,7 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase): name = 'foo' inp = swc_io.InputConfig(name=name) self.assertIsNone(inp.default()) + self.assertIs(False, inp.replace_on_change()) self.assertEqual(name, inp.name()) self.assertEqual({'name': name, 'type': 'String'}, inp.as_dict()) self.assertEqual((name, None), inp.input_data()) @@ -928,11 +929,13 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase): def test_input_config(self): name = 'bar' inp = swc_io.InputConfig(name=name, description='test', type='Number', - default=0) + default=0, replace_on_change=True) self.assertEqual('0', inp.default()) + self.assertIs(True, inp.replace_on_change()) self.assertEqual(name, inp.name()) self.assertEqual({'name': name, 'type': 'Number', - 'description': 'test', 'default': '0'}, + 'description': 'test', 'default': '0', + 'replace_on_change': True}, inp.as_dict()) self.assertEqual((name, None), inp.input_data()) @@ -941,6 +944,7 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase): inp = swc_io.InputConfig(name=name, type='Number', default=0, value=42) self.assertEqual('0', inp.default()) + self.assertIs(False, inp.replace_on_change()) self.assertEqual(name, inp.name()) self.assertEqual({'name': name, 'type': 'Number', 'default': '0', 'value': 42}, diff --git a/heat/tests/openstack/heat/test_software_deployment.py b/heat/tests/openstack/heat/test_software_deployment.py index 90cdc81dd9..ac9c593fd8 100644 --- a/heat/tests/openstack/heat/test_software_deployment.py +++ b/heat/tests/openstack/heat/test_software_deployment.py @@ -233,6 +233,11 @@ class SoftwareDeploymentTest(common.HeatTestCase): 'name': 'bar', 'type': 'String', 'default': 'baz', + }, { + 'name': 'trigger_replace', + 'type': 'String', + 'default': 'default_value', + 'replace_on_change': True, }], 'outputs': [], } @@ -332,6 +337,12 @@ class SoftwareDeploymentTest(common.HeatTestCase): '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', @@ -784,6 +795,75 @@ class SoftwareDeploymentTest(common.HeatTestCase): '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)