Merge pull request #25131 from s0undt3ch/features/raas-17-salt-cloud-2015.8

Array support in salt.utils.config
This commit is contained in:
Thomas S Hatch 2015-07-02 13:40:34 -06:00
commit 3922992e6b
2 changed files with 437 additions and 20 deletions

View File

@ -497,6 +497,15 @@ class BaseConfigItemMeta(six.with_metaclass(Prepareable, type)):
instance._attributes.append(key)
# Init the class
instance.__init__(*args, **kwargs)
# Validate the instance after initialization
for base in reversed(inspect.getmro(cls)):
validate_attributes = getattr(base, '__validate_attributes__', None)
if validate_attributes:
if instance.__validate_attributes__.__func__ is not base.__validate_attributes__.__func__:
# The method was overridden, run base.__validate_attributes__ function
base.__validate_attributes__(instance)
# Finally, run the instance __validate_attributes__ function
instance.__validate_attributes__()
# Return the initialized class
return instance
@ -595,19 +604,42 @@ class BaseItem(six.with_metaclass(BaseConfigItemMeta, object)):
__serialize_attr_aliases__ = None
def __init__(self, required=False, **extra):
required = False
def __init__(self, required=None, **extra):
'''
:param required: If the configuration item is required. Defaults to ``False``.
'''
self.required = required
if required is not None:
self.required = required
self.extra = extra
def __validate_attributes__(self):
'''
Run any validation check you need the instance attributes.
ATTENTION:
Don't call the parent class when overriding this
method because it will just duplicate the executions. This class'es
metaclass will take care of that.
'''
if self.required not in (True, False):
raise RuntimeError(
'\'required\' can only be True/False'
)
def _get_argname_value(self, argname):
'''
Return the argname value looking up on all possible attributes
'''
# Let's see if the value is defined as a public class variable
argvalue = getattr(self, argname, None)
# Let's see if there's a private fuction to get the value
argvalue = getattr(self, '__get_{0}__'.format(argname), None)
if argvalue is not None and callable(argvalue):
argvalue = argvalue()
if argvalue is None:
# Let's see if the value is defined as a public class variable
argvalue = getattr(self, argname, None)
if argvalue is None:
# Let's see if it's defined as a private class variable
argvalue = getattr(self, '__{0}__'.format(argname), None)
@ -627,6 +659,8 @@ class BaseItem(six.with_metaclass(BaseConfigItemMeta, object)):
continue
argvalue = self._get_argname_value(argname)
if argvalue is not None:
if argvalue is Null:
argvalue = None
# None values are not meant to be included in the
# serialization, since this is not None...
if self.__serialize_attr_aliases__ and argname in self.__serialize_attr_aliases__:
@ -642,6 +676,20 @@ class BaseConfigItem(BaseItem):
All configurations must subclass it
'''
# Let's define description as a class attribute, this will allow a custom configuration
# item to do something like:
# class MyCustomConfig(StringConfig):
# '''
# This is my custom config, blah, blah, blah
# '''
# description = __doc__
#
description = None
# The same for all other base arguments
title = None
default = None
enum = None
def __init__(self, title=None, description=None, default=None, enum=None, **kwargs):
'''
:param required:
@ -656,18 +704,25 @@ class BaseConfigItem(BaseItem):
:param enum:
A list(list, tuple, set) of valid choices.
'''
self.title = title
self.description = description or self.__doc__
self.default = default
if title is not None:
self.title = title
if description is not None:
self.description = description
if default is not None:
self.default = default
if enum is not None:
if not isinstance(enum, (list, tuple, set)):
self.enum = enum
super(BaseConfigItem, self).__init__(**kwargs)
def __validate_attributes__(self):
if self.enum is not None:
if not isinstance(self.enum, (list, tuple, set)):
raise RuntimeError(
'Only the \'list\', \'tuple\' and \'set\' python types can be used '
'to define \'enum\''
)
enum = list(enum)
self.enum = enum
super(BaseConfigItem, self).__init__(**kwargs)
if not isinstance(self.enum, list):
self.enum = list(self.enum)
def render_as_rst(self, name):
'''
@ -717,6 +772,11 @@ class StringConfig(BaseConfigItem):
'max_length': 'maxLength'
}
format = None
pattern = None
min_length = None
max_length = None
def __init__(self,
format=None, # pylint: disable=redefined-builtin
pattern=None,
@ -744,12 +804,20 @@ class StringConfig(BaseConfigItem):
:param max_length:
The maximum length
'''
self.format = format or self.__format__
self.pattern = pattern
self.min_length = min_length
self.max_length = max_length
if format is not None: # pylint: disable=redefined-builtin
self.format = format
if pattern is not None:
self.pattern = pattern
if min_length is not None:
self.min_length = min_length
if max_length is not None:
self.max_length = max_length
super(StringConfig, self).__init__(**kwargs)
def __validate_attributes__(self):
if self.format is None and self.__format__ is not None:
self.format = self.__format__
class EMailConfig(StringConfig):
'''
@ -823,6 +891,12 @@ class NumberConfig(BaseConfigItem):
'exclusive_maximum': 'exclusiveMaximum',
}
multiple_of = None
minimum = None
exclusive_minimum = None
maximum = None
exclusive_maximum = None
def __init__(self,
multiple_of=None,
minimum=None,
@ -853,13 +927,111 @@ class NumberConfig(BaseConfigItem):
:param exclusive_maximum:
Wether a value is allowed to be exactly equal to the maximum
'''
self.multiple_of = multiple_of
self.minimum = minimum
self.exclusive_minimum = exclusive_minimum
self.maximum = maximum
self.exclusive_maximum = exclusive_maximum
if multiple_of is not None:
self.multiple_of = multiple_of
if minimum is not None:
self.minimum = minimum
if exclusive_minimum is not None:
self.exclusive_minimum = exclusive_minimum
if maximum is not None:
self.maximum = maximum
if exclusive_maximum is not None:
self.exclusive_maximum = exclusive_maximum
super(NumberConfig, self).__init__(**kwargs)
class IntegerConfig(NumberConfig):
__type__ = 'integer'
class ArrayConfig(BaseConfigItem):
__type__ = 'array'
__serialize_attr_aliases__ = {
'min_items': 'minItems',
'max_items': 'maxItems',
'unique_items': 'uniqueItems',
'additional_items': 'additionalItems'
}
items = None
min_items = None
max_items = None
unique_items = None
additional_items = None
def __init__(self,
items=None,
min_items=None,
max_items=None,
unique_items=None,
additional_items=None,
**kwargs):
'''
:param required:
If the configuration item is required. Defaults to ``False``.
:param title:
A short explanation about the purpose of the data described by this item.
:param description:
A detailed explanation about the purpose of the data described by this item.
:param default:
The default value for this configuration item. May be :data:`.Null` (a special value
to set the default value to null).
:param enum:
A list(list, tuple, set) of valid choices.
:param items:
Either of the following:
* :class:`BaseConfigItem` -- all items of the array must match the field schema;
* a list or a tuple of :class:`fields <.BaseConfigItem>` -- all items of the array must be
valid according to the field schema at the corresponding index (tuple typing);
:param min_items:
Minimum length of the array
:param max_items:
Maximum length of the array
:param unique_items:
Whether all the values in the array must be distinct.
:param additional_items:
If the value of ``items`` is a list or a tuple, and the array length is larger than
the number of fields in ``items``, then the additional items are described
by the :class:`.BaseField` passed using this argument.
:type additional_items: bool or :class:`.BaseConfigItem`
'''
if items is not None:
self.items = items
if min_items is not None:
self.min_items = min_items
if max_items is not None:
self.max_items = max_items
if unique_items is not None:
self.unique_items = unique_items
if additional_items is not None:
self.additional_items = additional_items
super(ArrayConfig, self).__init__(**kwargs)
def __validate_attributes__(self):
if self.items is not None:
if isinstance(self.items, (list, tuple)):
for item in self.items:
if not isinstance(item, (Configuration, BaseItem)):
raise RuntimeError(
'All items passed in the item argument tuple/list must be '
'a subclass of Configuration, BaseItem or BaseConfigItem, '
'not {0}'.format(type(item))
)
elif not isinstance(self.items, (Configuration, BaseItem)):
raise RuntimeError(
'The items argument passed must be a subclass of '
'Configuration, BaseItem or BaseConfigItem, not '
'{0}'.format(type(self.items))
)
def __get_items__(self):
if isinstance(self.items, (Configuration, BaseItem)):
# This is either a Configuration or a Basetem, return it in it's
# serialized form
return self.items.serialize()
if isinstance(self.items, (tuple, list)):
items = []
for item in self.items:
items.append(item.serialize())
return items

View File

@ -72,6 +72,18 @@ class ConfigTestCase(TestCase):
}
)
item = config.BooleanConfig(title='Hungry',
description='Are you hungry?',
default=config.Null)
self.assertDictEqual(
item.serialize(), {
'type': 'boolean',
'title': item.title,
'description': item.description,
'default': None
}
)
@skipIf(HAS_JSONSCHEMA is False, 'The \'jsonschema\' library is missing')
def test_boolean_config_validation(self):
class TestConf(config.Configuration):
@ -736,6 +748,239 @@ class ConfigTestCase(TestCase):
jsonschema.validate({'item': 3}, TestConf.serialize())
self.assertIn('is not one of', excinfo.exception.message)
def test_array_config(self):
item = config.ArrayConfig(title='Dog Names', description='Name your dogs')
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description
}
)
string_item = config.StringConfig(title='Dog Name',
description='The dog name')
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=string_item)
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description,
'items': {
'type': 'string',
'title': string_item.title,
'description': string_item.description
}
}
)
integer_item = config.IntegerConfig(title='Dog Age',
description='The dog age')
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=(string_item, integer_item))
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description,
'items': [
{
'type': 'string',
'title': string_item.title,
'description': string_item.description
},
{
'type': 'integer',
'title': integer_item.title,
'description': integer_item.description
}
]
}
)
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=(config.StringConfig(),
config.IntegerConfig()),
min_items=1,
max_items=3,
additional_items=False,
unique_items=True)
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description,
'minItems': item.min_items,
'maxItems': item.max_items,
'uniqueItems': item.unique_items,
'additionalItems': item.additional_items,
'items': [
{
'type': 'string',
},
{
'type': 'integer',
}
]
}
)
class HowManyConfig(config.Configuration):
item = config.IntegerConfig(title='How many dogs', description='Question')
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=HowManyConfig())
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description,
'items': HowManyConfig.serialize()
}
)
class AgesConfig(config.Configuration):
item = config.IntegerConfig()
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=(HowManyConfig(), AgesConfig()))
self.assertDictEqual(
item.serialize(), {
'type': 'array',
'title': item.title,
'description': item.description,
'items': [
HowManyConfig.serialize(),
AgesConfig.serialize()
]
}
)
@skipIf(HAS_JSONSCHEMA is False, 'The \'jsonschema\' library is missing')
def test_array_config_validation(self):
class TestConf(config.Configuration):
item = config.ArrayConfig(title='Dog Names', description='Name your dogs')
try:
jsonschema.validate({'item': ['Tobias', 'Óscar']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': 1}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is not of type', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=config.StringConfig())
try:
jsonschema.validate({'item': ['Tobias', 'Óscar']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': ['Tobias', 'Óscar', 3]}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is not of type', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
min_items=1,
max_items=2)
try:
jsonschema.validate({'item': ['Tobias', 'Óscar']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': ['Tobias', 'Óscar', 'Pepe']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is too long', excinfo.exception.message)
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': []}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is too short', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(title='Dog Names',
description='Name your dogs',
items=config.StringConfig(),
uniqueItems=True)
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': ['Tobias', 'Tobias']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('has non-unique elements', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(items=(config.StringConfig(),
config.IntegerConfig()))
try:
jsonschema.validate({'item': ['Óscar', 4]}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': ['Tobias', 'Óscar']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is not of type', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(
items=config.ArrayConfig(
items=(config.StringConfig(),
config.IntegerConfig())
)
)
try:
jsonschema.validate({'item': [['Tobias', 8], ['Óscar', 4]]}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': [['Tobias', 8], ['Óscar', '4']]}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is not of type', excinfo.exception.message)
class TestConf(config.Configuration):
item = config.ArrayConfig(items=config.StringConfig(enum=['Tobias', 'Óscar']))
try:
jsonschema.validate({'item': ['Óscar']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
try:
jsonschema.validate({'item': ['Tobias']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate({'item': ['Pepe']}, TestConf.serialize(),
format_checker=jsonschema.FormatChecker())
self.assertIn('is not one of', excinfo.exception.message)
if __name__ == '__main__':
from integration import run_tests