From b1075e9992d161fafe078912ccbbd74678b23b86 Mon Sep 17 00:00:00 2001 From: Meena Date: Mon, 10 Aug 2015 15:58:38 +0530 Subject: [PATCH] Validation of TOSCA version The TOSCA version is validated such that it follows the Maven version syntax. Added unittest cases for validating TOSCA version. Modified the log messages such that they support i18n standards Implemented tosca version property type in toscalib and added testcases. Closes-Bug: #1450854 Closes-Bug: #1482359 Change-Id: I2c3adecd7291b7bd3b7e9cc2e651e51a4c205fc5 --- translator/hot/tests/test_translate_inputs.py | 49 ++++++ translator/hot/tosca/tosca_compute.py | 7 +- translator/hot/translate_inputs.py | 4 + translator/toscalib/common/exception.py | 4 + translator/toscalib/dataentity.py | 2 + .../elements/TOSCA_definition_1_0.yaml | 2 +- translator/toscalib/elements/constraints.py | 4 +- .../tests/test_validate_tosca_version.py | 139 ++++++++++++++++++ translator/toscalib/utils/validateutils.py | 74 ++++++++++ 9 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 translator/toscalib/tests/test_validate_tosca_version.py diff --git a/translator/hot/tests/test_translate_inputs.py b/translator/hot/tests/test_translate_inputs.py index edbdc98..89eb3f1 100644 --- a/translator/hot/tests/test_translate_inputs.py +++ b/translator/hot/tests/test_translate_inputs.py @@ -293,3 +293,52 @@ class ToscaTemplateInputValidationTest(TestCase): input_params = {'storage_size': '-2 MB'} expectedmsg = _('"-2 MB" is not a valid scalar-unit') self._translate_input_test(tpl_snippet, input_params, expectedmsg) + + def test_invalid_input_type_version(self): + tpl_snippet = ''' + inputs: + version: + type: version + ''' + + input_params = {'version': '0.a'} + expectedmessage = _('Value of TOSCA version property ' + '"0.a" is invalid.') + self._translate_input_test(tpl_snippet, input_params, + expectedmessage) + + input_params = {'version': '0.0.0.abc'} + expectedmessage = _('Value of TOSCA version property ' + '"0.0.0.abc" is invalid.') + self._translate_input_test(tpl_snippet, input_params, + expectedmessage) + + def test_valid_input_type_version(self): + tpl_snippet = ''' + inputs: + version: + type: version + default: 12 + ''' + + expectedmessage = _('both equal.') + input_params = {'version': '18'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) + + input_params = {'version': '18.0'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) + + input_params = {'version': '18.0.1'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0.1')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) diff --git a/translator/hot/tosca/tosca_compute.py b/translator/hot/tosca/tosca_compute.py index 756ff05..81daa93 100755 --- a/translator/hot/tosca/tosca_compute.py +++ b/translator/hot/tosca/tosca_compute.py @@ -11,8 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. +import logging from translator.common.utils import MemoryUnit from translator.hot.syntax.hot_resource import HotResource +from translator.toscalib.utils.validateutils import TOSCAVersionProperty +log = logging.getLogger('tosca') # A design issue to be resolved is how to translate the generic TOSCA server # properties to OpenStack flavors and images. At the Atlanta design summit, @@ -76,9 +79,6 @@ class ToscaCompute(HotResource): pass def handle_properties(self): - self.properties.update(self.translate_compute_flavor_and_image( - self.nodetemplate.get_capability('host'), - self.nodetemplate.get_capability('os'))) self.properties = self.translate_compute_flavor_and_image( self.nodetemplate.get_capability('host'), self.nodetemplate.get_capability('os')) @@ -156,6 +156,7 @@ class ToscaCompute(HotResource): 'distribution', distribution) version = properties.get('version') + version = TOSCAVersionProperty(version).get_version() match_version = self._match_images(match_distribution, IMAGES, 'version', version) diff --git a/translator/hot/translate_inputs.py b/translator/hot/translate_inputs.py index 499b7b1..281fbd9 100644 --- a/translator/hot/translate_inputs.py +++ b/translator/hot/translate_inputs.py @@ -16,6 +16,7 @@ from translator.hot.syntax.hot_parameter import HotParameter from translator.toscalib.dataentity import DataEntity from translator.toscalib.elements.scalarunit import ScalarUnit_Size from translator.toscalib.utils.gettextutils import _ +from translator.toscalib.utils.validateutils import TOSCAVersionProperty INPUT_CONSTRAINTS = (CONSTRAINTS, DESCRIPTION, LENGTH, RANGE, @@ -49,6 +50,7 @@ TOSCA_TO_HOT_INPUT_TYPES = {'string': 'string', 'boolean': 'boolean', 'timestamp': 'string', 'scalar-unit.size': 'number', + 'version': 'string', 'null': 'string', 'PortDef': 'number'} @@ -100,6 +102,8 @@ class TranslateInputs(object): "to %(hot_default)s GB.") % {'input_value': input_value, 'hot_default': hot_default}) + if input.type == 'version': + hot_default = TOSCAVersionProperty(hot_default).get_version() hot_constraints = [] if input.constraints: diff --git a/translator/toscalib/common/exception.py b/translator/toscalib/common/exception.py index 2a1e8d2..6a03e18 100644 --- a/translator/toscalib/common/exception.py +++ b/translator/toscalib/common/exception.py @@ -94,3 +94,7 @@ class InvalidPropertyValueError(TOSCAException): class InvalidTemplateVersion(TOSCAException): msg_fmt = _('The template version "%(what)s" is invalid. ' 'The valid versions are: "%(valid_versions)s"') + + +class InvalidTOSCAVersionPropertyException(TOSCAException): + msg_fmt = _('Value of TOSCA version property "%(what)s" is invalid.') diff --git a/translator/toscalib/dataentity.py b/translator/toscalib/dataentity.py index a971b83..2a57b1e 100644 --- a/translator/toscalib/dataentity.py +++ b/translator/toscalib/dataentity.py @@ -131,6 +131,8 @@ class DataEntity(object): return ScalarUnit_Frequency(value).validate_scalar_unit() elif type == Schema.SCALAR_UNIT_TIME: return ScalarUnit_Time(value).validate_scalar_unit() + elif type == Schema.VERSION: + return validateutils.TOSCAVersionProperty(value).get_version() elif type == Schema.MAP: validateutils.validate_map(value) if entry_schema: diff --git a/translator/toscalib/elements/TOSCA_definition_1_0.yaml b/translator/toscalib/elements/TOSCA_definition_1_0.yaml index 121e7a1..3e488ca 100644 --- a/translator/toscalib/elements/TOSCA_definition_1_0.yaml +++ b/translator/toscalib/elements/TOSCA_definition_1_0.yaml @@ -542,7 +542,7 @@ tosca.capabilities.OperatingSystem: debian, fedora, rhel and ubuntu. version: required: false - type: string + type: version description: > The host Operating System version. diff --git a/translator/toscalib/elements/constraints.py b/translator/toscalib/elements/constraints.py index 1ed5f0b..c984340 100644 --- a/translator/toscalib/elements/constraints.py +++ b/translator/toscalib/elements/constraints.py @@ -35,12 +35,12 @@ class Schema(collections.Mapping): INTEGER, STRING, BOOLEAN, FLOAT, NUMBER, TIMESTAMP, LIST, MAP, SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, - PORTDEF + PORTDEF, VERSION ) = ( 'integer', 'string', 'boolean', 'float', 'number', 'timestamp', 'list', 'map', 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', - 'PortDef' + 'PortDef', 'version' ) SCALAR_UNIT_SIZE_DEFAULT = 'B' diff --git a/translator/toscalib/tests/test_validate_tosca_version.py b/translator/toscalib/tests/test_validate_tosca_version.py new file mode 100644 index 0000000..fb40b71 --- /dev/null +++ b/translator/toscalib/tests/test_validate_tosca_version.py @@ -0,0 +1,139 @@ +# 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. + +from translator.toscalib.common.exception import ( + InvalidTOSCAVersionPropertyException) +from translator.toscalib.tests.base import TestCase +from translator.toscalib.utils.validateutils import TOSCAVersionProperty + + +class TOSCAVersionPropertyTest(TestCase): + + def test_tosca_version_property(self): + version = '18.0.3.beta-1' + expected_output = '18.0.3.beta-1' + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 18 + expected_output = '18.0' + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 18.0 + expected_output = '18.0' + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = '18.0.3' + expected_output = '18.0.3' + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 0 + expected_output = None + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 00 + expected_output = None + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 0.0 + expected_output = None + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = 00.00 + expected_output = None + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + version = '0.0.0' + expected_output = None + output = TOSCAVersionProperty(version).get_version() + self.assertEqual(output, expected_output) + + def test_tosca_version_property_invalid_major_version(self): + + version = 'x' + exp_msg = ('Value of TOSCA version property "x" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + def test_tosca_version_property_invalid_minor_version(self): + + version = '18.x' + exp_msg = ('Value of TOSCA version property "18.x" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + version = '18.x.y' + exp_msg = ('Value of TOSCA version property "18.x.y" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + version = '18-2' + exp_msg = ('Value of TOSCA version property "18-2" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + def test_tosca_version_property_invalid_fix_version(self): + + version = '18.0.a' + exp_msg = ('Value of TOSCA version property "18.0.a" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + def test_tosca_version_property_invalid_qualifier(self): + + version = '18.0.1-xyz' + exp_msg = ('Value of TOSCA version property "18.0.1-xyz" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + version = '0.0.0.abc' + exp_msg = ('Value of TOSCA version property "0.0.0.abc" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + def test_tosca_version_property_invalid_build_version(self): + + version = '18.0.1.abc-x' + exp_msg = ('Value of TOSCA version property ' + '"18.0.1.abc-x" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) + + version = '0.0.0.abc-x' + exp_msg = ('Value of TOSCA version property "0.0.0.abc-x" is invalid.') + try: + TOSCAVersionProperty(version).get_version() + except InvalidTOSCAVersionPropertyException as err: + self.assertEqual(exp_msg, err.__str__()) diff --git a/translator/toscalib/utils/validateutils.py b/translator/toscalib/utils/validateutils.py index c184b7e..56116af 100644 --- a/translator/toscalib/utils/validateutils.py +++ b/translator/toscalib/utils/validateutils.py @@ -12,10 +12,15 @@ import collections import dateutil.parser +import logging import numbers +import re import six +from translator.toscalib.common.exception import ( + InvalidTOSCAVersionPropertyException) from translator.toscalib.utils.gettextutils import _ +log = logging.getLogger('tosca') def str_to_num(value): @@ -78,3 +83,72 @@ def validate_boolean(value): def validate_timestamp(value): return dateutil.parser.parse(value) + + +class TOSCAVersionProperty(object): + + VERSION_RE = re.compile('^(?P([0-9][0-9]*))' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9A-Za-z]+)))?' + '(\-(?P[0-9])*)?$') + + def __init__(self, version): + self.version = str(version) + match = self.VERSION_RE.match(self.version) + if not match: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + ver = match.groupdict() + if self.version in ['0', '0.0', '0.0.0']: + log.warning(_('Version assumed as not provided')) + self.version = None + self.minor_version = ver['minor_version'] + self.major_version = ver['major_version'] + self.fix_version = ver['fix_version'] + self.qualifier = self._validate_qualifier(ver['qualifier']) + self.build_version = self._validate_build(ver['build_version']) + self._validate_major_version(self.major_version) + + def _validate_major_version(self, value): + """Validate major version + + Checks if only major version is provided and assumes + minor version as 0. + Eg: If version = 18, then it returns version = '18.0' + """ + + if self.minor_version is None and self.build_version is None and \ + value != '0': + log.warning(_('Minor version assumed "0"')) + self.version = '.'.join([value, '0']) + return value + + def _validate_qualifier(self, value): + """Validate qualifier + + TOSCA version is invalid if a qualifier is present without the + fix version or with all of major, minor and fix version 0s. + + For example, the following versions are invalid + 18.0.abc + 0.0.0.abc + """ + if (self.fix_version is None and value) or \ + (self.minor_version == self.major_version == + self.fix_version == '0' and value): + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def _validate_build(self, value): + """Validate build version + + TOSCA version is invalid if build version is present without the + qualifier. + Eg: version = 18.0.0-1 is invalid. + """ + if not self.qualifier and value: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def get_version(self): + return self.version