Skip to content

Commit

Permalink
Add a modulo core constraint
Browse files Browse the repository at this point in the history
A modulo constraint will be used to restrict numeric values to
leave a certain remainder when divided with certain divisor.
E.g. we can use this to constrain values to even/odd numbers.

Change-Id: I9d7db4307be2a2b93cc928cf5912af7b49c72076
  • Loading branch information
infraredgirl committed Oct 11, 2016
1 parent f2182e1 commit b1144b2
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 30 deletions.
6 changes: 6 additions & 0 deletions doc/source/developing_guides/pluginguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ the end user.
Constrains a numerical value. Applicable to INTEGER and NUMBER.
Both ``min`` and ``max`` default to ``None``.

*Modulo(step, offset, description)*:
Starting with the specified ``offset``, every multiple of ``step`` is a valid
value. Applicable to INTEGER and NUMBER.

Available from template version 2017-02-24.

*CustomConstraint(name, description, environment)*:
This constructor brings in a named constraint class from an
environment. If the given environment is ``None`` (its default)
Expand Down
18 changes: 18 additions & 0 deletions doc/source/template_guide/hot_spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,24 @@ following range constraint would allow for all numeric values between 0 and
range: { min: 0, max: 10 }
modulo
++++++
The ``modulo`` constraint applies to parameters of type ``number``. The value
is valid if it is a multiple of ``step``, starting with ``offset``.

The syntax of the ``modulo`` constraint is

.. code-block:: yaml
modulo: { step: <step>, offset: <offset> }
Both ``step`` and ``offset`` must be specified.

For example, the following modulo constraint would only allow for odd numbers

.. code-block:: yaml
modulo: { step: 2, offset: 1 }
allowed_values
++++++++++++++
Expand Down
7 changes: 7 additions & 0 deletions heat/engine/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,13 @@ def format_validate_parameter(param):
if c.max is not None:
res[rpc_api.PARAM_MAX_VALUE] = c.max

elif isinstance(c, constr.Modulo):
if c.step is not None:
res[rpc_api.PARAM_STEP] = c.step

if c.offset is not None:
res[rpc_api.PARAM_OFFSET] = c.offset

elif isinstance(c, constr.AllowedValues):
res[rpc_api.PARAM_ALLOWED_VALUES] = list(c.allowed)

Expand Down
79 changes: 79 additions & 0 deletions heat/engine/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,85 @@ def _is_valid(self, value, schema, context, template):
template)


class Modulo(Constraint):
"""Constrain values to modulo.
Serializes to JSON as::
{
'modulo': {'step': <step>, 'offset': <offset>},
'description': <description>
}
"""

(STEP, OFFSET) = ('step', 'offset')

valid_types = (Schema.INTEGER_TYPE, Schema.NUMBER_TYPE,)

def __init__(self, step=None, offset=None, description=None):
super(Modulo, self).__init__(description)
self.step = step
self.offset = offset

if step is None or offset is None:
raise exception.InvalidSchemaError(
message=_('A modulo constraint must have a step value and '
'an offset value specified.'))

for param in (step, offset):
if not isinstance(param, (float, six.integer_types, type(None))):
raise exception.InvalidSchemaError(
message=_('step/offset must be numeric'))

if not int(param) == param:
raise exception.InvalidSchemaError(
message=_('step/offset must be integer'))

step, offset = int(step), int(offset)

if step == 0:
raise exception.InvalidSchemaError(message=_('step cannot be 0.'))

if abs(offset) >= abs(step):
raise exception.InvalidSchemaError(
message=_('offset must be smaller (by absolute value) '
'than step.'))

if step * offset < 0:
raise exception.InvalidSchemaError(
message=_('step and offset must be both positive or both '
'negative.'))

def _str(self):
if self.step is None or self.offset is None:
fmt = _('The values must be specified.')
else:
fmt = _('The value must be a multiple of %(step)s '
'with an offset of %(offset)s.')
return fmt % self._constraint()

def _err_msg(self, value):
return '%s is not a multiple of %s with an offset of %s)' % (
value, self.step, self.offset)

def _is_valid(self, value, schema, context, template):
value = Schema.str_to_num(value)

if value % self.step != self.offset:
return False

return True

def _constraint(self):
def constraints():
if self.step is not None:
yield self.STEP, self.step
if self.offset is not None:
yield self.OFFSET, self.offset

return dict(constraints())


class AllowedValues(Constraint):
"""Constrain values to a predefined set.
Expand Down
78 changes: 50 additions & 28 deletions heat/engine/hot/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@


PARAM_CONSTRAINTS = (
DESCRIPTION, LENGTH, RANGE, ALLOWED_VALUES, ALLOWED_PATTERN,
DESCRIPTION, LENGTH, RANGE, MODULO, ALLOWED_VALUES, ALLOWED_PATTERN,
CUSTOM_CONSTRAINT,
) = (
'description', 'length', 'range', 'allowed_values', 'allowed_pattern',
'custom_constraint',
'description', 'length', 'range', 'modulo', 'allowed_values',
'allowed_pattern', 'custom_constraint',
)

RANGE_KEYS = (MIN, MAX) = ('min', 'max')

MODULO_KEYS = (STEP, OFFSET) = ('step', 'offset')


class HOTParamSchema(parameters.Schema):
"""HOT parameter schema."""
Expand All @@ -49,6 +51,34 @@ class HOTParamSchema(parameters.Schema):

PARAMETER_KEYS = KEYS

@classmethod
def _constraint_from_def(cls, constraint):
desc = constraint.get(DESCRIPTION)
if RANGE in constraint:
cdef = constraint.get(RANGE)
cls._check_dict(cdef, RANGE_KEYS, 'range constraint')
return constr.Range(parameters.Schema.get_num(MIN, cdef),
parameters.Schema.get_num(MAX, cdef),
desc)
elif LENGTH in constraint:
cdef = constraint.get(LENGTH)
cls._check_dict(cdef, RANGE_KEYS, 'length constraint')
return constr.Length(parameters.Schema.get_num(MIN, cdef),
parameters.Schema.get_num(MAX, cdef),
desc)
elif ALLOWED_VALUES in constraint:
cdef = constraint.get(ALLOWED_VALUES)
return constr.AllowedValues(cdef, desc)
elif ALLOWED_PATTERN in constraint:
cdef = constraint.get(ALLOWED_PATTERN)
return constr.AllowedPattern(cdef, desc)
elif CUSTOM_CONSTRAINT in constraint:
cdef = constraint.get(CUSTOM_CONSTRAINT)
return constr.CustomConstraint(cdef, desc)
else:
raise exception.InvalidSchemaError(
message=_("No constraint expressed"))

@classmethod
def from_dict(cls, param_name, schema_dict):
"""Return a Parameter Schema object from a legacy schema dictionary.
Expand All @@ -72,31 +102,7 @@ def constraints():
for constraint in constraints:
cls._check_dict(constraint, PARAM_CONSTRAINTS,
'parameter constraints')
desc = constraint.get(DESCRIPTION)
if RANGE in constraint:
cdef = constraint.get(RANGE)
cls._check_dict(cdef, RANGE_KEYS, 'range constraint')
yield constr.Range(parameters.Schema.get_num(MIN, cdef),
parameters.Schema.get_num(MAX, cdef),
desc)
elif LENGTH in constraint:
cdef = constraint.get(LENGTH)
cls._check_dict(cdef, RANGE_KEYS, 'length constraint')
yield constr.Length(parameters.Schema.get_num(MIN, cdef),
parameters.Schema.get_num(MAX, cdef),
desc)
elif ALLOWED_VALUES in constraint:
cdef = constraint.get(ALLOWED_VALUES)
yield constr.AllowedValues(cdef, desc)
elif ALLOWED_PATTERN in constraint:
cdef = constraint.get(ALLOWED_PATTERN)
yield constr.AllowedPattern(cdef, desc)
elif CUSTOM_CONSTRAINT in constraint:
cdef = constraint.get(CUSTOM_CONSTRAINT)
yield constr.CustomConstraint(cdef, desc)
else:
raise exception.InvalidSchemaError(
message=_("No constraint expressed"))
yield cls._constraint_from_def(constraint)

# make update_allowed true by default on TemplateResources
# as the template should deal with this.
Expand All @@ -109,6 +115,22 @@ def constraints():
immutable=schema_dict.get(HOTParamSchema.IMMUTABLE, False))


class HOTParamSchema20170224(HOTParamSchema):
@classmethod
def _constraint_from_def(cls, constraint):
desc = constraint.get(DESCRIPTION)

if MODULO in constraint:
cdef = constraint.get(MODULO)
cls._check_dict(cdef, MODULO_KEYS, 'modulo constraint')
return constr.Modulo(parameters.Schema.get_num(STEP, cdef),
parameters.Schema.get_num(OFFSET, cdef),
desc)
else:
return super(HOTParamSchema20170224, cls)._constraint_from_def(
constraint)


class HOTParameters(parameters.Parameters):
PSEUDO_PARAMETERS = (
PARAM_STACK_ID, PARAM_STACK_NAME, PARAM_REGION, PARAM_PROJECT_ID
Expand Down
6 changes: 5 additions & 1 deletion heat/engine/hot/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ class HOTemplate20130523(template_common.CommonTemplate):
'Snapshot': rsrc_defn.ResourceDefinition.SNAPSHOT
}

param_schema_class = parameters.HOTParamSchema

def __getitem__(self, section):
""""Get the relevant section in the template."""
# first translate from CFN into HOT terminology if necessary
Expand Down Expand Up @@ -211,7 +213,7 @@ def param_schemata(self, param_defaults=None):
parameter_section[name]['default'] = pdefaults[name]

params = six.iteritems(parameter_section)
return dict((name, parameters.HOTParamSchema.from_dict(name, schema))
return dict((name, self.param_schema_class.from_dict(name, schema))
for name, schema in params)

def parameters(self, stack_identifier, user_params, param_defaults=None):
Expand Down Expand Up @@ -549,3 +551,5 @@ class HOTemplate20170224(HOTemplate20161014):
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}

param_schema_class = parameters.HOTParamSchema20170224
3 changes: 2 additions & 1 deletion heat/rpc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,13 @@
PARAM_TYPE, PARAM_DEFAULT, PARAM_NO_ECHO,
PARAM_ALLOWED_VALUES, PARAM_ALLOWED_PATTERN, PARAM_MAX_LENGTH,
PARAM_MIN_LENGTH, PARAM_MAX_VALUE, PARAM_MIN_VALUE,
PARAM_STEP, PARAM_OFFSET,
PARAM_DESCRIPTION, PARAM_CONSTRAINT_DESCRIPTION, PARAM_LABEL,
PARAM_CUSTOM_CONSTRAINT, PARAM_VALUE
) = (
'Type', 'Default', 'NoEcho',
'AllowedValues', 'AllowedPattern', 'MaxLength',
'MinLength', 'MaxValue', 'MinValue',
'MinLength', 'MaxValue', 'MinValue', 'Step', 'Offset',
'Description', 'ConstraintDescription', 'Label',
'CustomConstraint', 'Value'
)
Expand Down
Loading

0 comments on commit b1144b2

Please sign in to comment.