From 24c71e28e8c5159bb505ec3890cd45a9e902c169 Mon Sep 17 00:00:00 2001 From: shangxdy Date: Thu, 21 Jul 2016 22:41:11 +0800 Subject: [PATCH] Implement substitution mappings Add validation for substitute_mappings and a test example. The substitution is responsibility of an orchestrator so work needs to be done in heat-translator to translate substitute_mapping in HOT. Partially example of substitute_mappings is given in the test/data/topology_template/subsystem.yaml Refer to the spec section 'Using node template substitution for model composition' and related sections to read detail on the substitute_mappings. The related heat-translator blueprint is https://blueprints.launchpad.net/heat-translator/+spec/ nested-templates Partially-Implements: blueprint tosca-substitution-mappings Co-Authored-By: Sahdev Zala Change-Id: I7baadef4f0b4c7cd426f710f4c74c198d02bc030 Signed-off-by: shangxdy --- toscaparser/elements/tosca_type_validation.py | 5 +- toscaparser/functions.py | 15 +- toscaparser/imports.py | 37 +++-- toscaparser/nodetemplate.py | 1 + toscaparser/substitution_mappings.py | 157 ++++++++++++++++++ .../topology_template/databasesubsystem.yaml | 90 ++++++++++ .../data/topology_template/definitions.yaml | 39 +++-- .../topology_template/queuingsubsystem.yaml | 75 +++++++++ .../tests/data/topology_template/system.yaml | 49 +++--- ...bsystem.yaml => transactionsubsystem.yaml} | 9 +- toscaparser/tests/test_topology_template.py | 8 +- toscaparser/topology_template.py | 31 +++- toscaparser/tosca_template.py | 68 +++++++- 13 files changed, 520 insertions(+), 64 deletions(-) create mode 100644 toscaparser/substitution_mappings.py create mode 100644 toscaparser/tests/data/topology_template/databasesubsystem.yaml create mode 100644 toscaparser/tests/data/topology_template/queuingsubsystem.yaml rename toscaparser/tests/data/topology_template/{subsystem.yaml => transactionsubsystem.yaml} (89%) diff --git a/toscaparser/elements/tosca_type_validation.py b/toscaparser/elements/tosca_type_validation.py index 16764bcf..82b0b46f 100644 --- a/toscaparser/elements/tosca_type_validation.py +++ b/toscaparser/elements/tosca_type_validation.py @@ -22,12 +22,13 @@ class TypeValidation(object): DSL_DEFINITIONS, NODE_TYPES, REPOSITORIES, DATA_TYPES, ARTIFACT_TYPES, GROUP_TYPES, RELATIONSHIP_TYPES, CAPABILITY_TYPES, - INTERFACE_TYPES, POLICY_TYPES) = \ + INTERFACE_TYPES, POLICY_TYPES, + TOPOLOGY_TEMPLATE) = \ ('tosca_definitions_version', 'description', 'imports', 'dsl_definitions', 'node_types', 'repositories', 'data_types', 'artifact_types', 'group_types', 'relationship_types', 'capability_types', - 'interface_types', 'policy_types') + 'interface_types', 'policy_types', 'topology_template') VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0'] exttools = ExtTools() VALID_TEMPLATE_VERSIONS.extend(exttools.get_versions()) diff --git a/toscaparser/functions.py b/toscaparser/functions.py index 1b644162..b0fd6ae2 100644 --- a/toscaparser/functions.py +++ b/toscaparser/functions.py @@ -724,11 +724,12 @@ def get_function(tosca_tpl, node_template, raw_function): parsing was unsuccessful. """ if is_function(raw_function): - func_name = list(raw_function.keys())[0] - if func_name in function_mappings: - func = function_mappings[func_name] - func_args = list(raw_function.values())[0] - if not isinstance(func_args, list): - func_args = [func_args] - return func(tosca_tpl, node_template, func_name, func_args) + if isinstance(raw_function, dict): + func_name = list(raw_function.keys())[0] + if func_name in function_mappings: + func = function_mappings[func_name] + func_args = list(raw_function.values())[0] + if not isinstance(func_args, list): + func_args = [func_args] + return func(tosca_tpl, node_template, func_name, func_args) return raw_function diff --git a/toscaparser/imports.py b/toscaparser/imports.py index 789a5440..451c952c 100644 --- a/toscaparser/imports.py +++ b/toscaparser/imports.py @@ -37,6 +37,7 @@ class ImportsLoader(object): tpl=None): self.importslist = importslist self.custom_defs = {} + self.nested_tosca_tpls = [] if not path and not tpl: msg = _('Input tosca template is not provided.') log.warning(msg) @@ -56,6 +57,9 @@ class ImportsLoader(object): def get_custom_defs(self): return self.custom_defs + def get_nested_tosca_tpls(self): + return self.nested_tosca_tpls + def _validate_and_load_imports(self): imports_names = set() @@ -77,8 +81,8 @@ class ImportsLoader(object): ValidationError(message=msg)) imports_names.add(import_name) - custom_type = self._load_import_template(import_name, - import_uri) + full_file_name, custom_type = self._load_import_template( + import_name, import_uri) namespace_prefix = None if isinstance(import_uri, dict): namespace_prefix = import_uri.get( @@ -87,13 +91,15 @@ class ImportsLoader(object): TypeValidation(custom_type, import_def) self._update_custom_def(custom_type, namespace_prefix) else: # old style of imports - custom_type = self._load_import_template(None, - import_def) + full_file_name, custom_type = self._load_import_template( + None, import_def) if custom_type: TypeValidation( custom_type, import_def) self._update_custom_def(custom_type, None) + self._update_nested_tosca_tpls(full_file_name, custom_type) + def _update_custom_def(self, custom_type, namespace_prefix): outer_custom_types = {} for type_def in self.type_definition_list: @@ -113,6 +119,11 @@ class ImportsLoader(object): else: self.custom_defs.update(outer_custom_types) + def _update_nested_tosca_tpls(self, full_file_name, custom_tpl): + if full_file_name and custom_tpl: + topo_tpl = {full_file_name: custom_tpl} + self.nested_tosca_tpls.append(topo_tpl) + def _validate_import_keys(self, import_name, import_uri_def): if self.FILE not in import_uri_def.keys(): log.warning(_('Missing keyname "file" in import "%(name)s".') @@ -173,10 +184,10 @@ class ImportsLoader(object): % {'import_name': import_name}) log.error(msg) ExceptionCollector.appendException(ValidationError(message=msg)) - return + return None, None if toscaparser.utils.urlutils.UrlUtils.validate_url(file_name): - return YAML_LOADER(file_name, False) + return file_name, YAML_LOADER(file_name, False) elif not repository: import_template = None if self.path: @@ -188,7 +199,7 @@ class ImportsLoader(object): % {'name': file_name, 'template': self.path}) log.error(msg) ExceptionCollector.appendException(ImportError(msg)) - return + return None, None import_template = toscaparser.utils.urlutils.UrlUtils.\ join_url(self.path, file_name) a_file = False @@ -231,7 +242,7 @@ class ImportsLoader(object): % {'name': file_name}) log.error(msg) ExceptionCollector.appendException(ImportError(msg)) - return + return None, None if not import_template: log.error(_('Import "%(name)s" is not valid.') % @@ -239,14 +250,14 @@ class ImportsLoader(object): ExceptionCollector.appendException( ImportError(_('Import "%s" is not valid.') % import_uri_def)) - return - return YAML_LOADER(import_template, a_file) + return None, None + return import_template, YAML_LOADER(import_template, a_file) if short_import_notation: log.error(_('Import "%(name)s" is not valid.') % import_uri_def) ExceptionCollector.appendException( ImportError(_('Import "%s" is not valid.') % import_uri_def)) - return + return None, None full_url = "" if repository: @@ -264,10 +275,10 @@ class ImportsLoader(object): % {'n_uri': repository, 'tpl': import_name}) log.error(msg) ExceptionCollector.appendException(ImportError(msg)) - return + return None, None if toscaparser.utils.urlutils.UrlUtils.validate_url(full_url): - return YAML_LOADER(full_url, False) + return full_url, YAML_LOADER(full_url, False) else: msg = (_('repository url "%(n_uri)s" is not valid in import ' 'definition "%(tpl)s".') diff --git a/toscaparser/nodetemplate.py b/toscaparser/nodetemplate.py index d969d51b..5ea1c273 100644 --- a/toscaparser/nodetemplate.py +++ b/toscaparser/nodetemplate.py @@ -48,6 +48,7 @@ class NodeTemplate(EntityTemplate): self.available_rel_tpls = available_rel_tpls self.available_rel_types = available_rel_types self._relationships = {} + self.sub_mapping_tosca_template = None @property def relationships(self): diff --git a/toscaparser/substitution_mappings.py b/toscaparser/substitution_mappings.py new file mode 100644 index 00000000..80d3f9d4 --- /dev/null +++ b/toscaparser/substitution_mappings.py @@ -0,0 +1,157 @@ +# 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 InvalidNodeTypeError +from toscaparser.common.exception import MissingRequiredFieldError +from toscaparser.common.exception import UnknownFieldError + + +log = logging.getLogger('tosca') + + +class SubstitutionMappings(object): + '''SubstitutionMappings class declaration + + SubstitutionMappings exports the topology template as an + implementation of a Node type. + ''' + + SECTIONS = (NODE_TYPE, REQUIREMENTS, CAPABILITIES) = \ + ('node_type', 'requirements', 'capabilities') + + def __init__(self, sub_mapping_def, nodetemplates, inputs, outputs, + sub_mapped_node_template, custom_defs): + self.nodetemplates = nodetemplates + self.sub_mapping_def = sub_mapping_def + self.inputs = inputs or [] + self.outputs = outputs or [] + self.sub_mapped_node_template = sub_mapped_node_template + self.custom_defs = custom_defs or {} + self._validate() + + self._capabilities = None + self._requirements = None + + @property + def type(self): + if self.sub_mapping_def: + return self.sub_mapping_def['node_type'] + + @classmethod + def get_node_type(cls, sub_mapping_def): + if isinstance(sub_mapping_def, dict): + return sub_mapping_def.get(cls.NODE_TYPE) + + @property + def node_type(self): + return self.sub_mapping_def.get(self.NODE_TYPE) + + @property + def capabilities(self): + return self.sub_mapping_def.get(self.CAPABILITIES) + + @property + def requirements(self): + return self.sub_mapping_def.get(self.REQUIREMENTS) + + def _validate(self): + self._validate_keys() + self._validate_type() + self._validate_inputs() + self._validate_capabilities() + self._validate_requirements() + self._validate_outputs() + + def _validate_keys(self): + """validate the keys of substitution mappings.""" + for key in self.sub_mapping_def.keys(): + if key not in self.SECTIONS: + ExceptionCollector.appendException( + UnknownFieldError(what='SubstitutionMappings', + field=key)) + + def _validate_type(self): + """validate the node_type of substitution mappings.""" + node_type = self.sub_mapping_def.get(self.NODE_TYPE) + if not node_type: + ExceptionCollector.appendException( + MissingRequiredFieldError( + what=_('SubstitutionMappings used in topology_template'), + required=self.NODE_TYPE)) + + node_type_def = self.custom_defs.get(node_type) + if not node_type_def: + ExceptionCollector.appendException( + InvalidNodeTypeError(what=node_type_def)) + + def _validate_inputs(self): + """validate the inputs of substitution mappings.""" + + # The inputs in service template which defines substutition mappings + # must be in properties of node template which is mapped or provide + # defualt value. Currently the input.name is not restrict to be the + # same as properte's name in specification, but they should be equal + # for current implementation. + property_names = list(self.sub_mapped_node_template + .get_properties().keys() + if self.sub_mapped_node_template else []) + for input in self.inputs: + if input.name not in property_names and input.default is None: + ExceptionCollector.appendException( + UnknownFieldError(what='SubstitutionMappings', + field=input.name)) + + def _validate_capabilities(self): + """validate the capabilities of substitution mappings.""" + + # The capabilites must be in node template wchich be mapped. + tpls_capabilities = self.sub_mapping_def.get(self.CAPABILITIES) + node_capabiliteys = self.sub_mapped_node_template.get_capabilities() \ + if self.sub_mapped_node_template else None + for cap in node_capabiliteys.keys() if node_capabiliteys else []: + if (tpls_capabilities and + cap not in list(tpls_capabilities.keys())): + pass + # ExceptionCollector.appendException( + # UnknownFieldError(what='SubstitutionMappings', + # field=cap)) + + def _validate_requirements(self): + """validate the requirements of substitution mappings.""" + + # The requirements must be in node template wchich be mapped. + tpls_requirements = self.sub_mapping_def.get(self.REQUIREMENTS) + node_requirements = self.sub_mapped_node_template.requirements \ + if self.sub_mapped_node_template else None + for req in node_requirements if node_requirements else []: + if (tpls_requirements and + req not in list(tpls_requirements.keys())): + pass + # ExceptionCollector.appendException( + # UnknownFieldError(what='SubstitutionMappings', + # field=req)) + + def _validate_outputs(self): + """validate the outputs of substitution mappings.""" + pass + # The outputs in service template which defines substutition mappings + # must be in atrributes of node template wchich be mapped. + # outputs_names = self.sub_mapped_node_template.get_properties(). + # keys() if self.sub_mapped_node_template else None + # for name in outputs_names: + # if name not in [output.name for input in self.outputs]: + # ExceptionCollector.appendException( + # UnknownFieldError(what='SubstitutionMappings', + # field=name)) diff --git a/toscaparser/tests/data/topology_template/databasesubsystem.yaml b/toscaparser/tests/data/topology_template/databasesubsystem.yaml new file mode 100644 index 00000000..ebf18564 --- /dev/null +++ b/toscaparser/tests/data/topology_template/databasesubsystem.yaml @@ -0,0 +1,90 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: > + Database subsystem, which is service template with topology_template, + act as a nested system inside another system and also act as stand + alone service template. + +imports: + - definitions.yaml + +topology_template: + description: Template of a database including its hosting stack. + + inputs: + user: + type: string + description: the user name of database. + default: test + port: + type: integer + description: the port of database. + default: 3306 + name: + type: string + description: the name of database. + default: test + my_cpus: + type: integer + description: Number of CPUs for the server. + default: 2 + constraints: + - valid_values: [ 1, 2, 4, 8 ] + + substitution_mappings: + node_type: example.DatabaseSubsystem + capabilities: + database_endpoint: [ db_app, database_endpoint ] + + node_templates: + db_app: + type: tosca.nodes.Database + properties: + user: { get_input: user } + port: { get_input: port } + name: { get_input: name } + capabilities: + database_endpoint: + properties: + port: 1234 + requirements: + - host: + node: dbms + + dbms: + type: tosca.nodes.DBMS + properties: + port: 3306 + root_password: 123456789 + requirements: + - host: + node: server + + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + disk_size: 10 GB + num_cpus: { get_input: my_cpus } + mem_size: 4096 MB + os: + properties: + architecture: x86_64 + type: Linux + distribution: Ubuntu + version: 14.04 + + outputs: + receiver_ip: + description: private IP address of the database application + value: { get_attribute: [ server, private_address ] } +# It seems current _process_intrisic_function can not handle more than 2 arguments, save it for later +# receiver_port: +# description: Port of the message receiver endpoint +# value: { get_attribute: [ app, data_endpoint, port_name ] } + + groups: + dbserver_group: + members: [ dbms, server ] + type: tosca.groups.Root diff --git a/toscaparser/tests/data/topology_template/definitions.yaml b/toscaparser/tests/data/topology_template/definitions.yaml index cfa0614d..77829c6f 100644 --- a/toscaparser/tests/data/topology_template/definitions.yaml +++ b/toscaparser/tests/data/topology_template/definitions.yaml @@ -1,6 +1,28 @@ tosca_definitions_version: tosca_simple_yaml_1_0 node_types: + example.QueuingSubsystem: + derived_from: tosca.nodes.SoftwareComponent + properties: + server_ip: + type: string + server_port: + type: integer + attributes: + server_ip: + type: string + server_port: + type: integer + requirements: + - receiver1: + node: example.TransactionSubsystem + capability: example.capabilities.Receiver + relationship: tosca.relationships.ConnectsTo + - receiver2: + node: example.TransactionSubsystem + capability: example.capabilities.Receiver + relationship: tosca.relationships.ConnectsTo + example.TransactionSubsystem: properties: mq_server_ip: @@ -17,18 +39,8 @@ node_types: type: example.capabilities.Receiver requirements: - database_endpoint: - capability: tosca.capabilities.Endpoint.Database node: tosca.nodes.Database - relationship: tosca.relationships.ConnectsTo - - example.QueuingSubsystem: - derived_from: tosca.nodes.SoftwareComponent - requirements: - - receiver1: - node: example.TransactionSubsystem - relationship: tosca.relationships.ConnectsTo - - receiver2: - node: example.TransactionSubsystem + capability: tosca.capabilities.Endpoint.Database relationship: tosca.relationships.ConnectsTo example.DatabaseSubsystem: @@ -44,6 +56,11 @@ node_types: capabilities: message_receiver: type: example.capabilities.Receiver + requirements: + - database: + node: tosca.nodes.Database + capability: tosca.capabilities.Endpoint.Database + relationship: tosca.relationships.ConnectsTo capability_types: example.capabilities.Receiver: diff --git a/toscaparser/tests/data/topology_template/queuingsubsystem.yaml b/toscaparser/tests/data/topology_template/queuingsubsystem.yaml new file mode 100644 index 00000000..76fa7e26 --- /dev/null +++ b/toscaparser/tests/data/topology_template/queuingsubsystem.yaml @@ -0,0 +1,75 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: > + Queue subsystem, which is service template with topology_template, + act as a nested system inside another system and also act as stand + alone service template. + +imports: + - definitions.yaml + +topology_template: + description: Template of a database including its hosting stack. + + inputs: + server_ip: + type: string + description: IP address of the message queuing server to receive messages from. + default: 127.0.0.1 + server_port: + type: integer + description: Port to be used for receiving messages. + default: 8080 + my_cpus: + type: integer + description: Number of CPUs for the server. + default: 2 + constraints: + - valid_values: [ 1, 2, 4, 8 ] + + substitution_mappings: + node_type: example.QueuingSubsystem + # capabilities: + # message_receiver: [ app, message_receiver ] + requirements: + receiver1: [ tran_app, receiver1 ] + receiver2: [ tran_app, receiver2 ] + + node_templates: + tran_app: + type: example.QueuingSubsystem + properties: + server_ip: { get_input: server_ip } + server_port: { get_input: server_port } + requirements: + - host: + node: server + + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + disk_size: 10 GB + num_cpus: { get_input: my_cpus } + mem_size: 4096 MB + os: + properties: + architecture: x86_64 + type: Linux + distribution: Ubuntu + version: 14.04 + + outputs: + receiver_ip: + description: private IP address of the message receiver application + value: { get_attribute: [ server, private_address ] } +# It seems current _process_intrisic_function can not handle more than 2 arguments, save it for later +# receiver_port: +# description: Port of the message receiver endpoint +# value: { get_attribute: [ app, data_endpoint, port_name ] } + + groups: + tran_server_group: + members: [ tran_app, server ] + type: tosca.groups.Root diff --git a/toscaparser/tests/data/topology_template/system.yaml b/toscaparser/tests/data/topology_template/system.yaml index 2d459aa5..3c946f3b 100644 --- a/toscaparser/tests/data/topology_template/system.yaml +++ b/toscaparser/tests/data/topology_template/system.yaml @@ -1,19 +1,34 @@ tosca_definitions_version: tosca_simple_yaml_1_0 imports: - - definitions.yaml + - queuingsubsystem.yaml + - transactionsubsystem.yaml + - databasesubsystem.yaml topology_template: description: Template of online transaction processing service. + inputs: + mq_server_ip: + type: string + default: 127.0.0.1 + description: IP address of the message queuing server to receive messages from. + mq_server_port: + type: integer + default1: 8080 + description: Port to be used for receiving messages. + node_templates: mq: type: example.QueuingSubsystem # properties: - # to be updated when substitution_mapping is implemented + # to be updated when substitution_mapping is validated later + properties: + server_ip: { get_input: mq_server_ip } + server_port: { get_input: mq_server_port } # capabilities: # message_queue_endpoint: - # to be updated when substitution_mapping is implemented + # to be updated when substitution_mapping is validated later requirements: - receiver1: trans1 - receiver2: trans2 @@ -21,37 +36,33 @@ topology_template: trans1: type: example.TransactionSubsystem properties: - # TODO to be updated when substitution_mapping is implemented - # mq_server_ip: { get_attribute: [ mq, server_ip ] } - # for now, we will use the loopback address to avoid errors as - # this property is required in the schema - mq_server_ip: 127.0.0.1 - receiver_port: 8080 + # mq_server_ip: 127.0.0.1 + mq_server_ip: { get_attribute: [ mq, server_ip ] } + # receiver_port: 8080 + receiver_port: { get_attribute: [ mq, server_port ] } # capabilities: # message_receiver: - # to be updated when substitution_mapping is implemented + # to be updated when substitution_mapping is validated later requirements: - database_endpoint: dbsys trans2: type: example.TransactionSubsystem properties: - # TODO to be updated when substitution_mapping is implemented - # mq_server_ip: { get_attribute: [ mq, server_ip ] } - # for now, we will use the loopback address to avoid errors as - # this property is required in the schema - mq_server_ip: 127.0.0.1 - receiver_port: 8080 + # mq_server_ip: 127.0.0.1 + mq_server_ip: { get_attribute: [ mq, server_ip ] } + # receiver_port: 8080 + receiver_port: { get_attribute: [ mq, server_port ] } # capabilities: # message_receiver: - # to be updated when substitution_mapping is implemented + # to be updated when substitution_mapping is validated later requirements: - database_endpoint: dbsys dbsys: type: example.DatabaseSubsystem # properties: - # to be updated when substitution_mapping is implemented + # to be updated when substitution_mapping is validated later # capabilities: # database_endpoint: - # to be updated when substitution_mapping is implemented \ No newline at end of file + # to be updated when substitution_mapping is validated later diff --git a/toscaparser/tests/data/topology_template/subsystem.yaml b/toscaparser/tests/data/topology_template/transactionsubsystem.yaml similarity index 89% rename from toscaparser/tests/data/topology_template/subsystem.yaml rename to toscaparser/tests/data/topology_template/transactionsubsystem.yaml index b27e6981..0f145a3e 100644 --- a/toscaparser/tests/data/topology_template/subsystem.yaml +++ b/toscaparser/tests/data/topology_template/transactionsubsystem.yaml @@ -1,7 +1,9 @@ tosca_definitions_version: tosca_simple_yaml_1_0 description: > - Service template with topology_template, act as a nested system inside another system. + Transaction subsystem, which is service template with topology_template, + act as a nested system inside another system and also act as stand + alone service template. imports: - definitions.yaml @@ -13,12 +15,15 @@ topology_template: mq_server_ip: type: string description: IP address of the message queuing server to receive messages from. + default: 127.0.0.1 receiver_port: - type: string + type: integer description: Port to be used for receiving messages. + default: 8080 my_cpus: type: integer description: Number of CPUs for the server. + default: 2 constraints: - valid_values: [ 1, 2, 4, 8 ] diff --git a/toscaparser/tests/test_topology_template.py b/toscaparser/tests/test_topology_template.py index 0f1a33ea..6e3eb622 100644 --- a/toscaparser/tests/test_topology_template.py +++ b/toscaparser/tests/test_topology_template.py @@ -27,7 +27,7 @@ class TopologyTemplateTest(TestCase): '''TOSCA template.''' self.tosca_tpl_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), - "data/topology_template/subsystem.yaml") + "data/topology_template/transactionsubsystem.yaml") self.tpl = YAML_LOADER(self.tosca_tpl_path) self.topo_tpl = self.tpl.get('topology_template') self.imports = self.tpl.get('imports') @@ -157,4 +157,8 @@ class TopologyTemplateTest(TestCase): tpl_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/topology_template/system.yaml") - self.assertIsNotNone(ToscaTemplate(tpl_path)) + system_tosca_template = ToscaTemplate(tpl_path) + self.assertIsNotNone(system_tosca_template) + self.assertEqual( + len(system_tosca_template. + nested_tosca_templates_with_topology), 4) diff --git a/toscaparser/topology_template.py b/toscaparser/topology_template.py index b22a4b89..d7bd53b9 100644 --- a/toscaparser/topology_template.py +++ b/toscaparser/topology_template.py @@ -22,6 +22,7 @@ from toscaparser.parameters import Input from toscaparser.parameters import Output from toscaparser.policy import Policy from toscaparser.relationship_template import RelationshipTemplate +from toscaparser.substitution_mappings import SubstitutionMappings from toscaparser.tpl_relationship_graph import ToscaGraph from toscaparser.utils.gettextutils import _ @@ -41,8 +42,10 @@ class TopologyTemplate(object): '''Load the template data.''' def __init__(self, template, custom_defs, - rel_types=None, parsed_params=None): + rel_types=None, parsed_params=None, + sub_mapped_node_template=None): self.tpl = template + self.sub_mapped_node_template = sub_mapped_node_template if self.tpl: self.custom_defs = custom_defs self.rel_types = rel_types @@ -58,6 +61,7 @@ class TopologyTemplate(object): self.groups = self._groups() self.policies = self._policies() self._process_intrinsic_functions() + self.substitution_mappings = self._substitution_mappings() def _inputs(self): inputs = [] @@ -105,7 +109,15 @@ class TopologyTemplate(object): return outputs def _substitution_mappings(self): - pass + tpl_substitution_mapping = self._tpl_substitution_mappings() + # if tpl_substitution_mapping and self.sub_mapped_node_template: + if tpl_substitution_mapping: + return SubstitutionMappings(tpl_substitution_mapping, + self.nodetemplates, + self.inputs, + self.outputs, + self.sub_mapped_node_template, + self.custom_defs) def _policies(self): policies = [] @@ -177,13 +189,16 @@ class TopologyTemplate(object): # topology template can act like node template # it is exposed by substitution_mappings. def nodetype(self): - pass + return self.substitution_mappings.node_type \ + if self.substitution_mappings else None def capabilities(self): - pass + return self.substitution_mappings.capabilities \ + if self.substitution_mappings else None def requirements(self): - pass + return self.substitution_mappings.requirements \ + if self.substitution_mappings else None def _tpl_description(self): description = self.tpl.get(DESCRIPTION) @@ -278,3 +293,9 @@ class TopologyTemplate(object): func = functions.get_function(self, self.outputs, output.value) if isinstance(func, functions.GetAttribute): output.attrs[output.VALUE] = func + + @classmethod + def get_sub_mapping_node_type(cls, topology_tpl): + if topology_tpl and isinstance(topology_tpl, dict): + submap_tpl = topology_tpl.get(SUBSTITUION_MAPPINGS) + return SubstitutionMappings.get_node_type(submap_tpl) diff --git a/toscaparser/tosca_template.py b/toscaparser/tosca_template.py index 2a731825..633b1e59 100644 --- a/toscaparser/tosca_template.py +++ b/toscaparser/tosca_template.py @@ -65,11 +65,14 @@ class ToscaTemplate(object): '''Load the template data.''' def __init__(self, path=None, parsed_params=None, a_file=True, yaml_dict_tpl=None): + ExceptionCollector.start() self.a_file = a_file self.input_path = None self.path = None self.tpl = None + self.nested_tosca_tpls_with_topology = {} + self.nested_tosca_templates_with_topology = [] if path: self.input_path = path self.path = self._get_path(path) @@ -101,6 +104,7 @@ class ToscaTemplate(object): self.relationship_templates = self._relationship_templates() self.nodetemplates = self._nodetemplates() self.outputs = self._outputs() + self._handle_nested_tosca_templates_with_topology() self.graph = ToscaGraph(self.nodetemplates) ExceptionCollector.stop() @@ -110,7 +114,8 @@ class ToscaTemplate(object): return TopologyTemplate(self._tpl_topology_template(), self._get_all_custom_defs(), self.relationship_types, - self.parsed_params) + self.parsed_params, + None) def _inputs(self): return self.topology_template.inputs @@ -188,9 +193,14 @@ class ToscaTemplate(object): imports = self._tpl_imports() if imports: - custom_defs = toscaparser.imports.\ + custom_service = toscaparser.imports.\ ImportsLoader(imports, self.path, - type_defs, self.tpl).get_custom_defs() + type_defs, self.tpl) + + nested_tosca_tpls = custom_service.get_nested_tosca_tpls() + self._update_nested_tosca_tpls_with_topology(nested_tosca_tpls) + + custom_defs = custom_service.get_custom_defs() if not custom_defs: return @@ -202,6 +212,33 @@ class ToscaTemplate(object): custom_defs.update(inner_custom_types) return custom_defs + def _update_nested_tosca_tpls_with_topology(self, nested_tosca_tpls): + for tpl in nested_tosca_tpls: + filename, tosca_tpl = list(tpl.items())[0] + if (tosca_tpl.get(TOPOLOGY_TEMPLATE) and + filename not in list( + self.nested_tosca_tpls_with_topology.keys())): + self.nested_tosca_tpls_with_topology.update(tpl) + + def _handle_nested_tosca_templates_with_topology(self): + for fname, tosca_tpl in self.nested_tosca_tpls_with_topology.items(): + for nodetemplate in self.nodetemplates: + if self._is_sub_mapped_node(nodetemplate, tosca_tpl): + topology_tpl = tosca_tpl.get(TOPOLOGY_TEMPLATE) + topology_with_sub_mapping = TopologyTemplate( + topology_tpl, + self._get_all_custom_defs(), + self.relationship_types, + self.parsed_params, + nodetemplate) + if topology_with_sub_mapping.substitution_mappings: + # Record nested topo templates in top level template + self.nested_tosca_templates_with_topology.\ + append(topology_with_sub_mapping) + # Set substitution mapping object for mapped node + nodetemplate.sub_mapping_tosca_template = \ + topology_with_sub_mapping.substitution_mappings + def _validate_field(self): version = self._tpl_version() if not version: @@ -264,3 +301,28 @@ class ToscaTemplate(object): msg = _('The pre-parsed input successfully passed validation.') log.info(msg) + + def _is_sub_mapped_node(self, nodetemplate, tosca_tpl): + """Return True if the nodetemple is substituted.""" + if (nodetemplate and not nodetemplate.sub_mapping_tosca_template and + self.get_sub_mapping_node_type(tosca_tpl) == nodetemplate.type + and len(nodetemplate.interfaces) < 1): + return True + else: + return False + + def get_sub_mapping_node_type(self, tosca_tpl): + """Return substitution mappings node type.""" + if tosca_tpl: + return TopologyTemplate.get_sub_mapping_node_type( + tosca_tpl.get(TOPOLOGY_TEMPLATE)) + + def has_substitution_mappings(self): + """Return True if the template has valid substitution mappings.""" + return self.topology_template is not None and \ + self.topology_template.substitution_mappings is not None + + def has_nested_templates(self): + """Return True if the tosca template has nested templates.""" + return self.nested_tosca_templates_with_topology is not None and \ + len(self.nested_tosca_templates_with_topology) >= 1