Add input validation in substitution_mapping class
[parser.git] / tosca2heat / tosca-parser / toscaparser / tosca_template.py
1 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
2 #    not use this file except in compliance with the License. You may obtain
3 #    a copy of the License at
4 #
5 #         http://www.apache.org/licenses/LICENSE-2.0
6 #
7 #    Unless required by applicable law or agreed to in writing, software
8 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 #    License for the specific language governing permissions and limitations
11 #    under the License.
12
13
14 import logging
15 import os
16
17 from copy import deepcopy
18 from toscaparser.common.exception import ExceptionCollector
19 from toscaparser.common.exception import InvalidTemplateVersion
20 from toscaparser.common.exception import MissingRequiredFieldError
21 from toscaparser.common.exception import UnknownFieldError
22 from toscaparser.common.exception import ValidationError
23 from toscaparser.elements.entity_type import update_definitions
24 from toscaparser.extensions.exttools import ExtTools
25 import toscaparser.imports
26 from toscaparser.prereq.csar import CSAR
27 from toscaparser.repositories import Repository
28 from toscaparser.topology_template import TopologyTemplate
29 from toscaparser.tpl_relationship_graph import ToscaGraph
30 from toscaparser.utils.gettextutils import _
31 import toscaparser.utils.yamlparser
32
33
34 # TOSCA template key names
35 SECTIONS = (DEFINITION_VERSION, DEFAULT_NAMESPACE, TEMPLATE_NAME,
36             TOPOLOGY_TEMPLATE, TEMPLATE_AUTHOR, TEMPLATE_VERSION,
37             DESCRIPTION, IMPORTS, DSL_DEFINITIONS, NODE_TYPES,
38             RELATIONSHIP_TYPES, RELATIONSHIP_TEMPLATES,
39             CAPABILITY_TYPES, ARTIFACT_TYPES, DATA_TYPES,
40             POLICY_TYPES, GROUP_TYPES, REPOSITORIES) = \
41            ('tosca_definitions_version', 'tosca_default_namespace',
42             'template_name', 'topology_template', 'template_author',
43             'template_version', 'description', 'imports', 'dsl_definitions',
44             'node_types', 'relationship_types', 'relationship_templates',
45             'capability_types', 'artifact_types', 'data_types',
46             'policy_types', 'group_types', 'repositories')
47 # Sections that are specific to individual template definitions
48 SPECIAL_SECTIONS = (METADATA) = ('metadata')
49
50 log = logging.getLogger("tosca.model")
51
52 YAML_LOADER = toscaparser.utils.yamlparser.load_yaml
53
54
55 class ToscaTemplate(object):
56     exttools = ExtTools()
57
58     VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0']
59
60     VALID_TEMPLATE_VERSIONS.extend(exttools.get_versions())
61
62     ADDITIONAL_SECTIONS = {'tosca_simple_yaml_1_0': SPECIAL_SECTIONS}
63
64     ADDITIONAL_SECTIONS.update(exttools.get_sections())
65
66     '''Load the template data.'''
67     def __init__(self, path=None, parsed_params=None, a_file=True,
68                  yaml_dict_tpl=None, sub_mapped_node_template=None):
69         if sub_mapped_node_template is None:
70             ExceptionCollector.start()
71         self.a_file = a_file
72         self.input_path = None
73         self.path = None
74         self.tpl = None
75         self.sub_mapped_node_template = sub_mapped_node_template
76         self.nested_tosca_tpls_with_topology = {}
77         self.nested_tosca_templates_with_topology = []
78         if path:
79             self.input_path = path
80             self.path = self._get_path(path)
81             if self.path:
82                 self.tpl = YAML_LOADER(self.path, self.a_file)
83             if yaml_dict_tpl:
84                 msg = (_('Both path and yaml_dict_tpl arguments were '
85                          'provided. Using path and ignoring yaml_dict_tpl.'))
86                 log.info(msg)
87                 print(msg)
88         else:
89             if yaml_dict_tpl:
90                 self.tpl = yaml_dict_tpl
91             else:
92                 ExceptionCollector.appendException(
93                     ValueError(_('No path or yaml_dict_tpl was provided. '
94                                  'There is nothing to parse.')))
95
96         if self.tpl:
97             self.parsed_params = parsed_params
98             self._validate_field()
99             self.version = self._tpl_version()
100             self.relationship_types = self._tpl_relationship_types()
101             self.description = self._tpl_description()
102             self.topology_template = self._topology_template()
103             self.repositories = self._tpl_repositories()
104             if self.topology_template.tpl:
105                 self.inputs = self._inputs()
106                 self.relationship_templates = self._relationship_templates()
107                 self.nodetemplates = self._nodetemplates()
108                 self.outputs = self._outputs()
109                 self._handle_nested_tosca_templates_with_topology()
110                 self.graph = ToscaGraph(self.nodetemplates)
111
112         if sub_mapped_node_template is None:
113             ExceptionCollector.stop()
114         self.verify_template()
115
116     def _topology_template(self):
117         return TopologyTemplate(self._tpl_topology_template(),
118                                 self._get_all_custom_defs(),
119                                 self.relationship_types,
120                                 self.parsed_params,
121                                 self.sub_mapped_node_template)
122
123     def _inputs(self):
124         return self.topology_template.inputs
125
126     def _nodetemplates(self):
127         return self.topology_template.nodetemplates
128
129     def _relationship_templates(self):
130         return self.topology_template.relationship_templates
131
132     def _outputs(self):
133         return self.topology_template.outputs
134
135     def _tpl_version(self):
136         return self.tpl.get(DEFINITION_VERSION)
137
138     def _tpl_description(self):
139         desc = self.tpl.get(DESCRIPTION)
140         if desc:
141             return desc.rstrip()
142
143     def _tpl_imports(self):
144         return self.tpl.get(IMPORTS)
145
146     def _tpl_repositories(self):
147         repositories = self.tpl.get(REPOSITORIES)
148         reposit = []
149         if repositories:
150             for name, val in repositories.items():
151                 reposits = Repository(name, val)
152                 reposit.append(reposits)
153         return reposit
154
155     def _tpl_relationship_types(self):
156         return self._get_custom_types(RELATIONSHIP_TYPES)
157
158     def _tpl_relationship_templates(self):
159         topology_template = self._tpl_topology_template()
160         return topology_template.get(RELATIONSHIP_TEMPLATES)
161
162     def _tpl_topology_template(self):
163         return self.tpl.get(TOPOLOGY_TEMPLATE)
164
165     def _get_all_custom_defs(self, imports=None):
166         types = [IMPORTS, NODE_TYPES, CAPABILITY_TYPES, RELATIONSHIP_TYPES,
167                  DATA_TYPES, POLICY_TYPES, GROUP_TYPES]
168         custom_defs_final = {}
169         custom_defs = self._get_custom_types(types, imports)
170         if custom_defs:
171             custom_defs_final.update(custom_defs)
172             if custom_defs.get(IMPORTS):
173                 import_defs = self._get_all_custom_defs(
174                     custom_defs.get(IMPORTS))
175                 custom_defs_final.update(import_defs)
176
177         # As imports are not custom_types, removing from the dict
178         custom_defs_final.pop(IMPORTS, None)
179         return custom_defs_final
180
181     def _get_custom_types(self, type_definitions, imports=None):
182         """Handle custom types defined in imported template files
183
184         This method loads the custom type definitions referenced in "imports"
185         section of the TOSCA YAML template.
186         """
187
188         custom_defs = {}
189         type_defs = []
190         if not isinstance(type_definitions, list):
191             type_defs.append(type_definitions)
192         else:
193             type_defs = type_definitions
194
195         if not imports:
196             imports = self._tpl_imports()
197
198         if imports:
199             custom_service = \
200                 toscaparser.imports.ImportsLoader(imports, self.path,
201                                                   type_defs, self.tpl)
202
203             nested_tosca_tpls = custom_service.get_nested_tosca_tpls()
204             self._update_nested_tosca_tpls_with_topology(nested_tosca_tpls)
205
206             custom_defs = custom_service.get_custom_defs()
207             if not custom_defs:
208                 return
209
210         # Handle custom types defined in current template file
211         for type_def in type_defs:
212             if type_def != IMPORTS:
213                 inner_custom_types = self.tpl.get(type_def) or {}
214                 if inner_custom_types:
215                     custom_defs.update(inner_custom_types)
216         return custom_defs
217
218     def _update_nested_tosca_tpls_with_topology(self, nested_tosca_tpls):
219         for tpl in nested_tosca_tpls:
220             filename, tosca_tpl = list(tpl.items())[0]
221             if (tosca_tpl.get(TOPOLOGY_TEMPLATE) and
222                 filename not in list(
223                     self.nested_tosca_tpls_with_topology.keys())):
224                 self.nested_tosca_tpls_with_topology.update(tpl)
225
226     def _handle_nested_tosca_templates_with_topology(self):
227         for fname, tosca_tpl in self.nested_tosca_tpls_with_topology.items():
228             for nodetemplate in self.nodetemplates:
229                 if self._is_sub_mapped_node(nodetemplate, tosca_tpl):
230                     parsed_params = self._get_params_for_nested_template(
231                         nodetemplate)
232                     nested_template = ToscaTemplate(
233                         path=fname, parsed_params=parsed_params,
234                         yaml_dict_tpl=tosca_tpl,
235                         sub_mapped_node_template=nodetemplate)
236                     if nested_template.has_substitution_mappings():
237                         # Record the nested templates in top level template
238                         self.nested_tosca_templates_with_topology.\
239                             append(nested_template)
240                         # Set the substitution toscatemplate for mapped node
241                         nodetemplate.sub_mapping_tosca_template = \
242                             nested_template
243
244     def _validate_field(self):
245         version = self._tpl_version()
246         if not version:
247             ExceptionCollector.appendException(
248                 MissingRequiredFieldError(what='Template',
249                                           required=DEFINITION_VERSION))
250         else:
251             self._validate_version(version)
252             self.version = version
253
254         for name in self.tpl:
255             if (name not in SECTIONS and
256                name not in self.ADDITIONAL_SECTIONS.get(version, ())):
257                 ExceptionCollector.appendException(
258                     UnknownFieldError(what='Template', field=name))
259
260     def _validate_version(self, version):
261         if version not in self.VALID_TEMPLATE_VERSIONS:
262             ExceptionCollector.appendException(
263                 InvalidTemplateVersion(
264                     what=version,
265                     valid_versions=', '. join(self.VALID_TEMPLATE_VERSIONS)))
266         else:
267             if version != 'tosca_simple_yaml_1_0':
268                 update_definitions(version)
269
270     def _get_path(self, path):
271         if path.lower().endswith('.yaml'):
272             return path
273         elif path.lower().endswith(('.zip', '.csar')):
274             # a CSAR archive
275             csar = CSAR(path, self.a_file)
276             if csar.validate():
277                 csar.decompress()
278                 self.a_file = True  # the file has been decompressed locally
279                 return os.path.join(csar.temp_dir, csar.get_main_template())
280         else:
281             ExceptionCollector.appendException(
282                 ValueError(_('"%(path)s" is not a valid file.')
283                            % {'path': path}))
284
285     def verify_template(self):
286         if ExceptionCollector.exceptionsCaught():
287             if self.input_path:
288                 raise ValidationError(
289                     message=(_('\nThe input "%(path)s" failed validation with '
290                                'the following error(s): \n\n\t')
291                              % {'path': self.input_path}) +
292                     '\n\t'.join(ExceptionCollector.getExceptionsReport()))
293             else:
294                 raise ValidationError(
295                     message=_('\nThe pre-parsed input failed validation with '
296                               'the following error(s): \n\n\t') +
297                     '\n\t'.join(ExceptionCollector.getExceptionsReport()))
298         else:
299             if self.input_path:
300                 msg = (_('The input "%(path)s" successfully passed '
301                          'validation.') % {'path': self.input_path})
302             else:
303                 msg = _('The pre-parsed input successfully passed validation.')
304
305             log.info(msg)
306
307     def _is_sub_mapped_node(self, nodetemplate, tosca_tpl):
308         """Return True if the nodetemple is substituted."""
309         if (nodetemplate and not nodetemplate.substitution_mapped and
310                 self.get_sub_mapping_node_type(tosca_tpl) == nodetemplate.type
311                 and len(nodetemplate.interfaces) < 1):
312             return True
313         else:
314             return False
315
316     def _get_params_for_nested_template(self, nodetemplate):
317         """Return total params for nested_template."""
318         parsed_params = deepcopy(self.parsed_params) \
319             if self.parsed_params else {}
320         if nodetemplate:
321             for pname in nodetemplate.get_properties():
322                 parsed_params.update({pname:
323                                       nodetemplate.get_property_value(pname)})
324         return parsed_params
325
326     def get_sub_mapping_node_type(self, tosca_tpl):
327         """Return substitution mappings node type."""
328         if tosca_tpl:
329             return TopologyTemplate.get_sub_mapping_node_type(
330                 tosca_tpl.get(TOPOLOGY_TEMPLATE))
331
332     def has_substitution_mappings(self):
333         """Return True if the template has valid substitution mappings."""
334         return self.topology_template is not None and \
335             self.topology_template.substitution_mappings is not None
336
337     def has_nested_templates(self):
338         """Return True if the tosca template has nested templates."""
339         return self.nested_tosca_templates_with_topology is not None and \
340             len(self.nested_tosca_templates_with_topology) >= 1