Make template available to parameter constraint checking

The validate method for a constraint only accepts the value being
verified. This is sufficient for most data checks (range, regex) and the
calls to one of the service clients. But it is limiting in the sense
that it prevents us from ever having constraints that span multiple
parameters (such as ensuring that two values are compatible).

What caused this is the atttempt to add a constraint to ensure a
parameter's value is a valid Heat resource type. To verify this, the
constraint code needs access to the resolved environment. I opted to
pass the entire template down instead to make things more flexible.

The call to the BaseCustomConstraint (which appears to be the entry
point for most plugins) will attempt to use the new signature with the
template, but will fall back to the original call for backward
compatibility.

Change-Id: I5dc94c964b35578e84da641278a4175a37038498
This commit is contained in:
Jay Dobies 2016-02-25 15:38:00 -05:00
parent 6499a52e4c
commit 4fdf72b000
4 changed files with 53 additions and 38 deletions

View File

@ -205,14 +205,15 @@ class Schema(collections.Mapping):
return value
def validate_constraints(self, value, context=None, skipped=None):
def validate_constraints(self, value, context=None, skipped=None,
template=None):
if not skipped:
skipped = []
try:
for constraint in self.constraints:
if type(constraint) not in skipped:
constraint.validate(value, self, context)
constraint.validate(value, self, context, template)
except ValueError as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
@ -296,8 +297,8 @@ class Constraint(collections.Mapping):
return '\n'.join(desc())
def validate(self, value, schema=None, context=None):
if not self._is_valid(value, schema, context):
def validate(self, value, schema=None, context=None, template=None):
if not self._is_valid(value, schema, context, template):
if self.description:
err_msg = self.description
else:
@ -374,7 +375,7 @@ class Range(Constraint):
self.min,
self.max)
def _is_valid(self, value, schema, context):
def _is_valid(self, value, schema, context, template):
value = Schema.str_to_num(value)
if self.min is not None:
@ -437,8 +438,9 @@ class Length(Range):
self.min,
self.max)
def _is_valid(self, value, schema, context):
return super(Length, self)._is_valid(len(value), schema, context)
def _is_valid(self, value, schema, context, template):
return super(Length, self)._is_valid(len(value), schema, context,
template)
class AllowedValues(Constraint):
@ -471,7 +473,7 @@ class AllowedValues(Constraint):
allowed = '[%s]' % ', '.join(str(a) for a in self.allowed)
return '"%s" is not an allowed value %s' % (value, allowed)
def _is_valid(self, value, schema, context):
def _is_valid(self, value, schema, context, template):
# For list values, check if all elements of the list are contained
# in allowed list.
if isinstance(value, list):
@ -514,7 +516,7 @@ class AllowedPattern(Constraint):
def _err_msg(self, value):
return '"%s" does not match pattern "%s"' % (value, self.pattern)
def _is_valid(self, value, schema, context):
def _is_valid(self, value, schema, context, template):
match = self.match(value)
return match is not None and match.end() == len(value)
@ -565,11 +567,19 @@ class CustomConstraint(Constraint):
return _('"%(value)s" does not validate %(name)s') % {
"value": value, "name": self.name}
def _is_valid(self, value, schema, context):
def _is_valid(self, value, schema, context, template):
constraint = self.custom_constraint
if not constraint:
return False
return constraint.validate(value, context)
try:
result = constraint.validate(value, context,
template=template)
except TypeError:
# for backwards compatibility with older service constraints
result = constraint.validate(value, context)
return result
class BaseCustomConstraint(object):
@ -591,7 +601,7 @@ class BaseCustomConstraint(object):
return _("Error validating value '%(value)s': %(message)s") % {
"value": value, "message": self._error_message}
def validate(self, value, context):
def validate(self, value, context, template=None):
@MEMOIZE
def check_cache_or_validate_value(cache_value_prefix,

View File

@ -163,8 +163,9 @@ class Schema(constr.Schema):
'false')).lower() == 'true',
label=schema_dict.get(LABEL))
def validate_value(self, value, context=None):
super(Schema, self).validate_constraints(value, context)
def validate_value(self, value, context=None, template=None):
super(Schema, self).validate_constraints(value, context=context,
template=template)
def __getitem__(self, key):
if key == self.TYPE:
@ -214,7 +215,7 @@ class Parameter(object):
self.user_value = value
self.user_default = None
def validate(self, validate_value=True, context=None):
def validate(self, validate_value=True, context=None, template=None):
"""Validates the parameter.
This method validates if the parameter's schema is valid,
@ -230,9 +231,9 @@ class Parameter(object):
return
if self.user_value is not None:
self._validate(self.user_value, context)
self._validate(self.user_value, context, template)
elif self.has_default():
self._validate(self.default(), context)
self._validate(self.default(), context, template)
else:
raise exception.UserParameterMissing(key=self.name)
except exception.StackValidationFailed as ex:
@ -305,12 +306,12 @@ class NumberParam(Parameter):
"""Return a float representation of the parameter."""
return float(super(NumberParam, self).value())
def _validate(self, val, context):
def _validate(self, val, context, template=None):
try:
Schema.str_to_num(val)
except ValueError as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
self.schema.validate_value(val, context)
self.schema.validate_value(val, context=context, template=template)
def value(self):
return Schema.str_to_num(super(NumberParam, self).value())
@ -319,12 +320,12 @@ class NumberParam(Parameter):
class BooleanParam(Parameter):
"""A template parameter of type "Boolean"."""
def _validate(self, val, context):
def _validate(self, val, context, template=None):
try:
strutils.bool_from_string(val, strict=True)
except ValueError as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
self.schema.validate_value(val, context)
self.schema.validate_value(val, context=context, template=template)
def value(self):
if self.user_value is not None:
@ -337,8 +338,8 @@ class BooleanParam(Parameter):
class StringParam(Parameter):
"""A template parameter of type "String"."""
def _validate(self, val, context):
self.schema.validate_value(val, context)
def _validate(self, val, context, template=None):
self.schema.validate_value(val, context=context, template=template)
def value(self):
return self.schema.to_schema_type(super(StringParam, self).value())
@ -403,12 +404,12 @@ class CommaDelimitedListParam(ParsedParameter, collections.Sequence):
return super(CommaDelimitedListParam, self).__str__()
return ",".join(self.value())
def _validate(self, val, context):
def _validate(self, val, context, template=None):
try:
parsed = self.parse(val)
except ValueError as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
self.schema.validate_value(parsed, context)
self.schema.validate_value(parsed, context=context, template=template)
class JsonParam(ParsedParameter):
@ -451,12 +452,12 @@ class JsonParam(ParsedParameter):
return super(JsonParam, self).__str__()
return encodeutils.safe_decode(jsonutils.dumps(self.value()))
def _validate(self, val, context):
def _validate(self, val, context, template=None):
try:
parsed = self.parse(val)
except ValueError as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
self.schema.validate_value(parsed, context)
self.schema.validate_value(parsed, context=context, template=template)
class Parameters(collections.Mapping):
@ -514,7 +515,7 @@ class Parameters(collections.Mapping):
self._validate_user_parameters()
for param in six.itervalues(self.params):
param.validate(validate_value, context)
param.validate(validate_value, context, self.tmpl)
def __contains__(self, key):
"""Return whether the specified parameter exists."""

View File

@ -307,7 +307,7 @@ class Property(object):
return normalised == 'true'
def get_value(self, value, validate=False):
def get_value(self, value, validate=False, template=None):
"""Get value from raw value and sanitize according to data type."""
t = self.type()
@ -325,7 +325,8 @@ class Property(object):
_value = self._get_bool(value)
if validate:
self.schema.validate_constraints(_value, self.context)
self.schema.validate_constraints(_value, self.context,
template=template)
return _value
@ -357,7 +358,7 @@ class Properties(collections.Mapping):
in params_snippet.items())
return {}
def validate(self, with_value=True):
def validate(self, with_value=True, template=None):
try:
for key in self.data:
if key not in self.props:
@ -377,7 +378,9 @@ class Properties(collections.Mapping):
if with_value:
try:
self._get_property_value(key, validate=True)
self._get_property_value(key,
validate=True,
template=template)
except exception.StackValidationFailed as ex:
path = [key]
path.extend(ex.path)
@ -412,7 +415,7 @@ class Properties(collections.Mapping):
if any(res.action == res.INIT for res in deps):
return True
def get_user_value(self, key, validate=False):
def get_user_value(self, key, validate=False, template=None):
if key not in self:
raise KeyError(_('Invalid Property %s') % key)
@ -425,7 +428,7 @@ class Properties(collections.Mapping):
validate = False
value = self.resolve(unresolved_value)
return prop.get_value(value, validate)
return prop.get_value(value, validate, template=template)
# Children can raise StackValidationFailed with unique path which
# is necessary for further use in StackValidationFailed exception.
# So we need to handle this exception in this method.
@ -437,15 +440,15 @@ class Properties(collections.Mapping):
except Exception as e:
raise ValueError(six.text_type(e))
def _get_property_value(self, key, validate=False):
def _get_property_value(self, key, validate=False, template=None):
if key not in self:
raise KeyError(_('Invalid Property %s') % key)
prop = self.props[key]
if key in self.data:
return self.get_user_value(key, validate)
return self.get_user_value(key, validate, template=template)
elif prop.has_default():
return prop.get_value(None, validate)
return prop.get_value(None, validate, template=template)
elif prop.required():
raise ValueError(_('Property %s not assigned') % key)

View File

@ -1379,7 +1379,8 @@ class Resource(object):
self.context).validate()
try:
validate = self.properties.validate(
with_value=self.stack.strict_validate)
with_value=self.stack.strict_validate,
template=self.t)
except exception.StackValidationFailed as ex:
path = [self.stack.t.RESOURCES, ex.path[0],
self.stack.t.get_section_name(ex.path[1])]