diff --git a/toscaparser/common/exception.py b/toscaparser/common/exception.py index 4f99dda2..bd0ed9cf 100644 --- a/toscaparser/common/exception.py +++ b/toscaparser/common/exception.py @@ -88,6 +88,15 @@ class InvalidTypeError(TOSCAException): msg_fmt = _('Type "%(what)s" is not a valid type.') +class InvalidTypeAdditionalRequirementsError(TOSCAException): + msg_fmt = _('Additional requirements for type "%(type)s" not met.') + + +class RangeValueError(TOSCAException): + msg_fmt = _('The value "%(pvalue)s" of property "%(pname)s" is out of ' + 'range "(min:%(vmin)s, max:%(vmax)s)".') + + class InvalidSchemaError(TOSCAException): msg_fmt = _('%(message)s') diff --git a/toscaparser/dataentity.py b/toscaparser/dataentity.py index 6e7d59e4..ebca9292 100644 --- a/toscaparser/dataentity.py +++ b/toscaparser/dataentity.py @@ -16,10 +16,10 @@ from toscaparser.common.exception import TypeMismatchError from toscaparser.common.exception import UnknownFieldError from toscaparser.elements.constraints import Schema from toscaparser.elements.datatype import DataType +from toscaparser.elements.portspectype import PortSpec from toscaparser.elements.scalarunit import ScalarUnit_Frequency from toscaparser.elements.scalarunit import ScalarUnit_Size from toscaparser.elements.scalarunit import ScalarUnit_Time - from toscaparser.utils.gettextutils import _ from toscaparser.utils import validateutils @@ -27,11 +27,13 @@ from toscaparser.utils import validateutils class DataEntity(object): '''A complex data value entity.''' - def __init__(self, datatypename, value_dict, custom_def=None): + def __init__(self, datatypename, value_dict, custom_def=None, + prop_name=None): self.custom_def = custom_def self.datatype = DataType(datatypename, custom_def) self.schema = self.datatype.get_all_properties() self.value = value_dict + self.property_name = prop_name def validate(self): '''Validate the value by the definition of the datatype.''' @@ -43,7 +45,7 @@ class DataEntity(object): self.value, None, self.custom_def) - schema = Schema(None, self.datatype.defs) + schema = Schema(self.property_name, self.datatype.defs) for constraint in schema.constraints: constraint.validate(self.value) # If the datatype has 'properties' definition @@ -110,7 +112,8 @@ class DataEntity(object): return self.schema[name].schema @staticmethod - def validate_datatype(type, value, entry_schema=None, custom_def=None): + def validate_datatype(type, value, entry_schema=None, custom_def=None, + prop_name=None): '''Validate value with given type. If type is list or map, validate its entry by entry_schema(if defined) @@ -123,7 +126,7 @@ class DataEntity(object): elif type == Schema.FLOAT: return validateutils.validate_float(value) elif type == Schema.NUMBER: - return validateutils.validate_number(value) + return validateutils.validate_numeric(value) elif type == Schema.BOOLEAN: return validateutils.validate_boolean(value) elif type == Schema.RANGE: @@ -149,6 +152,10 @@ class DataEntity(object): if entry_schema: DataEntity.validate_entry(value, entry_schema, custom_def) return value + elif type == Schema.PORTSPEC: + # TODO(TBD) bug 1567063, validate source & target as PortDef type + # as complex types not just as integers + PortSpec.validate_additional_req(value, prop_name, custom_def) else: data = DataEntity(type, value, custom_def) return data.validate() diff --git a/toscaparser/elements/capabilitytype.py b/toscaparser/elements/capabilitytype.py index 04134436..02c7fee3 100644 --- a/toscaparser/elements/capabilitytype.py +++ b/toscaparser/elements/capabilitytype.py @@ -16,6 +16,7 @@ from toscaparser.elements.statefulentitytype import StatefulEntityType class CapabilityTypeDef(StatefulEntityType): '''TOSCA built-in capabilities type.''' + TOSCA_TYPEURI_CAPABILITY_ROOT = 'tosca.capabilities.Root' def __init__(self, name, ctype, ntype, custom_def=None): self.name = name @@ -61,7 +62,7 @@ class CapabilityTypeDef(StatefulEntityType): capabilities = {} parent_cap = self.parent_type if parent_cap: - while parent_cap != 'tosca.capabilities.Root': + while parent_cap != self.TOSCA_TYPEURI_CAPABILITY_ROOT: if parent_cap in self.TOSCA_DEF.keys(): capabilities[parent_cap] = self.TOSCA_DEF[parent_cap] elif custom_def and parent_cap in custom_def.keys(): diff --git a/toscaparser/elements/constraints.py b/toscaparser/elements/constraints.py index 9883da3d..8594b851 100644 --- a/toscaparser/elements/constraints.py +++ b/toscaparser/elements/constraints.py @@ -18,6 +18,7 @@ import toscaparser from toscaparser.common.exception import ExceptionCollector from toscaparser.common.exception import InvalidSchemaError from toscaparser.common.exception import ValidationError +from toscaparser.elements.portspectype import PortSpec from toscaparser.elements import scalarunit from toscaparser.utils.gettextutils import _ @@ -36,12 +37,12 @@ class Schema(collections.Mapping): INTEGER, STRING, BOOLEAN, FLOAT, RANGE, NUMBER, TIMESTAMP, LIST, MAP, SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, - PORTDEF, VERSION + VERSION, PORTDEF, PORTSPEC ) = ( 'integer', 'string', 'boolean', 'float', 'range', 'number', 'timestamp', 'list', 'map', 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', - 'PortDef', 'version' + 'version', 'PortDef', PortSpec.SHORTNAME ) SCALAR_UNIT_SIZE_DEFAULT = 'B' @@ -127,8 +128,6 @@ class Constraint(object): 'less_or_equal', 'in_range', 'valid_values', 'length', 'min_length', 'max_length', 'pattern') - UNBOUNDED = 'UNBOUNDED' - def __new__(cls, property_name, property_type, constraint): if cls is not Constraint: return super(Constraint, cls).__new__(cls) @@ -370,6 +369,7 @@ class InRange(Constraint): Constrains a property or parameter to a value in range of (inclusive) the two values declared. """ + UNBOUNDED = 'UNBOUNDED' constraint_key = Constraint.IN_RANGE diff --git a/toscaparser/elements/datatype.py b/toscaparser/elements/datatype.py index 7e05a695..93d1b3a2 100644 --- a/toscaparser/elements/datatype.py +++ b/toscaparser/elements/datatype.py @@ -18,7 +18,8 @@ class DataType(StatefulEntityType): '''TOSCA built-in and user defined complex data type.''' def __init__(self, datatypename, custom_def=None): - super(DataType, self).__init__(datatypename, self.DATATYPE_PREFIX, + super(DataType, self).__init__(datatypename, + self.DATATYPE_NETWORK_PREFIX, custom_def) self.custom_def = custom_def diff --git a/toscaparser/elements/entity_type.py b/toscaparser/elements/entity_type.py index 5d620a5a..af2b792a 100644 --- a/toscaparser/elements/entity_type.py +++ b/toscaparser/elements/entity_type.py @@ -56,7 +56,8 @@ class EntityType(object): GROUP_PREFIX = 'tosca.groups.' # currently the data types are defined only for network # but may have changes in the future. - DATATYPE_PREFIX = 'tosca.datatypes.network.' + DATATYPE_PREFIX = 'tosca.datatypes.' + DATATYPE_NETWORK_PREFIX = DATATYPE_PREFIX + 'network.' TOSCA = 'tosca' def derived_from(self, defs): diff --git a/toscaparser/elements/portspectype.py b/toscaparser/elements/portspectype.py new file mode 100644 index 00000000..d32e97e6 --- /dev/null +++ b/toscaparser/elements/portspectype.py @@ -0,0 +1,86 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from toscaparser.common.exception import ExceptionCollector +from toscaparser.common.exception import InvalidTypeAdditionalRequirementsError +from toscaparser.utils.gettextutils import _ +import toscaparser.utils.validateutils as validateutils + +log = logging.getLogger('tosca') + + +class PortSpec(object): + '''Parent class for tosca.datatypes.network.PortSpec type.''' + + SHORTNAME = 'PortSpec' + TYPE_URI = 'tosca.datatypes.network.' + SHORTNAME + + PROPERTY_NAMES = ( + PROTOCOL, SOURCE, SOURCE_RANGE, + TARGET, TARGET_RANGE + ) = ( + 'protocol', 'source', 'source_range', + 'target', 'target_range' + ) + + # TODO(TBD) May want to make this a subclass of DataType + # and change init method to set PortSpec's properties + def __init__(self): + pass + + # The following additional requirements MUST be tested: + # 1) A valid PortSpec MUST have at least one of the following properties: + # target, target_range, source or source_range. + # 2) A valid PortSpec MUST have a value for the source property that + # is within the numeric range specified by the property source_range + # when source_range is specified. + # 3) A valid PortSpec MUST have a value for the target property that is + # within the numeric range specified by the property target_range + # when target_range is specified. + @staticmethod + def validate_additional_req(properties, prop_name, custom_def=None, ): + try: + source = properties.get(PortSpec.SOURCE) + source_range = properties.get(PortSpec.SOURCE_RANGE) + target = properties.get(PortSpec.TARGET) + target_range = properties.get(PortSpec.TARGET_RANGE) + + # verify one of the specified values is set + if source is None and source_range is None and \ + target is None and target_range is None: + ExceptionCollector.appendException( + InvalidTypeAdditionalRequirementsError( + type=PortSpec.TYPE_URI)) + # Validate source value is in specified range + if source and source_range: + validateutils.validate_value_in_range(source, source_range, + PortSpec.SOURCE) + else: + from toscaparser.dataentity import DataEntity + portdef = DataEntity('PortDef', source, None, PortSpec.SOURCE) + portdef.validate() + # Validate target value is in specified range + if target and target_range: + validateutils.validate_value_in_range(target, target_range, + PortSpec.TARGET) + else: + from toscaparser.dataentity import DataEntity + portdef = DataEntity('PortDef', source, None, PortSpec.TARGET) + portdef.validate() + except Exception: + msg = _('"%(value)s" do not meet requirements ' + 'for type "%(type)s".') \ + % {'value': properties, 'type': PortSpec.SHORTNAME} + ExceptionCollector.appendException( + ValueError(msg)) diff --git a/toscaparser/parameters.py b/toscaparser/parameters.py index 983aee30..a58f6a01 100644 --- a/toscaparser/parameters.py +++ b/toscaparser/parameters.py @@ -68,13 +68,16 @@ class Input(object): ExceptionCollector.appendException( ValueError(_('Invalid type "%s".') % type)) + # TODO(anyone) Need to test for any built-in datatype not just network + # that is, tosca.datatypes.* and not assume tosca.datatypes.network.* + # TODO(anyone) Add support for tosca.datatypes.Credential def _validate_value(self, value): tosca = EntityType.TOSCA_DEF datatype = None if self.type in tosca: datatype = tosca[self.type] - elif EntityType.DATATYPE_PREFIX + self.type in tosca: - datatype = tosca[EntityType.DATATYPE_PREFIX + self.type] + elif EntityType.DATATYPE_NETWORK_PREFIX + self.type in tosca: + datatype = tosca[EntityType.DATATYPE_NETWORK_PREFIX + self.type] DataEntity.validate_datatype(self.type, value, None, datatype) diff --git a/toscaparser/properties.py b/toscaparser/properties.py index 23c1db23..c69b1516 100644 --- a/toscaparser/properties.py +++ b/toscaparser/properties.py @@ -67,7 +67,8 @@ class Property(object): self.value = str(self.value) self.value = DataEntity.validate_datatype(self.type, self.value, self.entry_schema, - self.custom_def) + self.custom_def, + self.name) self._validate_constraints() def _validate_constraints(self): diff --git a/toscaparser/tests/data/datatypes/test_datatype_portspec_add_req.yaml b/toscaparser/tests/data/datatypes/test_datatype_portspec_add_req.yaml new file mode 100644 index 00000000..f9449276 --- /dev/null +++ b/toscaparser/tests/data/datatypes/test_datatype_portspec_add_req.yaml @@ -0,0 +1,41 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test PortSpec Additional Requirement clauses + +node_types: + + MyNodeType: + derived_from: Root + properties: + test_port: + type: PortSpec + +topology_template: + + node_templates: + + # Test invalid source value below (default) specified range constraint + test_node2: + type: MyNodeType + properties: + test_port: + protocol: tcp + source: 0 + + # Test invalid source value over specified range + test_node3: + type: MyNodeType + properties: + test_port: + protocol: tcp + source: 65535 + source_range: [ 2, 65534 ] + + # Test invalid source value under specified range + test_node4: + type: MyNodeType + properties: + test_port: + protocol: tcp + source: 1 + source_range: [ 2, 65534 ] diff --git a/toscaparser/tests/test_datatypes.py b/toscaparser/tests/test_datatypes.py index 0e613b2a..58fb2fed 100644 --- a/toscaparser/tests/test_datatypes.py +++ b/toscaparser/tests/test_datatypes.py @@ -66,16 +66,21 @@ class DataTypeTest(TestCase): tosca.my.datatypes.TestLab: properties: - temperature: - type: range - required: false - constraints: - - in_range: [-256, UNBOUNDED] humidity: type: range required: false constraints: - in_range: [-256, INFINITY] + temperature1: + type: range + required: false + constraints: + - in_range: [-256, UNBOUNDED] + temperature2: + type: range + required: false + constraints: + - in_range: [UNBOUNDED, 256] ''' custom_type_def = yamlparser.simple_parse(custom_type_schema) @@ -84,15 +89,6 @@ class DataTypeTest(TestCase): value = yamlparser.simple_parse(value_snippet) self.assertEqual(value, {}) - # TODO(Matt) - opened as bug 1555300 - # Need a test for PortSpec normative data type - # that tests the spec. requirement: "A valid PortSpec - # must have at least one of the following properties: - # target, target_range, source or source_range." - # TODO(Matt) - opened as bug 1555310 - # test PortSpec value for source and target - # against the source_range and target_range - # when specified. def test_built_in_datatype(self): value_snippet = ''' private_network: @@ -140,6 +136,31 @@ class DataTypeTest(TestCase): data = DataEntity('PortInfo', value.get('ethernet_port')) self.assertIsNotNone(data.validate()) + # Test normative PortSpec datatype's additional requirements + # TODO(Matt) - opened as bug 1555300 + # Need a test for PortSpec normative data type + # that tests the spec. requirement: "A valid PortSpec + # must have at least one of the following properties: + # target, target_range, source or source_range." + # TODO(Matt) - opened as bug 1555310 + # test PortSpec value for source and target + # against the source_range and target_range + # when specified. + def test_port_spec_addl_reqs(self): + value_snippet = ''' + test_port: + protocol: tcp + target: 65535 + target_range: [ 1, 65535 ] + source: 1 + source_range: [ 1, 65535 ] + + ''' + value = yamlparser.simple_parse(value_snippet) + data = DataEntity('tosca.datatypes.network.PortSpec', + value.get('test_port')) + self.assertIsNotNone(data.validate()) + def test_built_in_datatype_without_properties(self): value_snippet = ''' 2 @@ -365,6 +386,7 @@ class DataTypeTest(TestCase): value_snippet = ''' user_port: protocol: tcp + target: 1 target_range: [20000] ''' value = yamlparser.simple_parse(value_snippet) @@ -377,6 +399,7 @@ class DataTypeTest(TestCase): value_snippet = ''' user_port: protocol: tcp + target: 1 target_range: [20000, 3000] ''' value = yamlparser.simple_parse(value_snippet) @@ -400,7 +423,55 @@ class DataTypeTest(TestCase): def test_range_unbounded(self): value_snippet = ''' - temperature: [-100, 999999] + humidity: [-100, 100] + ''' + value = yamlparser.simple_parse(value_snippet) + data = DataEntity('tosca.my.datatypes.TestLab', + value, DataTypeTest.custom_type_def) + err = self.assertRaises(exception.InvalidSchemaError, + lambda: data.validate()) + self.assertEqual(_('The property "in_range" expects comparable values.' + ), + err.__str__()) + + def test_invalid_ranges_against_constraints(self): + # The TestLab range type has min=-256, max=UNBOUNDED + value_snippet = ''' + temperature1: [-257, 999999] + ''' + value = yamlparser.simple_parse(value_snippet) + data = DataEntity('tosca.my.datatypes.TestLab', value, + DataTypeTest.custom_type_def) + err = self.assertRaises(exception.ValidationError, data.validate) + self.assertEqual(_('The value "-257" of property "temperature1" is ' + 'out of range "(min:-256, max:UNBOUNDED)".'), + err.__str__()) + + value_snippet = ''' + temperature2: [-999999, 257] + ''' + value = yamlparser.simple_parse(value_snippet) + data = DataEntity('tosca.my.datatypes.TestLab', value, + DataTypeTest.custom_type_def) + err = self.assertRaises(exception.ValidationError, data.validate) + self.assertEqual(_('The value "257" of property "temperature2" is ' + 'out of range "(min:UNBOUNDED, max:256)".'), + err.__str__()) + + def test_valid_ranges_against_constraints(self): + + # The TestLab range type has max=UNBOUNDED + value_snippet = ''' + temperature1: [-255, 999999] + ''' + value = yamlparser.simple_parse(value_snippet) + data = DataEntity('tosca.my.datatypes.TestLab', value, + DataTypeTest.custom_type_def) + self.assertIsNotNone(data.validate()) + + # The TestLab range type has min=UNBOUNDED + value_snippet = ''' + temperature2: [-999999, 255] ''' value = yamlparser.simple_parse(value_snippet) data = DataEntity('tosca.my.datatypes.TestLab', value, diff --git a/toscaparser/tests/test_toscatpl.py b/toscaparser/tests/test_toscatpl.py index 3fd49bf8..b532f573 100644 --- a/toscaparser/tests/test_toscatpl.py +++ b/toscaparser/tests/test_toscatpl.py @@ -16,6 +16,7 @@ import six from toscaparser.common import exception import toscaparser.elements.interfaces as ifaces from toscaparser.elements.nodetype import NodeType +from toscaparser.elements.portspectype import PortSpec from toscaparser.functions import GetInput from toscaparser.functions import GetProperty from toscaparser.nodetemplate import NodeTemplate @@ -730,3 +731,40 @@ class ToscaTemplateTest(TestCase): rel = tosca.relationship_templates[0] self.assertEqual(len(rel.interfaces), 1) self.assertEqual(rel.interfaces[0].type, "Configure") + + def test_various_portspec_errors(self): + tosca_tpl = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/datatypes/test_datatype_portspec_add_req.yaml") + self.assertRaises(exception.ValidationError, ToscaTemplate, tosca_tpl, + None) + + # TODO(TBD) find way to reuse error messages from constraints.py + msg = (_('The value "%(pvalue)s" of property "%(pname)s" is out of ' + 'range "(min:%(vmin)s, max:%(vmax)s)".') % + dict(pname=PortSpec.SOURCE, + pvalue='0', + vmin='1', + vmax='65535')) + exception.ExceptionCollector.assertExceptionMessage( + exception.ValidationError, msg) + + # Test value below range min. + msg = (_('The value "%(pvalue)s" of property "%(pname)s" is out of ' + 'range "(min:%(vmin)s, max:%(vmax)s)".') % + dict(pname=PortSpec.SOURCE, + pvalue='1', + vmin='2', + vmax='65534')) + exception.ExceptionCollector.assertExceptionMessage( + exception.RangeValueError, msg) + + # Test value above range max. + msg = (_('The value "%(pvalue)s" of property "%(pname)s" is out of ' + 'range "(min:%(vmin)s, max:%(vmax)s)".') % + dict(pname=PortSpec.SOURCE, + pvalue='65535', + vmin='2', + vmax='65534')) + exception.ExceptionCollector.assertExceptionMessage( + exception.RangeValueError, msg) diff --git a/toscaparser/utils/validateutils.py b/toscaparser/utils/validateutils.py index f9b9fc58..43e14d67 100644 --- a/toscaparser/utils/validateutils.py +++ b/toscaparser/utils/validateutils.py @@ -17,14 +17,20 @@ import numbers import re import six +# from toscaparser.elements import constraints from toscaparser.common.exception import ExceptionCollector from toscaparser.common.exception import InvalidTOSCAVersionPropertyException +from toscaparser.common.exception import RangeValueError from toscaparser.utils.gettextutils import _ + log = logging.getLogger('tosca') +RANGE_UNBOUNDED = 'UNBOUNDED' + def str_to_num(value): '''Convert a string representation of a number into a numeric type.''' + # TODO(TBD) we should not allow numeric values in, input should be str if isinstance(value, numbers.Number): return value try: @@ -33,8 +39,11 @@ def str_to_num(value): return float(value) -def validate_number(value): - return str_to_num(value) +def validate_numeric(value): + if not isinstance(value, numbers.Number): + ExceptionCollector.appendException( + ValueError(_('"%s" is not a numeric.') % value)) + return value def validate_integer(value): @@ -51,7 +60,7 @@ def validate_float(value): if not isinstance(value, float): ExceptionCollector.appendException( ValueError(_('"%s" is not a float.') % value)) - return validate_number(value) + return value def validate_string(value): @@ -68,15 +77,53 @@ def validate_list(value): return value -def validate_range(value): - validate_list(value) - if isinstance(value, list): - if len(value) != 2 or not (value[0] <= value[1]): +def validate_range(range): + # list class check + validate_list(range) + # validate range list has a min and max + if len(range) != 2: + ExceptionCollector.appendException( + ValueError(_('"%s" is not a valid range.') % range)) + # validate min and max are numerics or the keyword UNBOUNDED + min_test = max_test = False + if not range[0] == RANGE_UNBOUNDED: + min = validate_numeric(range[0]) + else: + min_test = True + if not range[1] == RANGE_UNBOUNDED: + max = validate_numeric(range[1]) + else: + max_test = True + # validate the max > min (account for UNBOUNDED) + if not min_test and not max_test: + # Note: min == max is allowed + if min > max: ExceptionCollector.appendException( - ValueError(_('"%s" is not a valid range.') % value)) - validate_integer(value[0]) - if not value[1] == "UNBOUNDED": - validate_integer(value[1]) + ValueError(_('"%s" is not a valid range.') % range)) + + return range + + +def validate_value_in_range(value, range, prop_name): + validate_numeric(value) + validate_range(range) + + # Note: value is valid if equal to min + if range[0] != RANGE_UNBOUNDED: + if value < range[0]: + ExceptionCollector.appendException( + RangeValueError(pname=prop_name, + pvalue=value, + vmin=range[0], + vmax=range[1])) + # Note: value is valid if equal to max + if range[1] != RANGE_UNBOUNDED: + if value > range[1]: + ExceptionCollector.appendException( + RangeValueError(pname=prop_name, + pvalue=value, + vmin=range[0], + vmax=range[1])) return value