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