Immutable parameters

Add a new "immutable" boolean field to the parameters section in a HOT
template.  A value of True will result in the engine rejecting
stack-updates that include changes to that parameter.  When not
specified in the template, "immutable" defaults to False to ensure
backwards compatibility with old templates.

blueprint: immutable-parameters
Change-Id: If17df0b97e96ff2a1926451634e4744d6c3e26f1
This commit is contained in:
Jason Dunsmore 2015-12-02 16:24:25 -06:00
parent 834ac4a041
commit 98262c10d8
7 changed files with 159 additions and 10 deletions

View File

@ -133,6 +133,16 @@ class InvalidTemplateParameter(HeatException):
msg_fmt = _("The Parameter (%(key)s) has no attributes.")
class ImmutableParameterModified(HeatException):
msg_fmt = _("The following parameters are immutable and may not be "
"updated: %(keys)s")
def __init__(self, *args, **kwargs):
if args:
kwargs.update({'keys': ", ".join(args)})
super(ImmutableParameterModified, self).__init__(**kwargs)
class InvalidTemplateAttribute(HeatException):
msg_fmt = _("The Referenced Attribute (%(resource)s %(key)s)"
" is incorrect.")

View File

@ -64,8 +64,10 @@ class Schema(collections.Mapping):
KEYS = (
TYPE, DESCRIPTION, DEFAULT, SCHEMA, REQUIRED, CONSTRAINTS,
IMMUTABLE,
) = (
'type', 'description', 'default', 'schema', 'required', 'constraints',
'immutable',
)
# Keywords for data types; each Schema subclass can define its respective
@ -88,7 +90,8 @@ class Schema(collections.Mapping):
def __init__(self, data_type, description=None,
default=None, schema=None,
required=False, constraints=None, label=None):
required=False, constraints=None, label=None,
immutable=False):
self._len = None
self.label = label
self.type = data_type
@ -102,6 +105,7 @@ class Schema(collections.Mapping):
self.description = description
self.required = required
self.immutable = immutable
if isinstance(schema, type(self)):
if self.type != self.LIST:

View File

@ -33,10 +33,10 @@ class HOTParamSchema(parameters.Schema):
KEYS = (
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS,
HIDDEN, LABEL
HIDDEN, LABEL, IMMUTABLE
) = (
'type', 'description', 'default', 'schema', 'constraints',
'hidden', 'label'
'hidden', 'label', 'immutable'
)
# For Parameters the type name for Schema.LIST is comma_delimited_list
@ -105,7 +105,8 @@ class HOTParamSchema(parameters.Schema):
default=schema_dict.get(HOTParamSchema.DEFAULT),
constraints=list(constraints()),
hidden=schema_dict.get(HOTParamSchema.HIDDEN, False),
label=schema_dict.get(HOTParamSchema.LABEL))
label=schema_dict.get(HOTParamSchema.LABEL),
immutable=schema_dict.get(HOTParamSchema.IMMUTABLE, False))
class HOTParameters(parameters.Parameters):

View File

@ -39,10 +39,11 @@ class Schema(constr.Schema):
"""Parameter schema."""
KEYS = (
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS, HIDDEN, LABEL
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS, HIDDEN,
LABEL, IMMUTABLE
) = (
'Type', 'Description', 'Default', 'Schema', 'Constraints', 'NoEcho',
'Label'
'Label', 'Immutable'
)
PARAMETER_KEYS = PARAMETER_KEYS
@ -56,14 +57,15 @@ class Schema(constr.Schema):
)
def __init__(self, data_type, description=None, default=None, schema=None,
constraints=None, hidden=False, label=None):
constraints=None, hidden=False, label=None, immutable=False):
super(Schema, self).__init__(data_type=data_type,
description=description,
default=default,
schema=schema,
required=default is None,
constraints=constraints,
label=label)
label=label,
immutable=immutable)
self.hidden = hidden
# Schema class validates default value for lists assuming list type. For
@ -493,6 +495,8 @@ class Parameters(collections.Mapping):
self.params = dict((p.name,
p) for p in itertools.chain(pseudo_parameters,
user_parameters))
self.non_pseudo_param_keys = [p for p in self.params if p not in
self.PSEUDO_PARAMETERS]
for pd in six.iterkeys(param_defaults):
if pd in self.params:
@ -585,3 +589,21 @@ class Parameters(collections.Mapping):
'ap-southeast-1',
'ap-northeast-1']
)]))
def immutable_params_modified(self, new_parameters, input_params):
# A parameter must have been present in the old stack for its
# immutability to be enforced
common_params = list(set(new_parameters.non_pseudo_param_keys)
& set(self.non_pseudo_param_keys))
invalid_params = []
for param in common_params:
old_value = self.params[param]
if param in input_params:
new_value = input_params[param]
else:
new_value = new_parameters[param]
immutable = new_parameters.params[param].schema.immutable
if immutable and old_value.value() != new_value:
invalid_params.append(param)
if invalid_params:
return invalid_params

View File

@ -62,10 +62,10 @@ class Schema(constr.Schema):
support_status=support.SupportStatus(),
allow_conversion=False):
super(Schema, self).__init__(data_type, description, default,
schema, required, constraints)
schema, required, constraints,
immutable=immutable)
self.implemented = implemented
self.update_allowed = update_allowed
self.immutable = immutable
self.support_status = support_status
self.allow_conversion = allow_conversion
# validate structural correctness of schema itself

View File

@ -851,6 +851,12 @@ class EngineService(service.Service):
current_kwargs.update(common_params)
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
**current_kwargs)
invalid_params = current_stack.parameters.immutable_params_modified(
updated_stack.parameters, params)
if invalid_params:
raise exception.ImmutableParameterModified(*invalid_params)
self.resource_enforcer.enforce_stack(updated_stack)
updated_stack.parameters.set_stack_id(current_stack.identifier())

View File

@ -675,6 +675,112 @@ class ServiceStackUpdateTest(common.HeatTestCase):
self.assertIn("PATCH update to non-COMPLETE stack",
six.text_type(ex.exc_info[1]))
def test_update_immutable_parameter_disallowed(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: true
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
params = {}
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=old_stack.t)
self.patchobject(environment, 'Environment',
return_value=old_stack.env)
params = {'param1': 'bar'}
exc = self.assertRaises(dispatcher.ExpectedException,
self.man.update_stack,
self.ctx, old_stack.identifier(),
templatem.Template(template), params,
None, {})
self.assertEqual(exception.ImmutableParameterModified, exc.exc_info[0])
self.assertEqual('The following parameters are immutable and may not '
'be updated: param1', exc.exc_info[1].message)
def test_update_mutable_parameter_allowed(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: false
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
params = {}
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=old_stack.t)
self.patchobject(environment, 'Environment',
return_value=old_stack.env)
params = {'param1': 'bar'}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
templatem.Template(template), params,
None, {})
self.assertEqual(s.id, result['stack_id'])
def test_update_immutable_parameter_same_value(self):
template = '''
heat_template_version: 2014-10-16
parameters:
param1:
type: string
immutable: true
default: foo
'''
self.ctx = utils.dummy_context(password=None)
stack_name = 'test_update_immutable_parameters'
params = {}
old_stack = tools.get_stack(stack_name, self.ctx,
template=template)
sid = old_stack.store()
old_stack.set_stack_user_project_id('1234')
s = stack_object.Stack.get_by_id(self.ctx, sid)
# prepare mocks
self.patchobject(self.man, '_get_stack', return_value=s)
self.patchobject(stack, 'Stack', return_value=old_stack)
self.patchobject(stack.Stack, 'load', return_value=old_stack)
self.patchobject(templatem, 'Template', return_value=old_stack.t)
self.patchobject(environment, 'Environment',
return_value=old_stack.env)
params = {'param1': 'foo'}
result = self.man.update_stack(self.ctx, old_stack.identifier(),
templatem.Template(template), params,
None, {})
self.assertEqual(s.id, result['stack_id'])
class ServiceStackUpdatePreviewTest(common.HeatTestCase):