Merge "Handle unicode in constraints"
This commit is contained in:
commit
7ffcda79da
|
@ -12,6 +12,7 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import json
|
||||||
import numbers
|
import numbers
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -196,7 +197,8 @@ class Schema(collections.Mapping):
|
||||||
elif self.type == self.STRING:
|
elif self.type == self.STRING:
|
||||||
return six.text_type(value)
|
return six.text_type(value)
|
||||||
elif self.type == self.BOOLEAN:
|
elif self.type == self.BOOLEAN:
|
||||||
return strutils.bool_from_string(str(value), strict=True)
|
return strutils.bool_from_string(six.text_type(value),
|
||||||
|
strict=True)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(_('Value "%(val)s" is invalid for data type '
|
raise ValueError(_('Value "%(val)s" is invalid for data type '
|
||||||
'"%(type)s".')
|
'"%(type)s".')
|
||||||
|
@ -264,7 +266,7 @@ class AnyIndexDict(collections.Mapping):
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if key != self.ANYTHING and not isinstance(key, six.integer_types):
|
if key != self.ANYTHING and not isinstance(key, six.integer_types):
|
||||||
raise KeyError(_('Invalid key %s') % str(key))
|
raise KeyError(_('Invalid key %s') % key)
|
||||||
|
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -275,6 +277,7 @@ class AnyIndexDict(collections.Mapping):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@six.python_2_unicode_compatible
|
||||||
class Constraint(collections.Mapping):
|
class Constraint(collections.Mapping):
|
||||||
"""Parent class for constraints on allowable values for a Property.
|
"""Parent class for constraints on allowable values for a Property.
|
||||||
|
|
||||||
|
@ -293,7 +296,7 @@ class Constraint(collections.Mapping):
|
||||||
yield self.description
|
yield self.description
|
||||||
yield self._str()
|
yield self._str()
|
||||||
|
|
||||||
return '\n'.join(desc())
|
return u'\n'.join(desc())
|
||||||
|
|
||||||
def validate(self, value, schema=None, context=None):
|
def validate(self, value, schema=None, context=None):
|
||||||
if not self._is_valid(value, schema, context):
|
if not self._is_valid(value, schema, context):
|
||||||
|
@ -369,9 +372,10 @@ class Range(Constraint):
|
||||||
return fmt % self._constraint()
|
return fmt % self._constraint()
|
||||||
|
|
||||||
def _err_msg(self, value):
|
def _err_msg(self, value):
|
||||||
return '%s is out of range (min: %s, max: %s)' % (value,
|
return _('%(value)s is out of range '
|
||||||
self.min,
|
'(min: %(min)s, max: %(max)s)') % {'value': value,
|
||||||
self.max)
|
'min': self.min,
|
||||||
|
'max': self.max}
|
||||||
|
|
||||||
def _is_valid(self, value, schema, context):
|
def _is_valid(self, value, schema, context):
|
||||||
value = Schema.str_to_num(value)
|
value = Schema.str_to_num(value)
|
||||||
|
@ -432,9 +436,10 @@ class Length(Range):
|
||||||
return fmt % self._constraint()
|
return fmt % self._constraint()
|
||||||
|
|
||||||
def _err_msg(self, value):
|
def _err_msg(self, value):
|
||||||
return 'length (%d) is out of range (min: %s, max: %s)' % (len(value),
|
return _('length (%(length)d) is out of range '
|
||||||
self.min,
|
'(min: %(min)s, max: %(max)s)') % {'length': len(value),
|
||||||
self.max)
|
'min': self.min,
|
||||||
|
'max': self.max}
|
||||||
|
|
||||||
def _is_valid(self, value, schema, context):
|
def _is_valid(self, value, schema, context):
|
||||||
return super(Length, self)._is_valid(len(value), schema, context)
|
return super(Length, self)._is_valid(len(value), schema, context)
|
||||||
|
@ -498,8 +503,10 @@ class Modulo(Constraint):
|
||||||
return fmt % self._constraint()
|
return fmt % self._constraint()
|
||||||
|
|
||||||
def _err_msg(self, value):
|
def _err_msg(self, value):
|
||||||
return '%s is not a multiple of %s with an offset of %s)' % (
|
return _('%(value)s is not a multiple of %(step)s '
|
||||||
value, self.step, self.offset)
|
'with an offset of %(offset)s') % {'value': value,
|
||||||
|
'step': self.step,
|
||||||
|
'offset': self.offset}
|
||||||
|
|
||||||
def _is_valid(self, value, schema, context):
|
def _is_valid(self, value, schema, context):
|
||||||
value = Schema.str_to_num(value)
|
value = Schema.str_to_num(value)
|
||||||
|
@ -542,12 +549,14 @@ class AllowedValues(Constraint):
|
||||||
self.allowed = tuple(allowed)
|
self.allowed = tuple(allowed)
|
||||||
|
|
||||||
def _str(self):
|
def _str(self):
|
||||||
allowed = ', '.join(str(a) for a in self.allowed)
|
allowed = ', '.join(json.dumps(a) for a in self.allowed)
|
||||||
return _('Allowed values: %s') % allowed
|
return _('Allowed values: %s') % allowed
|
||||||
|
|
||||||
def _err_msg(self, value):
|
def _err_msg(self, value):
|
||||||
allowed = '[%s]' % ', '.join(str(a) for a in self.allowed)
|
allowed = '[%s]' % ', '.join(json.dumps(a) for a in self.allowed)
|
||||||
return '"%s" is not an allowed value %s' % (value, allowed)
|
return _('%(value)s is not an allowed value '
|
||||||
|
'%(allowed)s') % {'value': json.dumps(value),
|
||||||
|
'allowed': allowed}
|
||||||
|
|
||||||
def _is_valid(self, value, schema, context):
|
def _is_valid(self, value, schema, context):
|
||||||
# For list values, check if all elements of the list are contained
|
# For list values, check if all elements of the list are contained
|
||||||
|
@ -590,7 +599,8 @@ class AllowedPattern(Constraint):
|
||||||
return _('Value must match pattern: %s') % self.pattern
|
return _('Value must match pattern: %s') % self.pattern
|
||||||
|
|
||||||
def _err_msg(self, value):
|
def _err_msg(self, value):
|
||||||
return '"%s" does not match pattern "%s"' % (value, self.pattern)
|
return _('"%(value)s" does not match pattern '
|
||||||
|
'"%(pattern)s"') % {'value': value, 'pattern': self.pattern}
|
||||||
|
|
||||||
def _is_valid(self, value, schema, context):
|
def _is_valid(self, value, schema, context):
|
||||||
match = self.match(value)
|
match = self.match(value)
|
||||||
|
@ -691,7 +701,7 @@ class BaseCustomConstraint(object):
|
||||||
try:
|
try:
|
||||||
self.validate_with_client(context.clients, value_to_validate)
|
self.validate_with_client(context.clients, value_to_validate)
|
||||||
except self.expected_exceptions as e:
|
except self.expected_exceptions as e:
|
||||||
self._error_message = str(e)
|
self._error_message = six.text_type(e)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -134,7 +134,7 @@ class CompositeAlarmTest(common.HeatTestCase):
|
||||||
props = test_stack.t['resources']['cps_alarm']['Properties']
|
props = test_stack.t['resources']['cps_alarm']['Properties']
|
||||||
props['composite_rule']['operator'] = 'invalid'
|
props['composite_rule']['operator'] = 'invalid'
|
||||||
res = test_stack['cps_alarm']
|
res = test_stack['cps_alarm']
|
||||||
error_msg = '"invalid" is not an allowed value [or, and]'
|
error_msg = '"invalid" is not an allowed value'
|
||||||
|
|
||||||
exc = self.assertRaises(exception.StackValidationFailed,
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
res.validate)
|
res.validate)
|
||||||
|
|
|
@ -177,10 +177,8 @@ class GlanceImageTest(common.HeatTestCase):
|
||||||
props['disk_format'] = 'incorrect_format'
|
props['disk_format'] = 'incorrect_format'
|
||||||
image.t = image.t.freeze(properties=props)
|
image.t = image.t.freeze(properties=props)
|
||||||
image.reparse()
|
image.reparse()
|
||||||
error_msg = ('Property error: '
|
error_msg = ('resources.image.properties.disk_format: '
|
||||||
'resources.image.properties.disk_format: '
|
'"incorrect_format" is not an allowed value')
|
||||||
'"incorrect_format" is not an allowed value '
|
|
||||||
'[ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, iso]')
|
|
||||||
self._test_validate(image, error_msg)
|
self._test_validate(image, error_msg)
|
||||||
|
|
||||||
def test_miss_container_format(self):
|
def test_miss_container_format(self):
|
||||||
|
@ -210,10 +208,8 @@ class GlanceImageTest(common.HeatTestCase):
|
||||||
props['container_format'] = 'incorrect_format'
|
props['container_format'] = 'incorrect_format'
|
||||||
image.t = image.t.freeze(properties=props)
|
image.t = image.t.freeze(properties=props)
|
||||||
image.reparse()
|
image.reparse()
|
||||||
error_msg = ('Property error: '
|
error_msg = ('resources.image.properties.container_format: '
|
||||||
'resources.image.properties.container_format: '
|
'"incorrect_format" is not an allowed value')
|
||||||
'"incorrect_format" is not an allowed value '
|
|
||||||
'[ami, ari, aki, bare, ova, ovf]')
|
|
||||||
self._test_validate(image, error_msg)
|
self._test_validate(image, error_msg)
|
||||||
|
|
||||||
def test_miss_location(self):
|
def test_miss_location(self):
|
||||||
|
@ -549,8 +545,7 @@ class GlanceWebImageTest(common.HeatTestCase):
|
||||||
image.reparse()
|
image.reparse()
|
||||||
error_msg = ('Property error: '
|
error_msg = ('Property error: '
|
||||||
'resources.image.properties.disk_format: '
|
'resources.image.properties.disk_format: '
|
||||||
'"incorrect_format" is not an allowed value '
|
'"incorrect_format" is not an allowed value')
|
||||||
'[ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, iso]')
|
|
||||||
self._test_validate(image, error_msg)
|
self._test_validate(image, error_msg)
|
||||||
|
|
||||||
def test_miss_container_format(self):
|
def test_miss_container_format(self):
|
||||||
|
@ -582,8 +577,7 @@ class GlanceWebImageTest(common.HeatTestCase):
|
||||||
image.reparse()
|
image.reparse()
|
||||||
error_msg = ('Property error: '
|
error_msg = ('Property error: '
|
||||||
'resources.image.properties.container_format: '
|
'resources.image.properties.container_format: '
|
||||||
'"incorrect_format" is not an allowed value '
|
'"incorrect_format" is not an allowed value')
|
||||||
'[ami, ari, aki, bare, ova, ovf]')
|
|
||||||
self._test_validate(image, error_msg)
|
self._test_validate(image, error_msg)
|
||||||
|
|
||||||
def test_miss_location(self):
|
def test_miss_location(self):
|
||||||
|
|
|
@ -229,7 +229,7 @@ class ManilaShareTest(common.HeatTestCase):
|
||||||
stack = utils.parse_stack(tmp, stack_name='access_type')
|
stack = utils.parse_stack(tmp, stack_name='access_type')
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
exception.StackValidationFailed,
|
exception.StackValidationFailed,
|
||||||
r'.* "domain" is not an allowed value \[ip, user, cert, cephx\]',
|
'.* "domain" is not an allowed value',
|
||||||
stack.validate)
|
stack.validate)
|
||||||
|
|
||||||
def test_get_live_state(self):
|
def test_get_live_state(self):
|
||||||
|
|
|
@ -73,14 +73,12 @@ class RBACPolicyTest(common.HeatTestCase):
|
||||||
|
|
||||||
def test_validate_action_for_network(self):
|
def test_validate_action_for_network(self):
|
||||||
msg = ('Property error: resources.rbac.properties.action: '
|
msg = ('Property error: resources.rbac.properties.action: '
|
||||||
'"invalid" is not an allowed value '
|
'"invalid" is not an allowed value')
|
||||||
r'\[access_as_shared, access_as_external\]')
|
|
||||||
self._test_validate_invalid_action(msg)
|
self._test_validate_invalid_action(msg)
|
||||||
|
|
||||||
def test_validate_action_for_qos_policy(self):
|
def test_validate_action_for_qos_policy(self):
|
||||||
msg = ('Property error: resources.rbac.properties.action: '
|
msg = ('Property error: resources.rbac.properties.action: '
|
||||||
'"invalid" is not an allowed value '
|
'"invalid" is not an allowed value')
|
||||||
r'\[access_as_shared, access_as_external\]')
|
|
||||||
self._test_validate_invalid_action(msg, obj_type='qos_policy')
|
self._test_validate_invalid_action(msg, obj_type='qos_policy')
|
||||||
# we dont support access_as_external for qos_policy
|
# we dont support access_as_external for qos_policy
|
||||||
msg = ('Property error: resources.rbac.properties.action: '
|
msg = ('Property error: resources.rbac.properties.action: '
|
||||||
|
|
|
@ -366,7 +366,7 @@ class SchemaTest(common.HeatTestCase):
|
||||||
self.assertIsNone(schema.validate_constraints(1))
|
self.assertIsNone(schema.validate_constraints(1))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, 3)
|
schema.validate_constraints, 3)
|
||||||
self.assertEqual('"3" is not an allowed value [1, 2, 4]',
|
self.assertEqual('3 is not an allowed value [1, 2, 4]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
self.assertIsNone(schema.validate_constraints('1'))
|
self.assertIsNone(schema.validate_constraints('1'))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
|
@ -383,12 +383,12 @@ class SchemaTest(common.HeatTestCase):
|
||||||
self.assertIsNone(schema.validate_constraints(1))
|
self.assertIsNone(schema.validate_constraints(1))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, 3)
|
schema.validate_constraints, 3)
|
||||||
self.assertEqual('"3" is not an allowed value [1, 2, 4]',
|
self.assertEqual('3 is not an allowed value ["1", "2", "4"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
self.assertIsNone(schema.validate_constraints('1'))
|
self.assertIsNone(schema.validate_constraints('1'))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, '3')
|
schema.validate_constraints, '3')
|
||||||
self.assertEqual('"3" is not an allowed value [1, 2, 4]',
|
self.assertEqual('"3" is not an allowed value ["1", "2", "4"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
|
|
||||||
def test_allowed_values_numeric_float(self):
|
def test_allowed_values_numeric_float(self):
|
||||||
|
@ -408,7 +408,7 @@ class SchemaTest(common.HeatTestCase):
|
||||||
self.assertIsNone(schema.validate_constraints(1.1))
|
self.assertIsNone(schema.validate_constraints(1.1))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, 3.3)
|
schema.validate_constraints, 3.3)
|
||||||
self.assertEqual('"3.3" is not an allowed value [1.1, 2.2, 4.4]',
|
self.assertEqual('3.3 is not an allowed value [1.1, 2.2, 4.4]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
self.assertIsNone(schema.validate_constraints('1.1'))
|
self.assertIsNone(schema.validate_constraints('1.1'))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
|
@ -425,12 +425,12 @@ class SchemaTest(common.HeatTestCase):
|
||||||
self.assertIsNone(schema.validate_constraints(1.1))
|
self.assertIsNone(schema.validate_constraints(1.1))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, 3.3)
|
schema.validate_constraints, 3.3)
|
||||||
self.assertEqual('"3.3" is not an allowed value [1.1, 2.2, 4.4]',
|
self.assertEqual('3.3 is not an allowed value ["1.1", "2.2", "4.4"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
self.assertIsNone(schema.validate_constraints('1.1'))
|
self.assertIsNone(schema.validate_constraints('1.1'))
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
schema.validate_constraints, '3.3')
|
schema.validate_constraints, '3.3')
|
||||||
self.assertEqual('"3.3" is not an allowed value [1.1, 2.2, 4.4]',
|
self.assertEqual('"3.3" is not an allowed value ["1.1", "2.2", "4.4"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
|
|
||||||
def test_to_schema_type_int(self):
|
def test_to_schema_type_int(self):
|
||||||
|
|
|
@ -1757,7 +1757,7 @@ class ValidateTest(common.HeatTestCase):
|
||||||
stack = parser.Stack(self.ctx, 'test_stack', template)
|
stack = parser.Stack(self.ctx, 'test_stack', template)
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
stack.validate)
|
stack.validate)
|
||||||
self.assertIn('"3" is not an allowed value [1, 4, 8]',
|
self.assertIn('3 is not an allowed value [1, 4, 8]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
|
|
||||||
def test_validate_not_allowed_values_integer_str(self):
|
def test_validate_not_allowed_values_integer_str(self):
|
||||||
|
@ -1769,7 +1769,7 @@ class ValidateTest(common.HeatTestCase):
|
||||||
stack = parser.Stack(self.ctx, 'test_stack', template)
|
stack = parser.Stack(self.ctx, 'test_stack', template)
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
stack.validate)
|
stack.validate)
|
||||||
self.assertIn('"3" is not an allowed value [1, 4, 8]',
|
self.assertIn('"3" is not an allowed value ["1", "4", "8"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
|
|
||||||
# test with size parameter provided as number
|
# test with size parameter provided as number
|
||||||
|
@ -1777,7 +1777,7 @@ class ValidateTest(common.HeatTestCase):
|
||||||
stack = parser.Stack(self.ctx, 'test_stack', template)
|
stack = parser.Stack(self.ctx, 'test_stack', template)
|
||||||
err = self.assertRaises(exception.StackValidationFailed,
|
err = self.assertRaises(exception.StackValidationFailed,
|
||||||
stack.validate)
|
stack.validate)
|
||||||
self.assertIn('"3" is not an allowed value [1, 4, 8]',
|
self.assertIn('3 is not an allowed value ["1", "4", "8"]',
|
||||||
six.text_type(err))
|
six.text_type(err))
|
||||||
|
|
||||||
def test_validate_invalid_outputs(self):
|
def test_validate_invalid_outputs(self):
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Non-ASCII text that appears in parameter constraints (e.g. in the
|
||||||
|
description of a constraint, or a list of allowed values) will now be
|
||||||
|
handled correctly when generating error messages if the constraint is not
|
||||||
|
met.
|
Loading…
Reference in New Issue