Merge "fix import and vRNC Ctrl_net definition 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.constraints import Schema
22 from toscaparser.elements.datatype import DataType
23 from toscaparser.elements.entity_type import EntityType
24 from toscaparser.elements.relationshiptype import RelationshipType
25 from toscaparser.utils.gettextutils import _
26
27
28 GET_PROPERTY = 'get_property'
29 GET_ATTRIBUTE = 'get_attribute'
30 GET_INPUT = 'get_input'
31 CONCAT = 'concat'
32 TOKEN = 'token'
33
34 SELF = 'SELF'
35 HOST = 'HOST'
36 TARGET = 'TARGET'
37 SOURCE = 'SOURCE'
38
39 HOSTED_ON = 'tosca.relationships.HostedOn'
40
41
42 @six.add_metaclass(abc.ABCMeta)
43 class Function(object):
44     """An abstract type for representing a Tosca template function."""
45
46     def __init__(self, tosca_tpl, context, name, args):
47         self.tosca_tpl = tosca_tpl
48         self.context = context
49         self.name = name
50         self.args = args
51         self.validate()
52
53     @abc.abstractmethod
54     def result(self):
55         """Invokes the function and returns its result
56
57         Some methods invocation may only be relevant on runtime (for example,
58         getting runtime properties) and therefore its the responsibility of
59         the orchestrator/translator to take care of such functions invocation.
60
61         :return: Function invocation result.
62         """
63         return {self.name: self.args}
64
65     @abc.abstractmethod
66     def validate(self):
67         """Validates function arguments."""
68         pass
69
70
71 class GetInput(Function):
72     """Get a property value declared within the input of the service template.
73
74     Arguments:
75
76     * Input name.
77
78     Example:
79
80     * get_input: port
81     """
82
83     def validate(self):
84         if len(self.args) != 1:
85             ExceptionCollector.appendException(
86                 ValueError(_(
87                     'Expected one argument for function "get_input" but '
88                     'received "%s".') % self.args))
89         inputs = [input.name for input in self.tosca_tpl.inputs]
90         if self.args[0] not in inputs:
91             ExceptionCollector.appendException(
92                 UnknownInputError(input_name=self.args[0]))
93
94     def result(self):
95         if self.tosca_tpl.parsed_params and \
96            self.input_name in self.tosca_tpl.parsed_params:
97             return DataEntity.validate_datatype(
98                 self.tosca_tpl.tpl['inputs'][self.input_name]['type'],
99                 self.tosca_tpl.parsed_params[self.input_name])
100
101         input = [input_def for input_def in self.tosca_tpl.inputs
102                  if self.input_name == input_def.name][0]
103         return input.default
104
105     @property
106     def input_name(self):
107         return self.args[0]
108
109
110 class GetAttribute(Function):
111     """Get an attribute value of an entity defined in the service template
112
113     Node template attributes values are set in runtime and therefore its the
114     responsibility of the Tosca engine to implement the evaluation of
115     get_attribute functions.
116
117     Arguments:
118
119     * Node template name | HOST.
120     * Attribute name.
121
122     If the HOST keyword is passed as the node template name argument the
123     function will search each node template along the HostedOn relationship
124     chain until a node which contains the attribute is found.
125
126     Examples:
127
128     * { get_attribute: [ server, private_address ] }
129     * { get_attribute: [ HOST, private_address ] }
130     * { get_attribute: [ HOST, private_address, 0 ] }
131     * { get_attribute: [ HOST, private_address, 0, some_prop] }
132     """
133
134     def validate(self):
135         if len(self.args) < 2:
136             ExceptionCollector.appendException(
137                 ValueError(_('Illegal arguments for function "{0}". Expected '
138                              'arguments: "node-template-name", "req-or-cap"'
139                              '(optional), "property name"'
140                              ).format(GET_ATTRIBUTE)))
141             return
142         elif len(self.args) == 2:
143             self._find_node_template_containing_attribute()
144         else:
145             node_tpl = self._find_node_template(self.args[0])
146             index = 2
147             attrs = node_tpl.type_definition.get_attributes_def()
148             found = [attrs[self.args[1]]] if self.args[1] in attrs else []
149             if found:
150                 attr = found[0]
151             else:
152                 index = 3
153                 # then check the req or caps
154                 attr = self._find_req_or_cap_attribute(self.args[1],
155                                                        self.args[2])
156
157             value_type = attr.schema['type']
158             if len(self.args) > index:
159                 for elem in self.args[index:]:
160                     if value_type == "list":
161                         if not isinstance(elem, int):
162                             ExceptionCollector.appendException(
163                                 ValueError(_('Illegal arguments for function'
164                                              ' "{0}". "{1}" Expected positive'
165                                              ' integer argument'
166                                              ).format(GET_ATTRIBUTE, elem)))
167                         value_type = attr.schema['entry_schema']['type']
168                     elif value_type == "map":
169                         value_type = attr.schema['entry_schema']['type']
170                     elif value_type in Schema.PROPERTY_TYPES:
171                         ExceptionCollector.appendException(
172                             ValueError(_('Illegal arguments for function'
173                                          ' "{0}". Unexpected attribute/'
174                                          'index value "{1}"'
175                                          ).format(GET_ATTRIBUTE, elem)))
176                         return
177                     else:  # It is a complex type
178                         data_type = DataType(value_type)
179                         props = data_type.get_all_properties()
180                         found = [props[elem]] if elem in props else []
181                         if found:
182                             prop = found[0]
183                             value_type = prop.schema['type']
184                         else:
185                             ExceptionCollector.appendException(
186                                 KeyError(_('Illegal arguments for function'
187                                            ' "{0}". Attribute name "{1}" not'
188                                            ' found in "{2}"'
189                                            ).format(GET_ATTRIBUTE,
190                                                     elem,
191                                                     value_type)))
192
193     def result(self):
194         return self
195
196     def get_referenced_node_template(self):
197         """Gets the NodeTemplate instance the get_attribute function refers to.
198
199         If HOST keyword was used as the node template argument, the node
200         template which contains the attribute along the HostedOn relationship
201         chain will be returned.
202         """
203         return self._find_node_template_containing_attribute()
204
205     def _find_node_template_containing_attribute(self):
206         node_tpl = self._find_node_template(self.args[0])
207         if node_tpl and \
208             not self._attribute_exists_in_type(node_tpl.type_definition):
209             ExceptionCollector.appendException(
210                 KeyError(_('Attribute "%(att)s" was not found in node '
211                            'template "%(ntpl)s".') %
212                          {'att': self.attribute_name,
213                           'ntpl': node_tpl.name}))
214         return node_tpl
215
216     def _attribute_exists_in_type(self, type_definition):
217         attrs_def = type_definition.get_attributes_def()
218         found = [attrs_def[self.attribute_name]] \
219             if self.attribute_name in attrs_def else []
220         return len(found) == 1
221
222     def _find_host_containing_attribute(self, node_template_name=SELF):
223         node_template = self._find_node_template(node_template_name)
224         if node_template:
225             hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON]
226             for r in node_template.requirements:
227                 for requirement, target_name in r.items():
228                     target_node = self._find_node_template(target_name)
229                     target_type = target_node.type_definition
230                     for capability in target_type.get_capabilities_objects():
231                         if capability.type in \
232                             hosted_on_rel['valid_target_types']:
233                             if self._attribute_exists_in_type(target_type):
234                                 return target_node
235                             return self._find_host_containing_attribute(
236                                 target_name)
237
238     def _find_node_template(self, node_template_name):
239         if node_template_name == HOST:
240             # Currently this is the only way to tell whether the function
241             # is used within the outputs section of the TOSCA template.
242             if isinstance(self.context, list):
243                 ExceptionCollector.appendException(
244                     ValueError(_(
245                         '"get_attribute: [ HOST, ... ]" is not allowed in '
246                         '"outputs" section of the TOSCA template.')))
247                 return
248             node_tpl = self._find_host_containing_attribute()
249             if not node_tpl:
250                 ExceptionCollector.appendException(
251                     ValueError(_(
252                         '"get_attribute: [ HOST, ... ]" was used in node '
253                         'template "{0}" but "{1}" was not found in '
254                         'the relationship chain.').format(self.context.name,
255                                                           HOSTED_ON)))
256                 return
257             return node_tpl
258         if node_template_name == TARGET:
259             if not isinstance(self.context.type_definition, RelationshipType):
260                 ExceptionCollector.appendException(
261                     KeyError(_('"TARGET" keyword can only be used in context'
262                                ' to "Relationships" target node')))
263                 return
264             return self.context.target
265         if node_template_name == SOURCE:
266             if not isinstance(self.context.type_definition, RelationshipType):
267                 ExceptionCollector.appendException(
268                     KeyError(_('"SOURCE" keyword can only be used in context'
269                                ' to "Relationships" source node')))
270                 return
271             return self.context.source
272         name = self.context.name \
273             if node_template_name == SELF and \
274             not isinstance(self.context, list) \
275             else node_template_name
276         for node_template in self.tosca_tpl.nodetemplates:
277             if node_template.name == name:
278                 return node_template
279         ExceptionCollector.appendException(
280             KeyError(_(
281                 'Node template "{0}" was not found.'
282                 ).format(node_template_name)))
283
284     def _find_req_or_cap_attribute(self, req_or_cap, attr_name):
285         node_tpl = self._find_node_template(self.args[0])
286         # Find attribute in node template's requirements
287         for r in node_tpl.requirements:
288             for req, node_name in r.items():
289                 if req == req_or_cap:
290                     node_template = self._find_node_template(node_name)
291                     return self._get_capability_attribute(
292                         node_template,
293                         req,
294                         attr_name)
295         # If requirement was not found, look in node template's capabilities
296         return self._get_capability_attribute(node_tpl,
297                                               req_or_cap,
298                                               attr_name)
299
300     def _get_capability_attribute(self,
301                                   node_template,
302                                   capability_name,
303                                   attr_name):
304         """Gets a node template capability attribute."""
305         caps = node_template.get_capabilities()
306         if caps and capability_name in caps.keys():
307             cap = caps[capability_name]
308             attribute = None
309             attrs = cap.definition.get_attributes_def()
310             if attrs and attr_name in attrs.keys():
311                 attribute = attrs[attr_name]
312             if not attribute:
313                 ExceptionCollector.appendException(
314                     KeyError(_('Attribute "%(attr)s" was not found in '
315                                'capability "%(cap)s" of node template '
316                                '"%(ntpl1)s" referenced from node template '
317                                '"%(ntpl2)s".') % {'attr': attr_name,
318                                                   'cap': capability_name,
319                                                   'ntpl1': node_template.name,
320                                                   'ntpl2': self.context.name}))
321             return attribute
322         msg = _('Requirement/Capability "{0}" referenced from node template '
323                 '"{1}" was not found in node template "{2}".').format(
324                     capability_name,
325                     self.context.name,
326                     node_template.name)
327         ExceptionCollector.appendException(KeyError(msg))
328
329     @property
330     def node_template_name(self):
331         return self.args[0]
332
333     @property
334     def attribute_name(self):
335         return self.args[1]
336
337
338 class GetProperty(Function):
339     """Get a property value of an entity defined in the same service template.
340
341     Arguments:
342
343     * Node template name | SELF | HOST | SOURCE | TARGET.
344     * Requirement or capability name (optional).
345     * Property name.
346
347     If requirement or capability name is specified, the behavior is as follows:
348     The req or cap name is first looked up in the specified node template's
349     requirements.
350     If found, it would search for a matching capability
351     of an other node template and get its property as specified in function
352     arguments.
353     Otherwise, the req or cap name would be looked up in the specified
354     node template's capabilities and if found, it would return  the property of
355     the capability as specified in function arguments.
356
357     Examples:
358
359     * { get_property: [ mysql_server, port ] }
360     * { get_property: [ SELF, db_port ] }
361     * { get_property: [ SELF, database_endpoint, port ] }
362     * { get_property: [ SELF, database_endpoint, port, 1 ] }
363     """
364
365     def validate(self):
366         if len(self.args) < 2:
367             ExceptionCollector.appendException(
368                 ValueError(_(
369                     'Expected arguments: "node-template-name", "req-or-cap" '
370                     '(optional), "property name".')))
371             return
372         if len(self.args) == 2:
373             found_prop = self._find_property(self.args[1])
374             if not found_prop:
375                 return
376             prop = found_prop.value
377             if not isinstance(prop, Function):
378                 get_function(self.tosca_tpl, self.context, prop)
379         elif len(self.args) >= 3:
380             # do not use _find_property to avoid raise KeyError
381             # if the prop is not found
382             # First check if there is property with this name
383             node_tpl = self._find_node_template(self.args[0])
384             props = node_tpl.get_properties() if node_tpl else []
385             index = 2
386             found = [props[self.args[1]]] if self.args[1] in props else []
387             if found:
388                 property_value = found[0].value
389             else:
390                 index = 3
391                 # then check the req or caps
392                 property_value = self._find_req_or_cap_property(self.args[1],
393                                                                 self.args[2])
394             if len(self.args) > index:
395                 for elem in self.args[index:]:
396                     if isinstance(property_value, list):
397                         int_elem = int(elem)
398                         property_value = self._get_index_value(property_value,
399                                                                int_elem)
400                     else:
401                         property_value = self._get_attribute_value(
402                             property_value,
403                             elem)
404
405     def _find_req_or_cap_property(self, req_or_cap, property_name):
406         node_tpl = self._find_node_template(self.args[0])
407         # Find property in node template's requirements
408         for r in node_tpl.requirements:
409             for req, node_name in r.items():
410                 if req == req_or_cap:
411                     node_template = self._find_node_template(node_name)
412                     return self._get_capability_property(
413                         node_template,
414                         req,
415                         property_name)
416         # If requirement was not found, look in node template's capabilities
417         return self._get_capability_property(node_tpl,
418                                              req_or_cap,
419                                              property_name)
420
421     def _get_capability_property(self,
422                                  node_template,
423                                  capability_name,
424                                  property_name):
425         """Gets a node template capability property."""
426         caps = node_template.get_capabilities()
427         if caps and capability_name in caps.keys():
428             cap = caps[capability_name]
429             property = None
430             props = cap.get_properties()
431             if props and property_name in props.keys():
432                 property = props[property_name].value
433             if not property:
434                 ExceptionCollector.appendException(
435                     KeyError(_('Property "%(prop)s" was not found in '
436                                'capability "%(cap)s" of node template '
437                                '"%(ntpl1)s" referenced from node template '
438                                '"%(ntpl2)s".') % {'prop': property_name,
439                                                   'cap': capability_name,
440                                                   'ntpl1': node_template.name,
441                                                   'ntpl2': self.context.name}))
442             return property
443         msg = _('Requirement/Capability "{0}" referenced from node template '
444                 '"{1}" was not found in node template "{2}".').format(
445                     capability_name,
446                     self.context.name,
447                     node_template.name)
448         ExceptionCollector.appendException(KeyError(msg))
449
450     def _find_property(self, property_name):
451         node_tpl = self._find_node_template(self.args[0])
452         if not node_tpl:
453             return
454         props = node_tpl.get_properties()
455         found = [props[property_name]] if property_name in props else []
456         if len(found) == 0:
457             ExceptionCollector.appendException(
458                 KeyError(_('Property "%(prop)s" was not found in node '
459                            'template "%(ntpl)s".') %
460                          {'prop': property_name,
461                           'ntpl': node_tpl.name}))
462             return None
463         return found[0]
464
465     def _find_node_template(self, node_template_name):
466         if node_template_name == SELF:
467             return self.context
468         # enable the HOST value in the function
469         if node_template_name == HOST:
470             return self._find_host_containing_property()
471         if node_template_name == TARGET:
472             if not isinstance(self.context.type_definition, RelationshipType):
473                 ExceptionCollector.appendException(
474                     KeyError(_('"TARGET" keyword can only be used in context'
475                                ' to "Relationships" target node')))
476                 return
477             return self.context.target
478         if node_template_name == SOURCE:
479             if not isinstance(self.context.type_definition, RelationshipType):
480                 ExceptionCollector.appendException(
481                     KeyError(_('"SOURCE" keyword can only be used in context'
482                                ' to "Relationships" source node')))
483                 return
484             return self.context.source
485         if not hasattr(self.tosca_tpl, 'nodetemplates'):
486             return
487         for node_template in self.tosca_tpl.nodetemplates:
488             if node_template.name == node_template_name:
489                 return node_template
490         ExceptionCollector.appendException(
491             KeyError(_(
492                 'Node template "{0}" was not found.'
493                 ).format(node_template_name)))
494
495     def _get_index_value(self, value, index):
496         if isinstance(value, list):
497             if index < len(value):
498                 return value[index]
499             else:
500                 ExceptionCollector.appendException(
501                     KeyError(_(
502                         "Property '{0}' found in capability '{1}'"
503                         " referenced from node template {2}"
504                         " must have an element with index {3}.").
505                         format(self.args[2],
506                                self.args[1],
507                                self.context.name,
508                                index)))
509         else:
510             ExceptionCollector.appendException(
511                 KeyError(_(
512                     "Property '{0}' found in capability '{1}'"
513                     " referenced from node template {2}"
514                     " must be a list.").format(self.args[2],
515                                                self.args[1],
516                                                self.context.name)))
517
518     def _get_attribute_value(self, value, attibute):
519         if isinstance(value, dict):
520             if attibute in value:
521                 return value[attibute]
522             else:
523                 ExceptionCollector.appendException(
524                     KeyError(_(
525                         "Property '{0}' found in capability '{1}'"
526                         " referenced from node template {2}"
527                         " must have an attribute named {3}.").
528                         format(self.args[2],
529                                self.args[1],
530                                self.context.name,
531                                attibute)))
532         else:
533             ExceptionCollector.appendException(
534                 KeyError(_(
535                     "Property '{0}' found in capability '{1}'"
536                     " referenced from node template {2}"
537                     " must be a dict.").format(self.args[2],
538                                                self.args[1],
539                                                self.context.name)))
540
541     # Add this functions similar to get_attribute case
542     def _find_host_containing_property(self, node_template_name=SELF):
543         node_template = self._find_node_template(node_template_name)
544         hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON]
545         for r in node_template.requirements:
546             for requirement, target_name in r.items():
547                 target_node = self._find_node_template(target_name)
548                 target_type = target_node.type_definition
549                 for capability in target_type.get_capabilities_objects():
550                     if capability.type in hosted_on_rel['valid_target_types']:
551                         if self._property_exists_in_type(target_type):
552                             return target_node
553                         return self._find_host_containing_property(
554                             target_name)
555         return None
556
557     def _property_exists_in_type(self, type_definition):
558         props_def = type_definition.get_properties_def()
559         found = [props_def[self.args[1]]] \
560             if self.args[1] in props_def else []
561         return len(found) == 1
562
563     def result(self):
564         if len(self.args) >= 3:
565             # First check if there is property with this name
566             node_tpl = self._find_node_template(self.args[0])
567             props = node_tpl.get_properties() if node_tpl else []
568             index = 2
569             found = [props[self.args[1]]] if self.args[1] in props else []
570             if found:
571                 property_value = found[0].value
572             else:
573                 index = 3
574                 # then check the req or caps
575                 property_value = self._find_req_or_cap_property(self.args[1],
576                                                                 self.args[2])
577             if len(self.args) > index:
578                 for elem in self.args[index:]:
579                     if isinstance(property_value, list):
580                         int_elem = int(elem)
581                         property_value = self._get_index_value(property_value,
582                                                                int_elem)
583                     else:
584                         property_value = self._get_attribute_value(
585                             property_value,
586                             elem)
587         else:
588             property_value = self._find_property(self.args[1]).value
589         if isinstance(property_value, Function):
590             return property_value.result()
591         return get_function(self.tosca_tpl,
592                             self.context,
593                             property_value)
594
595     @property
596     def node_template_name(self):
597         return self.args[0]
598
599     @property
600     def property_name(self):
601         if len(self.args) > 2:
602             return self.args[2]
603         return self.args[1]
604
605     @property
606     def req_or_cap(self):
607         if len(self.args) > 2:
608             return self.args[1]
609         return None
610
611
612 class Concat(Function):
613     """Validate the function and provide an instance of the function
614
615     Concatenation of values are supposed to be produced at runtime and
616     therefore its the responsibility of the TOSCA engine to implement the
617     evaluation of Concat functions.
618
619     Arguments:
620
621     * List of strings that needs to be concatenated
622
623     Example:
624
625       [ 'http://',
626         get_attribute: [ server, public_address ],
627         ':' ,
628         get_attribute: [ server, port ] ]
629     """
630
631     def validate(self):
632         if len(self.args) < 1:
633             ExceptionCollector.appendException(
634                 ValueError(_('Invalid arguments for function "{0}". Expected '
635                              'at least one arguments.').format(CONCAT)))
636
637     def result(self):
638         return self
639
640
641 class Token(Function):
642     """Validate the function and provide an instance of the function
643
644     The token function is used within a TOSCA service template on a string to
645     parse out (tokenize) substrings separated by one or more token characters
646     within a larger string.
647
648
649     Arguments:
650
651     * The composite string that contains one or more substrings separated by
652       token characters.
653     * The string that contains one or more token characters that separate
654       substrings within the composite string.
655     * The integer indicates the index of the substring to return from the
656       composite string.  Note that the first substring is denoted by using
657       the '0' (zero) integer value.
658
659     Example:
660
661      [ get_attribute: [ my_server, data_endpoint, ip_address ], ':', 1 ]
662
663     """
664
665     def validate(self):
666         if len(self.args) < 3:
667             ExceptionCollector.appendException(
668                 ValueError(_('Invalid arguments for function "{0}". Expected '
669                              'at least three arguments.').format(TOKEN)))
670         else:
671             if not isinstance(self.args[1], str) or len(self.args[1]) != 1:
672                 ExceptionCollector.appendException(
673                     ValueError(_('Invalid arguments for function "{0}". '
674                                  'Expected single char value as second '
675                                  'argument.').format(TOKEN)))
676
677             if not isinstance(self.args[2], int):
678                 ExceptionCollector.appendException(
679                     ValueError(_('Invalid arguments for function "{0}". '
680                                  'Expected integer value as third '
681                                  'argument.').format(TOKEN)))
682
683     def result(self):
684         return self
685
686 function_mappings = {
687     GET_PROPERTY: GetProperty,
688     GET_INPUT: GetInput,
689     GET_ATTRIBUTE: GetAttribute,
690     CONCAT: Concat,
691     TOKEN: Token
692 }
693
694
695 def is_function(function):
696     """Returns True if the provided function is a Tosca intrinsic function.
697
698     Examples:
699
700     * "{ get_property: { SELF, port } }"
701     * "{ get_input: db_name }"
702     * Function instance
703
704     :param function: Function as string or a Function instance.
705     :return: True if function is a Tosca intrinsic function, otherwise False.
706     """
707     if isinstance(function, dict) and len(function) == 1:
708         func_name = list(function.keys())[0]
709         return func_name in function_mappings
710     return isinstance(function, Function)
711
712
713 def get_function(tosca_tpl, node_template, raw_function):
714     """Gets a Function instance representing the provided template function.
715
716     If the format provided raw_function format is not relevant for template
717     functions or if the function name doesn't exist in function mapping the
718     method returns the provided raw_function.
719
720     :param tosca_tpl: The tosca template.
721     :param node_template: The node template the function is specified for.
722     :param raw_function: The raw function as dict.
723     :return: Template function as Function instance or the raw_function if
724      parsing was unsuccessful.
725     """
726     if is_function(raw_function):
727         func_name = list(raw_function.keys())[0]
728         if func_name in function_mappings:
729             func = function_mappings[func_name]
730             func_args = list(raw_function.values())[0]
731             if not isinstance(func_args, list):
732                 func_args = [func_args]
733             return func(tosca_tpl, node_template, func_name, func_args)
734     return raw_function