Add a replace_on_change option to the SW Config input schema

Change-Id: Idf39e48f801de21e63fcceb8dd779f266a07199f
Co-Authored-By: Steve Baker <sbaker@redhat.com>
Closes-Bug: #1595040
This commit is contained in:
Zane Bitter 2016-03-22 17:17:42 -04:00
parent f238a6e1d5
commit 285802bdd5
4 changed files with 128 additions and 9 deletions

View File

@ -12,6 +12,7 @@
# under the License. # under the License.
import copy import copy
import six
import uuid import uuid
from oslo_config import cfg from oslo_config import cfg
@ -223,7 +224,7 @@ class SoftwareDeployment(signal_responder.SignalResponder):
self.context, **derived_params) self.context, **derived_params)
return derived_config[rpc_api.SOFTWARE_CONFIG_ID] return derived_config[rpc_api.SOFTWARE_CONFIG_ID]
def _handle_action(self, action): def _load_config(self):
if self.properties.get(self.CONFIG): if self.properties.get(self.CONFIG):
config = self.rpc_client().show_software_config( config = self.rpc_client().show_software_config(
self.context, self.properties.get(self.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, []) 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': if config.get(rpc_api.SOFTWARE_CONFIG_GROUP) == 'component':
valid_actions = set() valid_actions = set()
for conf in config[rpc_api.SOFTWARE_CONFIG_CONFIG]['configs']: for conf in config[rpc_api.SOFTWARE_CONFIG_CONFIG]['configs']:
@ -423,11 +430,28 @@ class SoftwareDeployment(signal_responder.SignalResponder):
return self._check_complete() return self._check_complete()
def handle_update(self, json_snippet, tmpl_diff, prop_diff): def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff: old_config_id = self.properties.get(self.CONFIG)
self.properties = json_snippet.properties(self.properties_schema, config = self._load_config()
self.context) 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): def check_update_complete(self, sd):
if not sd: if not sd:

View File

@ -27,11 +27,11 @@ from heat.engine import properties
( (
IO_NAME, DESCRIPTION, TYPE, IO_NAME, DESCRIPTION, TYPE,
DEFAULT, VALUE, DEFAULT, REPLACE_ON_CHANGE, VALUE,
ERROR_OUTPUT, ERROR_OUTPUT,
) = ( ) = (
'name', 'description', 'type', 'name', 'description', 'type',
'default', 'value', 'default', 'replace_on_change', 'value',
'error_output', 'error_output',
) )
@ -62,6 +62,12 @@ input_config_schema = {
properties.Schema.STRING, properties.Schema.STRING,
_('Default value for the input if none is specified.'), _('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 = { output_config_schema = {
@ -127,9 +133,14 @@ class InputConfig(IOConfig):
"""Return the default value of the input.""" """Return the default value of the input."""
return self._props[DEFAULT] return self._props[DEFAULT]
def replace_on_change(self):
return self._props[REPLACE_ON_CHANGE]
def as_dict(self): def as_dict(self):
"""Return a dict representation suitable for persisting.""" """Return a dict representation suitable for persisting."""
d = super(InputConfig, self).as_dict() 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: if self._value is not _no_value:
d[VALUE] = self._value d[VALUE] = self._value
return d return d

View File

@ -921,6 +921,7 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase):
name = 'foo' name = 'foo'
inp = swc_io.InputConfig(name=name) inp = swc_io.InputConfig(name=name)
self.assertIsNone(inp.default()) self.assertIsNone(inp.default())
self.assertIs(False, inp.replace_on_change())
self.assertEqual(name, inp.name()) self.assertEqual(name, inp.name())
self.assertEqual({'name': name, 'type': 'String'}, inp.as_dict()) self.assertEqual({'name': name, 'type': 'String'}, inp.as_dict())
self.assertEqual((name, None), inp.input_data()) self.assertEqual((name, None), inp.input_data())
@ -928,11 +929,13 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase):
def test_input_config(self): def test_input_config(self):
name = 'bar' name = 'bar'
inp = swc_io.InputConfig(name=name, description='test', type='Number', inp = swc_io.InputConfig(name=name, description='test', type='Number',
default=0) default=0, replace_on_change=True)
self.assertEqual('0', inp.default()) self.assertEqual('0', inp.default())
self.assertIs(True, inp.replace_on_change())
self.assertEqual(name, inp.name()) self.assertEqual(name, inp.name())
self.assertEqual({'name': name, 'type': 'Number', self.assertEqual({'name': name, 'type': 'Number',
'description': 'test', 'default': '0'}, 'description': 'test', 'default': '0',
'replace_on_change': True},
inp.as_dict()) inp.as_dict())
self.assertEqual((name, None), inp.input_data()) self.assertEqual((name, None), inp.input_data())
@ -941,6 +944,7 @@ class SoftwareConfigIOSchemaTest(common.HeatTestCase):
inp = swc_io.InputConfig(name=name, type='Number', inp = swc_io.InputConfig(name=name, type='Number',
default=0, value=42) default=0, value=42)
self.assertEqual('0', inp.default()) self.assertEqual('0', inp.default())
self.assertIs(False, inp.replace_on_change())
self.assertEqual(name, inp.name()) self.assertEqual(name, inp.name())
self.assertEqual({'name': name, 'type': 'Number', self.assertEqual({'name': name, 'type': 'Number',
'default': '0', 'value': 42}, 'default': '0', 'value': 42},

View File

@ -233,6 +233,11 @@ class SoftwareDeploymentTest(common.HeatTestCase):
'name': 'bar', 'name': 'bar',
'type': 'String', 'type': 'String',
'default': 'baz', 'default': 'baz',
}, {
'name': 'trigger_replace',
'type': 'String',
'default': 'default_value',
'replace_on_change': True,
}], }],
'outputs': [], 'outputs': [],
} }
@ -332,6 +337,12 @@ class SoftwareDeploymentTest(common.HeatTestCase):
'name': 'bar', 'name': 'bar',
'type': 'String', 'type': 'String',
'value': 'baz' 'value': 'baz'
}, {
'default': 'default_value',
'name': 'trigger_replace',
'replace_on_change': True,
'type': 'String',
'value': 'default_value'
}, { }, {
'name': 'bink', 'name': 'bink',
'type': 'String', 'type': 'String',
@ -784,6 +795,75 @@ class SoftwareDeploymentTest(common.HeatTestCase):
'status_reason': u'Deploy data available'}, 'status_reason': u'Deploy data available'},
self.rpc_client.update_software_deployment.call_args[1]) 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): def test_handle_suspend_resume(self):
self._create_stack(self.template_delete_suspend_resume) self._create_stack(self.template_delete_suspend_resume)