netsted template validate type error
[parser.git] / tosca2heat / tosca-parser / toscaparser / functions.py
1 #
2 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
3 #    not use this file except in compliance with the License. You may obtain
4 #    a copy of the License at
5 #
6 #         http://www.apache.org/licenses/LICENSE-2.0
7 #
8 #    Unless required by applicable law or agreed to in writing, software
9 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11 #    License for the specific language governing permissions and limitations
12 #    under the License.
13
14
15 import abc
16 import six
17
18 from toscaparser.common.exception import ExceptionCollector
19 from toscaparser.common.exception import UnknownInputError
20 from toscaparser.dataentity import DataEntity
21 from toscaparser.elements.entity_type import EntityType
22 from toscaparser.elements.relationshiptype import RelationshipType
23 from toscaparser.utils.gettextutils import _
24
25
26 GET_PROPERTY = 'get_property'
27 GET_ATTRIBUTE = 'get_attribute'
28 GET_INPUT = 'get_input'
29 CONCAT = 'concat'
30
31 SELF = 'SELF'
32 HOST = 'HOST'
33 TARGET = 'TARGET'
34 SOURCE = 'SOURCE'
35
36 HOSTED_ON = 'tosca.relationships.HostedOn'
37
38
39 @six.add_metaclass(abc.ABCMeta)
40 class Function(object):
41     """An abstract type for representing a Tosca template function."""
42
43     def __init__(self, tosca_tpl, context, name, args):
44         self.tosca_tpl = tosca_tpl
45         self.context = context
46         self.name = name
47         self.args = args
48         self.validate()
49
50     @abc.abstractmethod
51     def result(self):
52         """Invokes the function and returns its result
53
54         Some methods invocation may only be relevant on runtime (for example,
55         getting runtime properties) and therefore its the responsibility of
56         the orchestrator/translator to take care of such functions invocation.
57
58         :return: Function invocation result.
59         """
60         return {self.name: self.args}
61
62     @abc.abstractmethod
63     def validate(self):
64         """Validates function arguments."""
65         pass
66
67
68 class GetInput(Function):
69     """Get a property value declared within the input of the service template.
70
71     Arguments:
72
73     * Input name.
74
75     Example:
76
77     * get_input: port
78     """
79
80     def validate(self):
81         if len(self.args) != 1:
82             ExceptionCollector.appendException(
83                 ValueError(_(
84                     'Expected one argument for function "get_input" but '
85                     'received "%s".') % self.args))
86         inputs = [input.name for input in self.tosca_tpl.inputs]
87         if self.args[0] not in inputs:
88             ExceptionCollector.appendException(
89                 UnknownInputError(input_name=self.args[0]))
90
91     def result(self):
92         if self.tosca_tpl.parsed_params and \
93            self.input_name in self.tosca_tpl.parsed_params:
94             return DataEntity.validate_datatype(
95                 self.tosca_tpl.tpl['inputs'][self.input_name]['type'],
96                 self.tosca_tpl.parsed_params[self.input_name])
97
98         input = [input_def for input_def in self.tosca_tpl.inputs
99                  if self.input_name == input_def.name][0]
100         return input.default
101
102     @property
103     def input_name(self):
104         return self.args[0]
105
106
107 class GetAttribute(Function):
108     """Get an attribute value of an entity defined in the service template
109
110     Node template attributes values are set in runtime and therefore its the
111     responsibility of the Tosca engine to implement the evaluation of
112     get_attribute functions.
113
114     Arguments:
115
116     * Node template name | HOST.
117     * Attribute name.
118
119     If the HOST keyword is passed as the node template name argument the
120     function will search each node template along the HostedOn relationship
121     chain until a node which contains the attribute is found.
122
123     Examples:
124
125     * { get_attribute: [ server, private_address ] }
126     * { get_attribute: [ HOST, private_address ] }
127     * { get_attribute: [ HOST, private_address, 0 ] }
128     """
129
130     def validate(self):
131         if len(self.args) != 2 and len(self.args) != 3:
132             ExceptionCollector.appendException(
133                 ValueError(_('Illegal arguments for function "{0}". Expected '
134                              'arguments: "node-template-name", '
135                              '"attribute-name"').format(GET_ATTRIBUTE)))
136         node_tpl = self._find_node_template_containing_attribute()
137         if len(self.args) > 2:
138             # Currently we only check the first level
139             attrs_def = node_tpl.type_definition.get_attributes_def()
140             attr_def = attrs_def[self.attribute_name]
141             if attr_def.schema['type'] == "list":
142                 if not isinstance(self.args[2], int):
143                     ExceptionCollector.appendException(
144                         ValueError(_('Illegal arguments for function "{0}". '
145                                      'Third argument must be a positive'
146                                      ' integer') .format(GET_ATTRIBUTE)))
147             elif attr_def.schema['type'] != "map":
148                 ExceptionCollector.appendException(
149                     ValueError(_('Illegal arguments for function "{0}". '
150                                  'Expected arguments: "node-template-name", '
151                                  '"attribute-name"').format(GET_ATTRIBUTE)))
152
153     def result(self):
154         return self
155
156     def get_referenced_node_template(self):
157         """Gets the NodeTemplate instance the get_attribute function refers to.
158
159         If HOST keyword was used as the node template argument, the node
160         template which contains the attribute along the HostedOn relationship
161         chain will be returned.
162         """
163         return self._find_node_template_containing_attribute()
164
165     def _find_node_template_containing_attribute(self):
166         if self.node_template_name == HOST:
167             # Currently this is the only way to tell whether the function
168             # is used within the outputs section of the TOSCA template.
169             if isinstance(self.context, list):
170                 ExceptionCollector.appendException(
171                     ValueError(_(
172                         '"get_attribute: [ HOST, ... ]" is not allowed in '
173                         '"outputs" section of the TOSCA template.')))
174                 return
175             node_tpl = self._find_host_containing_attribute()
176             if not node_tpl:
177                 ExceptionCollector.appendException(
178                     ValueError(_(
179                         '"get_attribute: [ HOST, ... ]" was used in node '
180                         'template "{0}" but "{1}" was not found in '
181                         'the relationship chain.').format(self.context.name,
182                                                           HOSTED_ON)))
183         else:
184             node_tpl = self._find_node_template(self.args[0])
185         if node_tpl and \
186             not self._attribute_exists_in_type(node_tpl.type_definition):
187             ExceptionCollector.appendException(
188                 KeyError(_('Attribute "%(att)s" was not found in node '
189                            'template "%(ntpl)s".') %
190                          {'att': self.attribute_name,
191                           'ntpl': node_tpl.name}))
192         return node_tpl
193
194     def _attribute_exists_in_type(self, type_definition):
195         attrs_def = type_definition.get_attributes_def()
196         found = [attrs_def[self.attribute_name]] \
197             if self.attribute_name in attrs_def else []
198         return len(found) == 1
199
200     def _find_host_containing_attribute(self, node_template_name=SELF):
201         node_template = self._find_node_template(node_template_name)
202         if node_template:
203             hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON]
204             for r in node_template.requirements:
205                 for requirement, target_name in r.items():
206                     target_node = self._find_node_template(target_name)
207                     target_type = target_node.type_definition
208                     for capability in target_type.get_capabilities_objects():
209                         if capability.type in \
210                             hosted_on_rel['valid_target_types']:
211                             if self._attribute_exists_in_type(target_type):
212                                 return target_node
213                             return self._find_host_containing_attribute(
214                                 target_name)
215
216     def _find_node_template(self, node_template_name):
217         if node_template_name == TARGET:
218             if not isinstance(self.context.type_definition, RelationshipType):
219                 ExceptionCollector.appendException(
220                     KeyError(_('"TARGET" keyword can only be used in context'
221                                ' to "Relationships" target node')))
222                 return
223             return self.context.target
224         if node_template_name == SOURCE:
225             if not isinstance(self.context.type_definition, RelationshipType):
226                 ExceptionCollector.appendException(
227                     KeyError(_('"SOURCE" keyword can only be used in context'
228                                ' to "Relationships" source node')))
229                 return
230             return self.context.source
231         name = self.context.name \
232             if node_template_name == SELF and \
233             not isinstance(self.context, list) \
234             else node_template_name
235         for node_template in self.tosca_tpl.nodetemplates:
236             if node_template.name == name:
237                 return node_template
238         ExceptionCollector.appendException(
239             KeyError(_(
240                 'Node template "{0}" was not found.'
241                 ).format(node_template_name)))
242
243     @property
244     def node_template_name(self):
245         return self.args[0]
246
247     @property
248     def attribute_name(self):
249         return self.args[1]
250
251
252 class GetProperty(Function):
253     """Get a property value of an entity defined in the same service template.
254
255     Arguments:
256
257     * Node template name | SELF | HOST | SOURCE | TARGET.
258     * Requirement or capability name (optional).
259     * Property name.
260
261     If requirement or capability name is specified, the behavior is as follows:
262     The req or cap name is first looked up in the specified node template's
263     requirements.
264     If found, it would search for a matching capability
265     of an other node template and get its property as specified in function
266     arguments.
267     Otherwise, the req or cap name would be looked up in the specified
268     node template's capabilities and if found, it would return  the property of
269     the capability as specified in function arguments.
270
271     Examples:
272
273     * { get_property: [ mysql_server, port ] }
274     * { get_property: [ SELF, db_port ] }
275     * { get_property: [ SELF, database_endpoint, port ] }
276     * { get_property: [ SELF, database_endpoint, port, 1 ] }
277     """
278
279     def validate(self):
280         if len(self.args) < 2:
281             ExceptionCollector.appendException(
282                 ValueError(_(
283                     'Expected arguments: "node-template-name", "req-or-cap" '
284                     '(optional), "property name".')))
285             return
286         if len(self.args) == 2:
287             found_prop = self._find_property(self.args[1])
288             if not found_prop:
289                 return
290             prop = found_prop.value
291             if not isinstance(prop, Function):
292                 get_function(self.tosca_tpl, self.context, prop)
293         elif len(self.args) >= 3:
294             # do not use _find_property to avoid raise KeyError
295             # if the prop is not found
296             # First check if there is property with this name
297             node_tpl = self._find_node_template(self.args[0])
298             props = node_tpl.get_properties() if node_tpl else []
299             index = 2
300             found = [props[self.args[1]]] if self.args[1] in props else []
301             if found:
302                 property_value = found[0].value
303             else:
304                 index = 3
305                 # then check the req or caps
306                 property_value = self._find_req_or_cap_property(self.args[1],
307                                                                 self.args[2])
308             if len(self.args) > index:
309                 for elem in self.args[index:]:
310                     if isinstance(property_value, list):
311                         int_elem = int(elem)
312                         property_value = self._get_index_value(property_value,
313                                                                int_elem)
314                     else:
315                         property_value = self._get_attribute_value(
316                             property_value,
317                             elem)
318
319     def _find_req_or_cap_property(self, req_or_cap, property_name):
320         node_tpl = self._find_node_template(self.args[0])
321         # Find property in node template's requirements
322         for r in node_tpl.requirements:
323             for req, node_name in r.items():
324                 if req == req_or_cap:
325                     node_template = self._find_node_template(node_name)
326                     return self._get_capability_property(
327                         node_template,
328                         req,
329                         property_name)
330         # If requirement was not found, look in node template's capabilities
331         return self._get_capability_property(node_tpl,
332                                              req_or_cap,
333                                              property_name)
334
335     def _get_capability_property(self,
336                                  node_template,
337                                  capability_name,
338                                  property_name):
339         """Gets a node template capability property."""
340         caps = node_template.get_capabilities()
341         if caps and capability_name in caps.keys():
342             cap = caps[capability_name]
343             property = None
344             props = cap.get_properties()
345             if props and property_name in props.keys():
346                 property = props[property_name].value
347             if not property:
348                 ExceptionCollector.appendException(
349                     KeyError(_('Property "%(prop)s" was not found in '
350                                'capability "%(cap)s" of node template '
351                                '"%(ntpl1)s" referenced from node template '
352                                '"%(ntpl2)s".') % {'prop': property_name,
353                                                   'cap': capability_name,
354                                                   'ntpl1': node_template.name,
355                                                   'ntpl2': self.context.name}))
356             return property
357         msg = _('Requirement/Capability "{0}" referenced from node template '
358                 '"{1}" was not found in node template "{2}".').format(
359                     capability_name,
360                     self.context.name,
361                     node_template.name)
362         ExceptionCollector.appendException(KeyError(msg))
363
364     def _find_property(self, property_name):
365         node_tpl = self._find_node_template(self.args[0])
366         if not node_tpl:
367             return
368         props = node_tpl.get_properties()
369         found = [props[property_name]] if property_name in props else []
370         if len(found) == 0:
371             ExceptionCollector.appendException(
372                 KeyError(_('Property "%(prop)s" was not found in node '
373                            'template "%(ntpl)s".') %
374                          {'prop': property_name,
375                           'ntpl': node_tpl.name}))
376             return None
377         return found[0]
378
379     def _find_node_template(self, node_template_name):
380         if node_template_name == SELF:
381             return self.context
382         # enable the HOST value in the function
383         if node_template_name == HOST:
384             return self._find_host_containing_property()
385         if node_template_name == TARGET:
386             if not isinstance(self.context.type_definition, RelationshipType):
387                 ExceptionCollector.appendException(
388                     KeyError(_('"TARGET" keyword can only be used in context'
389                                ' to "Relationships" target node')))
390                 return
391             return self.context.target
392         if node_template_name == SOURCE:
393             if not isinstance(self.context.type_definition, RelationshipType):
394                 ExceptionCollector.appendException(
395                     KeyError(_('"SOURCE" keyword can only be used in context'
396                                ' to "Relationships" source node')))
397                 return
398             return self.context.source
399         if not hasattr(self.tosca_tpl, 'nodetemplates'):
400             return
401         for node_template in self.tosca_tpl.nodetemplates:
402             if node_template.name == node_template_name:
403                 return node_template
404         ExceptionCollector.appendException(
405             KeyError(_(
406                 'Node template "{0}" was not found.'
407                 ).format(node_template_name)))
408
409     def _get_index_value(self, value, index):
410         if isinstance(value, list):
411             if index < len(value):
412                 return value[index]
413             else:
414                 ExceptionCollector.appendException(
415                     KeyError(_(
416                         "Property '{0}' found in capability '{1}'"
417                         " referenced from node template {2}"
418                         " must have an element with index {3}.").
419                         format(self.args[2],
420                                self.args[1],
421                                self.context.name,
422                                index)))
423         else:
424             ExceptionCollector.appendException(
425                 KeyError(_(
426                     "Property '{0}' found in capability '{1}'"
427                     " referenced from node template {2}"
428                     " must be a list.").format(self.args[2],
429                                                self.args[1],
430                                                self.context.name)))
431
432     def _get_attribute_value(self, value, attibute):
433         if isinstance(value, dict):
434             if attibute in value:
435                 return value[attibute]
436             else:
437                 ExceptionCollector.appendException(
438                     KeyError(_(
439                         "Property '{0}' found in capability '{1}'"
440                         " referenced from node template {2}"
441                         " must have an attribute named {3}.").
442                         format(self.args[2],
443                                self.args[1],
444                                self.context.name,
445                                attibute)))
446         else:
447             ExceptionCollector.appendException(
448                 KeyError(_(
449                     "Property '{0}' found in capability '{1}'"
450                     " referenced from node template {2}"
451                     " must be a dict.").format(self.args[2],
452                                                self.args[1],
453                                                self.context.name)))
454
455     # Add this functions similar to get_attribute case
456     def _find_host_containing_property(self, node_template_name=SELF):
457         node_template = self._find_node_template(node_template_name)
458         hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON]
459         for r in node_template.requirements:
460             for requirement, target_name in r.items():
461                 target_node = self._find_node_template(target_name)
462                 target_type = target_node.type_definition
463                 for capability in target_type.get_capabilities_objects():
464                     if capability.type in hosted_on_rel['valid_target_types']:
465                         if self._property_exists_in_type(target_type):
466                             return target_node
467                         return self._find_host_containing_property(
468                             target_name)
469         return None
470
471     def _property_exists_in_type(self, type_definition):
472         props_def = type_definition.get_properties_def()
473         found = [props_def[self.args[1]]] \
474             if self.args[1] in props_def else []
475         return len(found) == 1
476
477     def result(self):
478         if len(self.args) >= 3:
479             # First check if there is property with this name
480             node_tpl = self._find_node_template(self.args[0])
481             props = node_tpl.get_properties() if node_tpl else []
482             index = 2
483             found = [props[self.args[1]]] if self.args[1] in props else []
484             if found:
485                 property_value = found[0].value
486             else:
487                 index = 3
488                 # then check the req or caps
489                 property_value = self._find_req_or_cap_property(self.args[1],
490                                                                 self.args[2])
491             if len(self.args) > index:
492                 for elem in self.args[index:]:
493                     if isinstance(property_value, list):
494                         int_elem = int(elem)
495                         property_value = self._get_index_value(property_value,
496                                                                int_elem)
497                     else:
498                         property_value = self._get_attribute_value(
499                             property_value,
500                             elem)
501         else:
502             property_value = self._find_property(self.args[1]).value
503         if isinstance(property_value, Function):
504             return property_value.result()
505         return get_function(self.tosca_tpl,
506                             self.context,
507                             property_value)
508
509     @property
510     def node_template_name(self):
511         return self.args[0]
512
513     @property
514     def property_name(self):
515         if len(self.args) > 2:
516             return self.args[2]
517         return self.args[1]
518
519     @property
520     def req_or_cap(self):
521         if len(self.args) > 2:
522             return self.args[1]
523         return None
524
525
526 class Concat(Function):
527     """Validate the function and provide an instance of the function
528
529     Concatenation of values are supposed to be produced at runtime and
530     therefore its the responsibility of the TOSCA engine to implement the
531     evaluation of Concat functions.
532
533     Arguments:
534
535     * List of strings that needs to be concatenated
536
537     Example:
538
539       [ 'http://',
540         get_attribute: [ server, public_address ],
541         ':' ,
542         get_attribute: [ server, port ] ]
543     """
544
545     def validate(self):
546         if len(self.args) < 1:
547             ExceptionCollector.appendException(
548                 ValueError(_('Invalid arguments for function "{0}". Expected '
549                              'at least one arguments.').format(CONCAT)))
550
551     def result(self):
552         return self
553
554 function_mappings = {
555     GET_PROPERTY: GetProperty,
556     GET_INPUT: GetInput,
557     GET_ATTRIBUTE: GetAttribute,
558     CONCAT: Concat
559 }
560
561
562 def is_function(function):
563     """Returns True if the provided function is a Tosca intrinsic function.
564
565     Examples:
566
567     * "{ get_property: { SELF, port } }"
568     * "{ get_input: db_name }"
569     * Function instance
570
571     :param function: Function as string or a Function instance.
572     :return: True if function is a Tosca intrinsic function, otherwise False.
573     """
574     if isinstance(function, dict) and len(function) == 1:
575         func_name = list(function.keys())[0]
576         return func_name in function_mappings
577     return isinstance(function, Function)
578
579
580 def get_function(tosca_tpl, node_template, raw_function):
581     """Gets a Function instance representing the provided template function.
582
583     If the format provided raw_function format is not relevant for template
584     functions or if the function name doesn't exist in function mapping the
585     method returns the provided raw_function.
586
587     :param tosca_tpl: The tosca template.
588     :param node_template: The node template the function is specified for.
589     :param raw_function: The raw function as dict.
590     :return: Template function as Function instance or the raw_function if
591      parsing was unsuccessful.
592     """
593     if is_function(raw_function):
594         func_name = list(raw_function.keys())[0]
595         if func_name in function_mappings:
596             func = function_mappings[func_name]
597             func_args = list(raw_function.values())[0]
598             if not isinstance(func_args, list):
599                 func_args = [func_args]
600             return func(tosca_tpl, node_template, func_name, func_args)
601     return raw_function