Added component config schema definition framework
This commit is contained in:
parent
13641572d8
commit
b33ceec53f
220
ostack_validator/schema.py
Normal file
220
ostack_validator/schema.py
Normal file
@ -0,0 +1,220 @@
|
||||
from ostack_validator.common import Inspection, MarkedError, Mark, find, index
|
||||
|
||||
class Version:
|
||||
def __init__(self, major, minor=0, maintenance=0):
|
||||
"Create Version object by either passing 3 integers, one string or an another Version object"
|
||||
if isinstance(major, str):
|
||||
self.parts = [int(x) for x in major.split('.', 3)]
|
||||
elif isinstance(major, Version):
|
||||
self.parts = major.parts
|
||||
else:
|
||||
self.parts = [int(major), int(minor), int(maintenance)]
|
||||
|
||||
def __str__(self):
|
||||
return '.'.join([str(p) for p in self.parts])
|
||||
|
||||
def __repr__(self):
|
||||
return '<Version %s>' % str(self)
|
||||
|
||||
def __cmp__(self, other):
|
||||
for i in xrange(0, 3):
|
||||
x = self.parts[i] - other.parts[i]
|
||||
if x != 0:
|
||||
return -1 if x < 0 else 1
|
||||
|
||||
return 0
|
||||
|
||||
class SchemaUpdateRecord(object):
|
||||
# checkpoint's data is version number
|
||||
def __init__(self, version, operation, data=None):
|
||||
super(SchemaUpdateRecord, self).__init__()
|
||||
if not operation in ['checkpoint', 'add', 'remove']:
|
||||
raise Error, 'Unknown operation "%s"' % operation
|
||||
version = Version(version)
|
||||
self.version = version
|
||||
self.operation = operation
|
||||
self.data = data
|
||||
|
||||
class SchemaBuilder(object):
|
||||
def __init__(self, data):
|
||||
super(SchemaBuilder, self).__init__()
|
||||
self.data = data
|
||||
|
||||
self.current_version = None
|
||||
self.current_section = None
|
||||
self.adds = []
|
||||
self.removals = []
|
||||
|
||||
def version(self, version, checkpoint=False):
|
||||
version = Version(version)
|
||||
|
||||
if self.current_version and self.current_version != version:
|
||||
self.commit()
|
||||
|
||||
if checkpoint or self.data == []:
|
||||
self.data.append(SchemaUpdateRecord(version, 'checkpoint'))
|
||||
|
||||
self.current_version = version
|
||||
|
||||
def section(self, name):
|
||||
self.current_section = name
|
||||
|
||||
def param(self, *args, **kwargs):
|
||||
if not 'section' in kwargs and self.current_section:
|
||||
kwargs['section'] = self.current_section
|
||||
|
||||
self.adds.append(ConfigParameterSchema(*args, **kwargs))
|
||||
|
||||
def remove_param(self, name):
|
||||
self.removals.append(name)
|
||||
|
||||
def commit(self):
|
||||
"Finalize schema building"
|
||||
if len(self.removals) > 0:
|
||||
self.data.append(SchemaUpdateRecord(self.current_version, 'remove', self.removals))
|
||||
self.removals = []
|
||||
if len(self.adds) > 0:
|
||||
self.data.append(SchemaUpdateRecord(self.current_version, 'add', self.adds))
|
||||
self.adds = []
|
||||
|
||||
class ConfigSchemaRegistry:
|
||||
__schemas = {}
|
||||
@classmethod
|
||||
def register_schema(self, project, configname=None):
|
||||
if not configname:
|
||||
configname = '%s.conf' % project
|
||||
fullname = '%s/%s' % (project, configname)
|
||||
self.__schemas[fullname] = []
|
||||
return SchemaBuilder(self.__schemas[fullname])
|
||||
|
||||
@classmethod
|
||||
def get_schema(self, project, version, configname=None):
|
||||
if not configname:
|
||||
configname = '%s.conf' % project
|
||||
fullname = '%s/%s' % (project, configname)
|
||||
version = Version(version)
|
||||
|
||||
records = self.__schemas[fullname]
|
||||
i = len(records)-1
|
||||
# Find latest checkpoint prior given version
|
||||
while i>=0 and not (records[i].operation == 'checkpoint' and records[i].version <= version): i-=1
|
||||
|
||||
if i < 0:
|
||||
return None
|
||||
|
||||
parameters = []
|
||||
seen_parameters = set()
|
||||
last_version = None
|
||||
|
||||
while i < len(records) and records[i].version <= version:
|
||||
last_version = records[i].version
|
||||
if records[i].operation == 'add':
|
||||
for param in records[i].data:
|
||||
if param.name in seen_parameters:
|
||||
old_param_index = index(parameters, lambda p: p.name == param.name)
|
||||
if old_param_index != -1:
|
||||
parameters[old_param_index] = param
|
||||
else:
|
||||
parameters.append(param)
|
||||
seen_parameters.add(param.name)
|
||||
elif records[i].operation == 'remove':
|
||||
for param_name in records[i].data:
|
||||
param_index = index(parameters, lambda p: p.name == param_name)
|
||||
if index != -1:
|
||||
parameters.pop(param_index)
|
||||
seen_parameters.remove(param_name)
|
||||
i += 1
|
||||
|
||||
return ConfigSchema(fullname, last_version, 'ini', parameters)
|
||||
|
||||
|
||||
class ConfigSchema:
|
||||
def __init__(self, name, version, format, parameters):
|
||||
self.name = name
|
||||
self.version = Version(version)
|
||||
self.format = format
|
||||
self.parameters = parameters
|
||||
|
||||
def get_parameter(name, section=None):
|
||||
# TODO: optimize this
|
||||
return find(self.parameters, lambda p: p.name == name and p.section == section)
|
||||
|
||||
def __repr__(self):
|
||||
return '<ConfigSchema name=%s version=%s format=%s parameters=%s>' % (self.name, self.version, self.format, self.parameters)
|
||||
|
||||
class ConfigParameterSchema:
|
||||
def __init__(self, name, type, section=None, description=None, default=None, required=False):
|
||||
self.section = section
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.description = description
|
||||
self.default = default
|
||||
self.required = required
|
||||
|
||||
def __repr__(self):
|
||||
return '<ConfigParameterSchema %s>' % ' '.join(['%s=%s' % (attr, getattr(self, attr)) for attr in ['section', 'name', 'type', 'description', 'default', 'required']])
|
||||
|
||||
|
||||
class TypeValidatorRegistry:
|
||||
__validators = {}
|
||||
@classmethod
|
||||
def register_validator(self, type_name, type_validator):
|
||||
self.__validators[type_name] = type_validator
|
||||
|
||||
@classmethod
|
||||
def get_validator(self, name):
|
||||
return self.__validators[name]
|
||||
|
||||
|
||||
class InvalidValueError(MarkedError):
|
||||
def __init__(self, message, mark=Mark('', 1, 1)):
|
||||
super(InvalidValueError, self).__init__(message, mark)
|
||||
|
||||
class TypeValidator(object):
|
||||
def __init__(self, f):
|
||||
super(TypeValidator, self).__init__()
|
||||
self.f = f
|
||||
|
||||
def validate(self, value):
|
||||
return getattr(self, 'f')(value)
|
||||
|
||||
|
||||
def type_validator(name, **kwargs):
|
||||
def wrap(fn):
|
||||
def wrapped(s):
|
||||
return fn(s, **kwargs)
|
||||
o = TypeValidator(wrapped)
|
||||
TypeValidatorRegistry.register_validator(name, o)
|
||||
return fn
|
||||
return wrap
|
||||
|
||||
@type_validator('boolean', values=['True', 'False'])
|
||||
def validate_enum(s, values=[]):
|
||||
if s in values:
|
||||
return None
|
||||
return InvalidValueError('Invalid value: valid values are: %s' % ', '.join(values))
|
||||
|
||||
@type_validator('string')
|
||||
def validate_string(s):
|
||||
return None
|
||||
|
||||
@type_validator('integer')
|
||||
@type_validator('port', min=1, max=65535)
|
||||
def validate_integer(s, min=None, max=None):
|
||||
leading_whitespace_len = 0
|
||||
while s[leading_whitespace_len].isspace(): leading_whitespace_len += 1
|
||||
|
||||
s = s.strip()
|
||||
for i, c in enumerate(s):
|
||||
if not c.isdigit() and not ((c == '-') and (i == 0)):
|
||||
return InvalidValueError('Invalid value: only digits are allowed, but found char "%s"' % c, Mark('', 1, i+1+leading_whitespace_len))
|
||||
|
||||
v = int(s)
|
||||
if min and v < min:
|
||||
return InvalidValueError('Invalid value: should be greater than or equal to %d' % min, Mark('', 1, leading_whitespace_len))
|
||||
if max and v > max:
|
||||
return InvalidValueError('Invalid value: should be less than or equal to %d' % max, Mark('', 1, leading_whitespace_len))
|
||||
|
||||
return None
|
||||
|
||||
|
48
ostack_validator/test_config_schema_registry.py
Normal file
48
ostack_validator/test_config_schema_registry.py
Normal file
@ -0,0 +1,48 @@
|
||||
from ostack_validator.schema import ConfigSchemaRegistry, Version
|
||||
from ostack_validator.common import find
|
||||
|
||||
import unittest
|
||||
|
||||
class ConfigSchemaRegistryTests(unittest.TestCase):
|
||||
def test_sample(self):
|
||||
nova = ConfigSchemaRegistry.register_schema(project='nova')
|
||||
|
||||
nova.version('1.0.0')
|
||||
nova.section('DEFAULT')
|
||||
nova.param(name='verbose', type='boolean')
|
||||
nova.param(name='rabbit_host', type='address')
|
||||
|
||||
nova.version('1.1.0')
|
||||
nova.section('DEFAULT')
|
||||
nova.param(name='verbose', type='boolean', default=False)
|
||||
nova.remove_param('rabbit_host')
|
||||
|
||||
nova.commit()
|
||||
|
||||
schema10 = ConfigSchemaRegistry.get_schema(project='nova', version='1.0.0')
|
||||
|
||||
self.assertEqual(Version('1.0.0'), schema10.version)
|
||||
self.assertEqual('ini', schema10.format)
|
||||
|
||||
verbose_param = find(schema10.parameters, lambda p: p.name == 'verbose')
|
||||
self.assertIsNotNone(verbose_param)
|
||||
self.assertEqual('boolean', verbose_param.type)
|
||||
self.assertEqual(None, verbose_param.default)
|
||||
|
||||
rabbit_host_param = find(schema10.parameters, lambda p: p.name == 'rabbit_host')
|
||||
self.assertIsNotNone(rabbit_host_param)
|
||||
self.assertEqual('address', rabbit_host_param.type)
|
||||
|
||||
schema11 = ConfigSchemaRegistry.get_schema(project='nova', version='1.1.0')
|
||||
|
||||
verbose_param11 = find(schema11.parameters, lambda p: p.name == 'verbose')
|
||||
self.assertIsNotNone(verbose_param11)
|
||||
self.assertEqual(False, verbose_param11.default)
|
||||
|
||||
rabbit_host_param11 = find(schema11.parameters, lambda p: p.name == 'rabbit_host')
|
||||
self.assertIsNone(rabbit_host_param11)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
100
ostack_validator/test_type_validators.py
Normal file
100
ostack_validator/test_type_validators.py
Normal file
@ -0,0 +1,100 @@
|
||||
from ostack_validator.common import Error, MarkedError
|
||||
from ostack_validator.schema import TypeValidatorRegistry
|
||||
|
||||
import unittest
|
||||
|
||||
class TypeValidatorTestHelper(object):
|
||||
def setUp(self):
|
||||
super(TypeValidatorTestHelper, self).setUp()
|
||||
self.validator = TypeValidatorRegistry.get_validator(self.type_name)
|
||||
|
||||
def assertValid(self, value):
|
||||
self.assertIsNone(self.validator.validate(value))
|
||||
|
||||
def assertInvalid(self, value):
|
||||
self.assertIsInstance(self.validator.validate(value), Error)
|
||||
|
||||
class StringTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
|
||||
type_name = 'string'
|
||||
|
||||
def test_empty_string_passes(self):
|
||||
self.assertValid('')
|
||||
|
||||
def test_validation_always_passes(self):
|
||||
self.assertValid('foo bar')
|
||||
|
||||
class BooleanTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
|
||||
type_name = 'boolean'
|
||||
|
||||
def test_True(self):
|
||||
self.assertValid('True')
|
||||
|
||||
def test_False(self):
|
||||
self.assertValid('False')
|
||||
|
||||
def test_other_values_produce_error(self):
|
||||
self.assertInvalid('foo')
|
||||
|
||||
class IntegerTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
|
||||
type_name = 'integer'
|
||||
|
||||
def test_positive_values_are_valid(self):
|
||||
self.assertValid('123')
|
||||
|
||||
def test_zero_is_valid(self):
|
||||
self.assertValid('0')
|
||||
|
||||
def test_negative_values_are_valid(self):
|
||||
self.assertValid('-123')
|
||||
|
||||
def test_leading_whitespace_is_ignored(self):
|
||||
self.assertValid(' 5')
|
||||
|
||||
def test_trailing_whitespace_is_ignored(self):
|
||||
self.assertValid('7 ')
|
||||
|
||||
def test_non_digits_are_invalid(self):
|
||||
self.assertInvalid('12a45')
|
||||
|
||||
def test_invalid_char_error_contains_proper_column_in_mark(self):
|
||||
error = self.validator.validate('12a45')
|
||||
self.assertIsInstance(error, MarkedError)
|
||||
self.assertEqual(3, error.mark.column)
|
||||
|
||||
def test_invalid_char_error_contains_proper_column_if_leading_whitespaces(self):
|
||||
error = self.validator.validate(' 12a45')
|
||||
self.assertIsInstance(error, MarkedError)
|
||||
self.assertEqual(5, error.mark.column)
|
||||
|
||||
class PortTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
|
||||
type_name = 'port'
|
||||
|
||||
def test_positive_integer(self):
|
||||
self.assertValid('123')
|
||||
|
||||
def test_zero_invalid(self):
|
||||
self.assertInvalid('0')
|
||||
|
||||
def test_negatives_are_invalid(self):
|
||||
self.assertInvalid('-1')
|
||||
|
||||
def test_values_greater_than_65535_are_invalid(self):
|
||||
self.assertInvalid('65536')
|
||||
|
||||
def test_low_boundary_is_valid(self):
|
||||
self.assertValid('1')
|
||||
|
||||
def test_high_boundary_is_valid(self):
|
||||
self.assertValid('65535')
|
||||
|
||||
def test_non_digits_are_invalid(self):
|
||||
self.assertInvalid('12a5')
|
||||
|
||||
def test_leading_and_or_trailing_whitespace_is_ignored(self):
|
||||
self.assertValid(' 123')
|
||||
self.assertValid('456 ')
|
||||
self.assertValid(' 123 ')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user