Implement PortSpec datatype and all spec. requirements

Support the built-in PortSpec datatype beyond basic schema by
checking for all sub properties and additional requirements
during validation.

Change-Id: Idb7ecbe7a8587badee59eae96c8f2b487e9dc0e6
This commit is contained in:
Matt Rutkowski 2016-03-28 23:00:20 -05:00
parent ceba80e85e
commit 0a91862a05
13 changed files with 347 additions and 41 deletions

View File

@ -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')

View File

@ -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()

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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))

View File

@ -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)

View File

@ -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):

View File

@ -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 ]

View File

@ -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,

View File

@ -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)

View File

@ -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