diff --git a/translator/toscalib/common/__init__.py b/translator/toscalib/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/translator/toscalib/common/exception.py b/translator/toscalib/common/exception.py new file mode 100644 index 0000000..76de267 --- /dev/null +++ b/translator/toscalib/common/exception.py @@ -0,0 +1,74 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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. + +''' +TOSCA exception classes +''' +import logging +import sys + +from translator.toscalib.utils.gettextutils import _ + + +log = logging.getLogger(__name__) + + +class TOSCAException(Exception): + '''Base exception class for TOSCA + + To correctly use this class, inherit from it and define + a 'msg_fmt' property. + + ''' + + _FATAL_EXCEPTION_FORMAT_ERRORS = False + + message = _('An unknown exception occurred.') + + def __init__(self, **kwargs): + try: + self.message = self.msg_fmt % kwargs + except KeyError: + exc_info = sys.exc_info() + log.exception(_('Exception in string format operation: %s') + % exc_info[1]) + + if TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS: + raise exc_info[0] + + def __str__(self): + return self.message + + @staticmethod + def set_fatal_format_exception(flag): + if isinstance(flag, bool): + TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS = flag + + +class MissingRequiredFieldError(TOSCAException): + msg_fmt = _('%(what)s is missing required field: "%(required)s".') + + +class UnknownFieldError(TOSCAException): + msg_fmt = _('%(what)s contain(s) unknown field: "%(field)s", ' + 'refer to the TOSCA specs to verify valid values.') + + +class TypeMismatchError(TOSCAException): + msg_fmt = _('%(what)s must be of type: "%(type)s".') + + +class InvalidNodeTypeError(TOSCAException): + msg_fmt = _('Node type "%(what)s" is not a valid type.') diff --git a/translator/toscalib/elements/interfaces.py b/translator/toscalib/elements/interfaces.py index e9d4b72..0d7ff97 100644 --- a/translator/toscalib/elements/interfaces.py +++ b/translator/toscalib/elements/interfaces.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.toscalib.common.exception import UnknownFieldError from translator.toscalib.elements.statefulentitytype import StatefulEntityType from translator.toscalib.functions import get_function @@ -20,6 +21,8 @@ SECTIONS = (LIFECYCLE, CONFIGURE) = \ ('tosca.interfaces.node.Lifecycle', 'tosca.interfaces.relationship.Configure') +INTERFACEVALUE = (IMPLEMENTATION, INPUT) = ('implementation', 'input') + class InterfacesDef(StatefulEntityType): '''TOSCA built-in interfaces type.''' @@ -41,8 +44,12 @@ class InterfacesDef(StatefulEntityType): for i, j in self.value.items(): if i == 'implementation': self.implementation = j - if i == 'input': + elif i == 'input': self.input = self._create_input_functions(j) + else: + what = ('Interfaces of node template %s' % + self.node_template.name) + raise UnknownFieldError(what=what, field=i) else: self.implementation = value diff --git a/translator/toscalib/elements/nodetype.py b/translator/toscalib/elements/nodetype.py index 6534501..0398b2c 100644 --- a/translator/toscalib/elements/nodetype.py +++ b/translator/toscalib/elements/nodetype.py @@ -13,12 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.toscalib.common.exception import InvalidNodeTypeError from translator.toscalib.elements.capabilitytype import CapabilityTypeDef from translator.toscalib.elements.interfaces import InterfacesDef from translator.toscalib.elements.property_definition import PropertyDef from translator.toscalib.elements.relationshiptype import RelationshipType from translator.toscalib.elements.statefulentitytype import StatefulEntityType -from translator.toscalib.utils.gettextutils import _ SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, @@ -37,8 +37,7 @@ class NodeType(StatefulEntityType): elif custom_def and ntype in list(custom_def.keys()): self.defs = custom_def[ntype] else: - raise ValueError(_('Node type %(ntype)s is not a valid type.') - % {'ntype': ntype}) + raise InvalidNodeTypeError(what=ntype) self.type = ntype self.related = {} @@ -69,16 +68,7 @@ class NodeType(StatefulEntityType): ''' relationship = {} - requires = self.requirements - parent_node = self.parent_type - if requires is None: - requires = self.get_value(REQUIREMENTS, None, True) - parent_node = parent_node.parent_type - if parent_node: - while parent_node.type != 'tosca.nodes.Root': - req = parent_node.get_value(REQUIREMENTS, None, True) - requires.extend(req) - parent_node = parent_node.parent_type + requires = self.get_all_requirements() if requires: for req in requires: for key, value in req.items(): @@ -128,6 +118,19 @@ class NodeType(StatefulEntityType): def requirements(self): return self.get_value(REQUIREMENTS) + def get_all_requirements(self): + requires = self.requirements + parent_node = self.parent_type + if requires is None: + requires = self.get_value(REQUIREMENTS, None, True) + parent_node = parent_node.parent_type + if parent_node: + while parent_node.type != 'tosca.nodes.Root': + req = parent_node.get_value(REQUIREMENTS, None, True) + requires.extend(req) + parent_node = parent_node.parent_type + return requires + @property def interfaces(self): return self.get_value(INTERFACES) diff --git a/translator/toscalib/nodetemplate.py b/translator/toscalib/nodetemplate.py index b0063b4..9c0eefb 100644 --- a/translator/toscalib/nodetemplate.py +++ b/translator/toscalib/nodetemplate.py @@ -16,16 +16,20 @@ import logging +from translator.toscalib.common.exception import MissingRequiredFieldError +from translator.toscalib.common.exception import TypeMismatchError +from translator.toscalib.common.exception import UnknownFieldError from translator.toscalib.elements.capabilitytype import CapabilityTypeDef from translator.toscalib.elements.interfaces import InterfacesDef +from translator.toscalib.elements.interfaces import LIFECYCLE, CONFIGURE from translator.toscalib.elements.nodetype import NodeType from translator.toscalib.properties import Property -from translator.toscalib.utils.gettextutils import _ + SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES) = \ + INTERFACES, CAPABILITIES, TYPE) = \ ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities') + 'capabilities', 'type') log = logging.getLogger('tosca') @@ -34,10 +38,11 @@ class NodeTemplate(object): '''Node template from a Tosca profile.''' def __init__(self, name, node_templates, custom_def=None): self.name = name - self.node_type = NodeType(node_templates[name]['type'], custom_def) self.node_templates = node_templates + self._validate_field() self.node_template = node_templates[self.name] - self.type = self.node_template['type'] + self.type = self.node_template[TYPE] + self.node_type = NodeType(self.type, custom_def) self.related = {} @property @@ -95,30 +100,12 @@ class NodeTemplate(object): def properties(self): props = [] properties = self.node_type.get_value(PROPERTIES, self.node_template) - requiredprop = [] - for p in self.node_type.properties_def: - if p.required: - requiredprop.append(p.name) if properties: - #make sure it's not missing any property required by a node type - missingprop = [] - for r in requiredprop: - if r not in properties.keys(): - missingprop.append(r) - if missingprop: - raise ValueError(_("Node template %(tpl)s is missing " - "one or more required properties %(prop)s") - % {'tpl': self.name, 'prop': missingprop}) for name, value in properties.items(): for p in self.node_type.properties_def: if p.name == name: prop = Property(name, value, p.schema) props.append(prop) - else: - if requiredprop: - raise ValueError(_("Node template %(tpl)s is missing" - "one or more required properties %(prop)s") - % {'tpl': self.name, 'prop': requiredprop}) return props def _add_next(self, nodetpl, relationship): @@ -151,5 +138,103 @@ class NodeTemplate(object): return c.property_value def validate(self): + self._validate_capabilities() + self._validate_requirments() + self._validate_properties() + self._validate_interfaces() for prop in self.properties: prop.validate() + + def _validate_capabilities(self): + type_capabilities = self.node_type.capabilities + allowed_caps = [] + if type_capabilities: + for tcap in type_capabilities: + allowed_caps.append(tcap.name) + capabilities = self.node_type.get_value(CAPABILITIES, + self.node_template) + if capabilities: + self._common_validate_field(capabilities, allowed_caps, + 'Capabilities') + + def _validate_requirments(self): + type_requires = self.node_type.get_all_requirements() + allowed_reqs = [] + if type_requires: + for treq in type_requires: + for key in treq: + allowed_reqs.append(key) + requires = self.node_type.get_value(REQUIREMENTS, self.node_template) + if requires: + if not isinstance(requires, list): + raise TypeMismatchError( + what='Requirements of node template %s' % self.name, + type='list') + for req in requires: + self._common_validate_field(req, allowed_reqs, 'Requirements') + + def _validate_interfaces(self): + ifaces = self.node_type.get_value(INTERFACES, self.node_template) + if ifaces: + for i in ifaces: + for name, value in ifaces.items(): + if name == LIFECYCLE: + self._common_validate_field( + value, InterfacesDef. + interfaces_node_lifecycle_operations, + 'Interfaces') + elif name == CONFIGURE: + self._common_validate_field( + value, InterfacesDef. + interfaces_relationship_confiure_operations, + 'Interfaces') + else: + raise UnknownFieldError( + what='Interfaces of node template %s' % self.name, + field=name) + + def _validate_properties(self): + properties = self.node_type.get_value(PROPERTIES, self.node_template) + allowed_props = [] + required_props = [] + for p in self.node_type.properties_def: + allowed_props.append(p.name) + if p.required: + required_props.append(p.name) + if properties: + self._common_validate_field(properties, allowed_props, + 'Properties') + #make sure it's not missing any property required by a node type + missingprop = [] + for r in required_props: + if r not in properties.keys(): + missingprop.append(r) + if missingprop: + raise MissingRequiredFieldError( + what='Properties of node template %s' % self.name, + required=missingprop) + else: + if required_props: + raise MissingRequiredFieldError( + what='Properties of node template %s' % self.name, + required=missingprop) + + def _validate_field(self): + if not isinstance(self.node_templates[self.name], dict): + raise MissingRequiredFieldError( + what='Node template %s' % self.name, required=TYPE) + try: + self.node_templates[self.name][TYPE] + except KeyError: + raise MissingRequiredFieldError( + what='Node template %s' % self.name, required=TYPE) + self._common_validate_field(self.node_templates[self.name], SECTIONS, + 'Second level') + + def _common_validate_field(self, schema, allowedlist, section): + for name in schema: + if name not in allowedlist: + raise UnknownFieldError( + what='%(section)s of node template %(nodename)s' + % {'section': section, 'nodename': self.name}, + field=name) diff --git a/translator/toscalib/parameters.py b/translator/toscalib/parameters.py index 34a724b..3fb8ff4 100644 --- a/translator/toscalib/parameters.py +++ b/translator/toscalib/parameters.py @@ -16,41 +16,63 @@ import logging +from translator.toscalib.common.exception import MissingRequiredFieldError +from translator.toscalib.common.exception import UnknownFieldError from translator.toscalib.elements.constraints import Constraint from translator.toscalib.properties import Property + log = logging.getLogger('tosca') class Input(object): + + INPUTFIELD = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS) = \ + ('type', 'description', 'default', 'constraints') + def __init__(self, name, schema): self.name = name self.schema = schema @property def type(self): - return self.schema['type'] + return self.schema[self.TYPE] @property def description(self): if Property.DESCRIPTION in self.schema: - return self.schema['description'] + return self.schema[self.DESCRIPTION] @property def default(self): if self.Property.DEFAULT in self.schema: - return self.schema['default'] + return self.schema[self.DEFAULT] @property def constraints(self): if Property.CONSTRAINTS in self.schema: - return self.schema['constraints'] + return self.schema[self.CONSTRAINTS] def validate(self): + self._validate_field() self.validate_type(self.type) if self.constraints: self.validate_constraints(self.constraints) + def _validate_field(self): + if not isinstance(self.schema, dict): + raise MissingRequiredFieldError(what='Input %s' % self.name, + required=self.TYPE) + try: + self.type + except KeyError: + raise MissingRequiredFieldError(what='Input %s' % self.name, + required=self.TYPE) + for name in self.schema: + if name not in self.INPUTFIELD: + raise UnknownFieldError(what='Input %s' % self.name, + field=name) + def validate_type(self, input_type): if input_type not in Property.PROPERTIY_TYPES: raise ValueError(_('Invalid type %s') % type) @@ -63,14 +85,34 @@ class Input(object): class Output(object): + + OUTPUTFIELD = (DESCRIPTION, VALUE) = ('description', 'value') + def __init__(self, name, attrs): self.name = name self.attrs = attrs @property def description(self): - return self.attrs['description'] + return self.attrs[self.DESCRIPTION] @property def value(self): - return self.attrs['value'] + return self.attrs[self.VALUE] + + def validate(self): + self._validate_field() + + def _validate_field(self): + if not isinstance(self.attrs, dict): + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + try: + self.value + except KeyError: + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + for name in self.attrs: + if name not in self.OUTPUTFIELD: + raise UnknownFieldError(what='Output %s' % self.name, + field=name) diff --git a/translator/toscalib/tests/data/test_tosca_top_level_error1.yaml b/translator/toscalib/tests/data/test_tosca_top_level_error1.yaml new file mode 100644 index 0000000..571d972 --- /dev/null +++ b/translator/toscalib/tests/data/test_tosca_top_level_error1.yaml @@ -0,0 +1,28 @@ +description: > + TOSCA simple profile missing version section. + +inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + +node_templates: + server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + +outputs: + server_address: + description: IP address of server instance. + value: { get_property: [server, ip_address] } diff --git a/translator/toscalib/tests/data/test_tosca_top_level_error2.yaml b/translator/toscalib/tests/data/test_tosca_top_level_error2.yaml new file mode 100644 index 0000000..917b218 --- /dev/null +++ b/translator/toscalib/tests/data/test_tosca_top_level_error2.yaml @@ -0,0 +1,30 @@ +tosca_definitions_version: tosca_simple_1.0 + +description: > + TOSCA simple profile with invalid top-level key: 'node_template'. + +inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + +node_template: + server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + +outputs: + server_address: + description: IP address of server instance. + value: { get_property: [server, ip_address] } diff --git a/translator/toscalib/tests/test_exception.py b/translator/toscalib/tests/test_exception.py new file mode 100644 index 0000000..5e30337 --- /dev/null +++ b/translator/toscalib/tests/test_exception.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 MissingRequiredFieldError +from translator.toscalib.common.exception import TOSCAException +from translator.toscalib.common.exception import UnknownFieldError +from translator.toscalib.tests.base import TestCase + + +class ExceptionTest(TestCase): + + def setUp(self): + super(TestCase, self).setUp() + TOSCAException.set_fatal_format_exception(False) + + def test_message(self): + ex = MissingRequiredFieldError(what='Template', required='type') + self.assertEqual('Template is missing required field: "type".', + ex.__str__()) + + def test_set_flag(self): + TOSCAException.set_fatal_format_exception('True') + self.assertFalse(TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS) + + def test_format_error(self): + ex = UnknownFieldError(what='Template') + self.assertEqual('An unknown exception occurred.', ex.__str__(),) + self.assertRaises(KeyError, self._formate_exception) + + def _formate_exception(self): + UnknownFieldError.set_fatal_format_exception(True) + raise UnknownFieldError(what='Template') diff --git a/translator/toscalib/tests/test_toscadef.py b/translator/toscalib/tests/test_toscadef.py index 5710e82..19e0562 100644 --- a/translator/toscalib/tests/test_toscadef.py +++ b/translator/toscalib/tests/test_toscadef.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.toscalib.common.exception import InvalidNodeTypeError from translator.toscalib.elements.nodetype import NodeType from translator.toscalib.tests.base import TestCase compute_type = NodeType('tosca.nodes.Compute') @@ -22,7 +23,8 @@ component_type = NodeType('tosca.nodes.SoftwareComponent') class ToscaDefTest(TestCase): def test_type(self): self.assertEqual(compute_type.type, "tosca.nodes.Compute") - self.assertRaises(ValueError, NodeType, 'tosca.nodes.Invalid') + self.assertRaises(InvalidNodeTypeError, NodeType, + 'tosca.nodes.Invalid') def test_parent_type(self): self.assertEqual(compute_type.parent_type.type, "tosca.nodes.Root") diff --git a/translator/toscalib/tests/test_toscatplvalidation.py b/translator/toscalib/tests/test_toscatplvalidation.py new file mode 100644 index 0000000..9650175 --- /dev/null +++ b/translator/toscalib/tests/test_toscatplvalidation.py @@ -0,0 +1,430 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 os +from translator.toscalib.common.exception import InvalidNodeTypeError +from translator.toscalib.common.exception import MissingRequiredFieldError +from translator.toscalib.common.exception import TypeMismatchError +from translator.toscalib.common.exception import UnknownFieldError +from translator.toscalib.nodetemplate import NodeTemplate +from translator.toscalib.parameters import Input, Output +from translator.toscalib.tests.base import TestCase +from translator.toscalib.tosca_template import ToscaTemplate +import translator.toscalib.utils.yamlparser + + +class ToscaTemplateValidationTest(TestCase): + + def test_well_defined_template(self): + tpl_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/tosca_single_instance_wordpress.yaml") + self.assertIsNotNone(ToscaTemplate(tpl_path)) + + def test_first_level_sections(self): + tpl_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/test_tosca_top_level_error1.yaml") + try: + ToscaTemplate(tpl_path) + except Exception as err: + self.assertTrue(isinstance(err, MissingRequiredFieldError)) + self.assertEqual('Template is missing required field: ' + '"tosca_definitions_version".', err.__str__()) + + tpl_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/test_tosca_top_level_error2.yaml") + try: + ToscaTemplate(tpl_path) + except Exception as err: + self.assertTrue(isinstance(err, UnknownFieldError)) + self.assertEqual('Template contain(s) unknown field: ' + '"node_template", refer to the TOSCA specs ' + 'to verify valid values.', err.__str__()) + + def test_inputs(self): + tpl_snippet = ''' + inputs: + cpus: + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + ''' + inputs = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet)['inputs']) + name, attrs = list(inputs.items())[0] + input = Input(name, attrs) + try: + input.validate() + except Exception as err: + self.assertTrue(isinstance(err, MissingRequiredFieldError)) + self.assertEqual('Input cpus is missing required field: ' + '"type".', err.__str__()) + + tpl_snippet = ''' + inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraint: + - valid_values: [ 1, 2, 4, 8 ] + ''' + inputs = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet)['inputs']) + name, attrs = list(inputs.items())[0] + input = Input(name, attrs) + try: + input.validate() + except Exception as err: + self.assertTrue(isinstance(err, UnknownFieldError)) + self.assertEqual('Input cpus contain(s) unknown field: ' + '"constraint", refer to the TOSCA specs to ' + 'verify valid values.', err.__str__()) + + def test_outputs(self): + tpl_snippet = ''' + outputs: + server_address: + description: IP address of server instance. + values: { get_property: [server, ip_address] } + ''' + outputs = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet)['outputs']) + name, attrs = list(outputs.items())[0] + output = Output(name, attrs) + try: + output.validate() + except Exception as err: + self.assertTrue(isinstance(err, MissingRequiredFieldError)) + self.assertEqual('Output server_address is missing required ' + 'field: "value".', err.__str__()) + + tpl_snippet = ''' + outputs: + server_address: + descriptions: IP address of server instance. + value: { get_property: [server, ip_address] } + ''' + outputs = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet)['outputs']) + name, attrs = list(outputs.items())[0] + output = Output(name, attrs) + try: + output.validate() + except Exception as err: + self.assertTrue(isinstance(err, UnknownFieldError)) + self.assertEqual('Output server_address contain(s) unknown ' + 'field: "descriptions", refer to the TOSCA ' + 'specs to verify valid values.', + err.__str__()) + + def _custom_types(self): + custom_types = {} + def_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/custom_types/wordpress.yaml") + custom_type = translator.toscalib.utils.yamlparser.load_yaml(def_file) + node_types = custom_type['node_types'] + for name in node_types: + defintion = node_types[name] + custom_types[name] = defintion + return custom_types + + def _single_node_template_content_test(self, tpl_snippet, expectederror, + expectedmessage): + nodetemplates = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet))['node_templates'] + name = list(nodetemplates.keys())[0] + try: + nodetemplate = NodeTemplate(name, nodetemplates, + self._custom_types()) + nodetemplate.validate() + nodetemplate.requirements + nodetemplate.capabilities + nodetemplate.properties + nodetemplate.interfaces + except Exception as err: + self.assertTrue(isinstance(err, expectederror)) + self.assertEqual(expectedmessage, err.__str__()) + + def test_node_templates(self): + tpl_snippet = ''' + node_templates: + server: + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + ''' + expectedmessage = ('Node template server is missing ' + 'required field: "type".') + self._single_node_template_content_test(tpl_snippet, + MissingRequiredFieldError, + expectedmessage) + + tpl_snippet = ''' + node_templates: + mysql_dbms: + type: tosca.nodes.DBMS + properties: + dbms_root_password: { get_input: db_root_pwd } + dbms_port: { get_input: db_port } + requirement: + - host: server + interfaces: + tosca.interfaces.node.Lifecycle: + create: mysql_dbms_install.sh + start: mysql_dbms_start.sh + configure: + implementation: mysql_dbms_configure.sh + input: + db_root_password: { get_property: [ mysql_dbms, \ + dbms_root_password ] } + ''' + expectedmessage = ('Second level of node template mysql_dbms ' + 'contain(s) unknown field: "requirement", ' + 'refer to the TOSCA specs to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + def test_node_template_type(self): + tpl_snippet = ''' + node_templates: + mysql_database: + type: tosca.nodes.Databases + properties: + db_name: { get_input: db_name } + db_user: { get_input: db_user } + db_password: { get_input: db_pwd } + capabilities: + database_endpoint: + properties: + port: { get_input: db_port } + requirements: + - host: mysql_dbms + interfaces: + tosca.interfaces.node.Lifecycle: + configure: mysql_database_configure.sh + ''' + expectedmessage = ('Node type "tosca.nodes.Databases" is not ' + 'a valid type.') + self._single_node_template_content_test(tpl_snippet, + InvalidNodeTypeError, + expectedmessage) + + def test_node_template_requirements(self): + tpl_snippet = ''' + node_templates: + webserver: + type: tosca.nodes.WebServer + requirements: + host: server + interfaces: + tosca.interfaces.node.Lifecycle: + create: webserver_install.sh + start: webserver_start.sh + ''' + expectedmessage = ('Requirements of node template webserver ' + 'must be of type: "list".') + self._single_node_template_content_test(tpl_snippet, + TypeMismatchError, + expectedmessage) + + tpl_snippet = ''' + node_templates: + mysql_database: + type: tosca.nodes.Database + properties: + db_name: { get_input: db_name } + db_user: { get_input: db_user } + db_password: { get_input: db_pwd } + capabilities: + database_endpoint: + properties: + port: { get_input: db_port } + requirements: + - host: mysql_dbms + - database_endpoint: mysql_database + interfaces: + tosca.interfaces.node.Lifecycle: + configure: mysql_database_configure.sh + ''' + expectedmessage = ('Requirements of node template mysql_database ' + 'contain(s) unknown field: "database_endpoint", ' + 'refer to the TOSCA specs to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + def test_node_template_capabilities(self): + tpl_snippet = ''' + node_templates: + mysql_database: + type: tosca.nodes.Database + properties: + db_name: { get_input: db_name } + db_user: { get_input: db_user } + db_password: { get_input: db_pwd } + capabilities: + http_endpoint: + properties: + port: { get_input: db_port } + requirements: + - host: mysql_dbms + interfaces: + tosca.interfaces.node.Lifecycle: + configure: mysql_database_configure.sh + ''' + expectedmessage = ('Capabilities of node template mysql_database ' + 'contain(s) unknown field: "http_endpoint", ' + 'refer to the TOSCA specs to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + def test_node_template_properties(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_distribution: Fedora + os_version: 18 + ''' + expectedmessage = ('Properties of node template server is missing ' + 'required field: "[\'os_type\']".') + self._single_node_template_content_test(tpl_snippet, + MissingRequiredFieldError, + expectedmessage) + + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + os_image: F18_x86_64 + ''' + expectedmessage = ('Properties of node template server contain(s) ' + 'unknown field: "os_image", refer to the TOSCA ' + 'specs to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + def test_node_template_interfaces(self): + tpl_snippet = ''' + node_templates: + wordpress: + type: tosca.nodes.WebApplication.WordPress + requirements: + - host: webserver + - database_endpoint: mysql_database + interfaces: + tosca.interfaces.node.Lifecycles: + create: wordpress_install.sh + configure: + implementation: wordpress_configure.sh + input: + wp_db_name: { get_property: [ mysql_database, db_name ] } + wp_db_user: { get_property: [ mysql_database, db_user ] } + wp_db_password: { get_property: [ mysql_database, \ + db_password ] } + wp_db_port: { get_ref_property: [ database_endpoint, \ + database_endpoint, port ] } + ''' + expectedmessage = ('Interfaces of node template wordpress ' + 'contain(s) unknown field: ' + '"tosca.interfaces.node.Lifecycles", ' + 'refer to the TOSCA specs to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + tpl_snippet = ''' + node_templates: + wordpress: + type: tosca.nodes.WebApplication.WordPress + requirements: + - host: webserver + - database_endpoint: mysql_database + interfaces: + tosca.interfaces.node.Lifecycle: + create: wordpress_install.sh + config: + implementation: wordpress_configure.sh + input: + wp_db_name: { get_property: [ mysql_database, db_name ] } + wp_db_user: { get_property: [ mysql_database, db_user ] } + wp_db_password: { get_property: [ mysql_database, \ + db_password ] } + wp_db_port: { get_ref_property: [ database_endpoint, \ + database_endpoint, port ] } + ''' + expectedmessage = ('Interfaces of node template wordpress contain(s) ' + 'unknown field: "config", refer to the TOSCA specs' + ' to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) + + tpl_snippet = ''' + node_templates: + wordpress: + type: tosca.nodes.WebApplication.WordPress + requirements: + - host: webserver + - database_endpoint: mysql_database + interfaces: + tosca.interfaces.node.Lifecycle: + create: wordpress_install.sh + configure: + implementation: wordpress_configure.sh + inputs: + wp_db_name: { get_property: [ mysql_database, db_name ] } + wp_db_user: { get_property: [ mysql_database, db_user ] } + wp_db_password: { get_property: [ mysql_database, \ + db_password ] } + wp_db_port: { get_ref_property: [ database_endpoint, \ + database_endpoint, port ] } + ''' + expectedmessage = ('Interfaces of node template wordpress contain(s) ' + 'unknown field: "inputs", refer to the TOSCA specs' + ' to verify valid values.') + self._single_node_template_content_test(tpl_snippet, + UnknownFieldError, + expectedmessage) diff --git a/translator/toscalib/tosca_template.py b/translator/toscalib/tosca_template.py index eabffdb..a778912 100644 --- a/translator/toscalib/tosca_template.py +++ b/translator/toscalib/tosca_template.py @@ -16,13 +16,17 @@ import logging import os + +from translator.toscalib.common.exception import MissingRequiredFieldError +from translator.toscalib.common.exception import UnknownFieldError from translator.toscalib.nodetemplate import NodeTemplate from translator.toscalib.parameters import Input, Output from translator.toscalib.tpl_relationship_graph import ToscaGraph -from translator.toscalib.utils.gettextutils import _ + import translator.toscalib.utils.yamlparser +#Simple YAML definition A.4.1 Keynames SECTIONS = (VERSION, DESCRIPTION, IMPORTS, INPUTS, NODE_TEMPLATES, OUTPUTS) = \ ('tosca_definitions_version', 'description', 'imports', 'inputs', @@ -38,6 +42,7 @@ class ToscaTemplate(object): def __init__(self, path): self.tpl = YAML_LOADER(path) self.path = path + self._validate_field() self.version = self._tpl_version() self.description = self._tpl_description() self.inputs = self._inputs() @@ -49,9 +54,6 @@ class ToscaTemplate(object): inputs = [] for name, attrs in self._tpl_inputs().items(): input = Input(name, attrs) - if not isinstance(input.schema, dict): - raise ValueError(_("The input %(input)s has no attributes.") - % {'input': input}) input.validate() inputs.append(input) return inputs @@ -73,7 +75,7 @@ class ToscaTemplate(object): custom_types[name] = defintion nodetemplates = [] tpls = self._tpl_nodetemplates() - for name, value in tpls.items(): + for name in tpls: tpl = NodeTemplate(name, tpls, custom_types) tpl.validate() nodetemplates.append(tpl) @@ -82,7 +84,9 @@ class ToscaTemplate(object): def _outputs(self): outputs = [] for name, attrs in self._tpl_outputs().items(): - outputs.append(Output(name, attrs)) + output = Output(name, attrs) + output.validate() + outputs.append(output) return outputs def _tpl_version(self): @@ -103,3 +107,12 @@ class ToscaTemplate(object): def _tpl_outputs(self): return self.tpl[OUTPUTS] + + def _validate_field(self): + try: + self._tpl_version() + except KeyError: + raise MissingRequiredFieldError(what='Template', required=VERSION) + for name in self.tpl: + if name not in SECTIONS: + raise UnknownFieldError(what='Template', field=name) diff --git a/translator/toscalib/utils/yamlparser.py b/translator/toscalib/utils/yamlparser.py index 3d8e978..72bc03c 100644 --- a/translator/toscalib/utils/yamlparser.py +++ b/translator/toscalib/utils/yamlparser.py @@ -24,3 +24,14 @@ else: def load_yaml(path): with open(path) as f: return yaml.load(f.read(), Loader=yaml_loader) + + +def simple_parse(tmpl_str): + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + return tpl