Update tosca lib to version 0.5
[parser.git] / tosca2heat / heat-translator / translator / hot / translate_node_templates.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 import importlib
15 import logging
16 import os
17 import six
18
19 from toscaparser.functions import GetAttribute
20 from toscaparser.functions import GetInput
21 from toscaparser.functions import GetProperty
22 from toscaparser.properties import Property
23 from toscaparser.relationship_template import RelationshipTemplate
24 from toscaparser.utils.gettextutils import _
25 from translator.common.exception import ToscaClassAttributeError
26 from translator.common.exception import ToscaClassImportError
27 from translator.common.exception import ToscaModImportError
28 from translator.conf.config import ConfigProvider as translatorConfig
29 from translator.hot.syntax.hot_resource import HotResource
30 from translator.hot.tosca.tosca_block_storage_attachment import (
31     ToscaBlockStorageAttachment
32     )
33
34 ###########################
35 # Module utility Functions
36 # for dynamic class loading
37 ###########################
38
39
40 def _generate_type_map():
41     '''Generate TOSCA translation types map.
42
43     Load user defined classes from location path specified in conf file.
44     Base classes are located within the tosca directory.
45
46     '''
47
48     # Base types directory
49     BASE_PATH = 'translator/hot/tosca'
50
51     # Custom types directory defined in conf file
52     custom_path = translatorConfig.get_value('DEFAULT',
53                                              'custom_types_location')
54
55     # First need to load the parent module, for example 'contrib.hot',
56     # for all of the dynamically loaded classes.
57     classes = []
58     _load_classes((BASE_PATH, custom_path), classes)
59     try:
60         types_map = {clazz.toscatype: clazz for clazz in classes}
61     except AttributeError as e:
62         raise ToscaClassAttributeError(message=e.message)
63
64     return types_map
65
66
67 def _load_classes(locations, classes):
68     '''Dynamically load all the classes from the given locations.'''
69
70     for cls_path in locations:
71         # Use the absolute path of the class path
72         abs_path = os.path.dirname(os.path.abspath(__file__))
73         abs_path = abs_path.replace('translator/hot', cls_path)
74
75         # Grab all the tosca type module files in the given path
76         mod_files = [f for f in os.listdir(abs_path) if f.endswith('.py')
77                      and not f.startswith('__init__')
78                      and f.startswith('tosca_')]
79
80         # For each module, pick out the target translation class
81         for f in mod_files:
82             # NOTE: For some reason the existing code does not use the map to
83             # instantiate ToscaBlockStorageAttachment. Don't add it to the map
84             # here until the dependent code is fixed to use the map.
85             if f == 'tosca_block_storage_attachment.py':
86                 continue
87
88             mod_name = cls_path + '/' + f.strip('.py')
89             mod_name = mod_name.replace('/', '.')
90             try:
91                 mod = importlib.import_module(mod_name)
92                 target_name = getattr(mod, 'TARGET_CLASS_NAME')
93                 clazz = getattr(mod, target_name)
94                 classes.append(clazz)
95             except ImportError:
96                 raise ToscaModImportError(mod_name=mod_name)
97             except AttributeError:
98                 if target_name:
99                     raise ToscaClassImportError(name=target_name,
100                                                 mod_name=mod_name)
101                 else:
102                     # TARGET_CLASS_NAME is not defined in module.
103                     # Re-raise the exception
104                     raise
105
106 ##################
107 # Module constants
108 ##################
109
110 SECTIONS = (TYPE, PROPERTIES, REQUIREMENTS, INTERFACES, LIFECYCLE, INPUT) = \
111            ('type', 'properties', 'requirements',
112             'interfaces', 'lifecycle', 'input')
113
114 # TODO(anyone):  the following requirement names should not be hard-coded
115 # in the translator.  Since they are basically arbitrary names, we have to get
116 # them from TOSCA type definitions.
117 # To be fixed with the blueprint:
118 # https://blueprints.launchpad.net/heat-translator/+spec/tosca-custom-types
119 REQUIRES = (CONTAINER, DEPENDENCY, DATABASE_ENDPOINT, CONNECTION, HOST) = \
120            ('container', 'dependency', 'database_endpoint',
121             'connection', 'host')
122
123 INTERFACES_STATE = (CREATE, START, CONFIGURE, START, DELETE) = \
124                    ('create', 'stop', 'configure', 'start', 'delete')
125
126
127 TOSCA_TO_HOT_REQUIRES = {'container': 'server', 'host': 'server',
128                          'dependency': 'depends_on', "connects": 'depends_on'}
129
130 TOSCA_TO_HOT_PROPERTIES = {'properties': 'input'}
131 log = logging.getLogger('heat-translator')
132
133 TOSCA_TO_HOT_TYPE = _generate_type_map()
134
135
136 class TranslateNodeTemplates(object):
137     '''Translate TOSCA NodeTemplates to Heat Resources.'''
138
139     def __init__(self, tosca, hot_template):
140         self.tosca = tosca
141         self.nodetemplates = self.tosca.nodetemplates
142         self.hot_template = hot_template
143         # list of all HOT resources generated
144         self.hot_resources = []
145         # mapping between TOSCA nodetemplate and HOT resource
146         log.debug(_('Mapping between TOSCA nodetemplate and HOT resource.'))
147         self.hot_lookup = {}
148         self.policies = self.tosca.topology_template.policies
149
150     def translate(self):
151         return self._translate_nodetemplates()
152
153     def _recursive_handle_properties(self, resource):
154         '''Recursively handle the properties of the depends_on_nodes nodes.'''
155         # Use of hashtable (dict) here should be faster?
156         if resource in self.processed_resources:
157             return
158         self.processed_resources.append(resource)
159         for depend_on in resource.depends_on_nodes:
160             self._recursive_handle_properties(depend_on)
161
162         if resource.type == "OS::Nova::ServerGroup":
163             resource.handle_properties(self.hot_resources)
164         else:
165             resource.handle_properties()
166
167     def _translate_nodetemplates(self):
168
169         log.debug(_('Translating the node templates.'))
170         suffix = 0
171         # Copy the TOSCA graph: nodetemplate
172         for node in self.nodetemplates:
173             base_type = HotResource.get_base_type(node.type_definition)
174             hot_node = TOSCA_TO_HOT_TYPE[base_type.type](node)
175             self.hot_resources.append(hot_node)
176             self.hot_lookup[node] = hot_node
177
178             # BlockStorage Attachment is a special case,
179             # which doesn't match to Heat Resources 1 to 1.
180             if base_type.type == "tosca.nodes.Compute":
181                 volume_name = None
182                 requirements = node.requirements
183                 if requirements:
184                     # Find the name of associated BlockStorage node
185                     for requires in requirements:
186                         for value in requires.values():
187                             if isinstance(value, dict):
188                                 for node_name in value.values():
189                                     for n in self.nodetemplates:
190                                         if n.name == node_name:
191                                             volume_name = node_name
192                                             break
193                             else:  # unreachable code !
194                                 for n in self.nodetemplates:
195                                     if n.name == node_name:
196                                         volume_name = node_name
197                                         break
198
199                     suffix = suffix + 1
200                     attachment_node = self._get_attachment_node(node,
201                                                                 suffix,
202                                                                 volume_name)
203                     if attachment_node:
204                         self.hot_resources.append(attachment_node)
205                 for i in self.tosca.inputs:
206                     if (i.name == 'key_name' and
207                             node.get_property_value('key_name') is None):
208                         schema = {'type': i.type, 'default': i.default}
209                         value = {"get_param": "key_name"}
210                         prop = Property(i.name, value, schema)
211                         node._properties.append(prop)
212
213         for policy in self.policies:
214             policy_type = policy.type_definition
215             policy_node = TOSCA_TO_HOT_TYPE[policy_type.type](policy)
216             self.hot_resources.append(policy_node)
217
218         # Handle life cycle operations: this may expand each node
219         # into multiple HOT resources and may change their name
220         lifecycle_resources = []
221         for resource in self.hot_resources:
222             expanded = resource.handle_life_cycle()
223             if expanded:
224                 lifecycle_resources += expanded
225         self.hot_resources += lifecycle_resources
226
227         # Handle configuration from ConnectsTo relationship in the TOSCA node:
228         # this will generate multiple HOT resources, set of 2 for each
229         # configuration
230         connectsto_resources = []
231         for node in self.nodetemplates:
232             for requirement in node.requirements:
233                 for endpoint, details in six.iteritems(requirement):
234                     relation = None
235                     if isinstance(details, dict):
236                         target = details.get('node')
237                         relation = details.get('relationship')
238                     else:
239                         target = details
240                     if (target and relation and
241                             not isinstance(relation, six.string_types)):
242                         interfaces = relation.get('interfaces')
243                         connectsto_resources += \
244                             self._create_connect_configs(node,
245                                                          target,
246                                                          interfaces)
247         self.hot_resources += connectsto_resources
248
249         # Copy the initial dependencies based on the relationship in
250         # the TOSCA template
251         for node in self.nodetemplates:
252             for node_depend in node.related_nodes:
253                 # if the source of dependency is a server and the
254                 # relationship type is 'tosca.relationships.HostedOn',
255                 # add dependency as properties.server
256                 if node_depend.type == 'tosca.nodes.Compute' and \
257                    node.related[node_depend].type == \
258                    node.type_definition.HOSTEDON:
259                     self.hot_lookup[node].properties['server'] = \
260                         {'get_resource': self.hot_lookup[node_depend].name}
261                 # for all others, add dependency as depends_on
262                 else:
263                     self.hot_lookup[node].depends_on.append(
264                         self.hot_lookup[node_depend].top_of_chain())
265
266                 self.hot_lookup[node].depends_on_nodes.append(
267                     self.hot_lookup[node_depend].top_of_chain())
268
269         # handle hosting relationship
270         for resource in self.hot_resources:
271             resource.handle_hosting()
272
273         # handle built-in properties of HOT resources
274         # if a resource depends on other resources,
275         # their properties need to be handled first.
276         # Use recursion to handle the properties of the
277         # dependent nodes in correct order
278         self.processed_resources = []
279         for resource in self.hot_resources:
280             self._recursive_handle_properties(resource)
281
282         # handle resources that need to expand to more than one HOT resource
283         expansion_resources = []
284         for resource in self.hot_resources:
285             expanded = resource.handle_expansion()
286             if expanded:
287                 expansion_resources += expanded
288         self.hot_resources += expansion_resources
289
290         # Resolve function calls:  GetProperty, GetAttribute, GetInput
291         # at this point, all the HOT resources should have been created
292         # in the graph.
293         for resource in self.hot_resources:
294             # traverse the reference chain to get the actual value
295             inputs = resource.properties.get('input_values')
296             if inputs:
297                 for name, value in six.iteritems(inputs):
298                     inputs[name] = self._translate_input(value, resource)
299
300         return self.hot_resources
301
302     def _translate_input(self, input_value, resource):
303         get_property_args = None
304         if isinstance(input_value, GetProperty):
305             get_property_args = input_value.args
306         # to remove when the parser is fixed to return GetProperty
307         if isinstance(input_value, dict) and 'get_property' in input_value:
308             get_property_args = input_value['get_property']
309         if get_property_args is not None:
310             hot_target = self._find_hot_resource_for_tosca(
311                 get_property_args[0], resource)
312             if hot_target:
313                 props = hot_target.get_tosca_props()
314                 prop_name = get_property_args[1]
315                 if prop_name in props:
316                     return props[prop_name]
317         elif isinstance(input_value, GetAttribute):
318             # for the attribute
319             # get the proper target type to perform the translation
320             args = input_value.result()
321             hot_target = self._find_hot_resource_for_tosca(args[0], resource)
322
323             return hot_target.get_hot_attribute(args[1], args)
324         # most of artifacts logic should move to the parser
325         elif isinstance(input_value, dict) and 'get_artifact' in input_value:
326             get_artifact_args = input_value['get_artifact']
327
328             hot_target = self._find_hot_resource_for_tosca(
329                 get_artifact_args[0], resource)
330             artifacts = TranslateNodeTemplates.get_all_artifacts(
331                 hot_target.nodetemplate)
332
333             if get_artifact_args[1] in artifacts:
334                 artifact = artifacts[get_artifact_args[1]]
335                 if artifact.get('type', None) == 'tosca.artifacts.File':
336                     return {'get_file': artifact.get('file')}
337         elif isinstance(input_value, GetInput):
338             if isinstance(input_value.args, list) \
339                     and len(input_value.args) == 1:
340                 return {'get_param': input_value.args[0]}
341             else:
342                 return {'get_param': input_value.args}
343
344         return input_value
345
346     @staticmethod
347     def get_all_artifacts(nodetemplate):
348         artifacts = nodetemplate.type_definition.get_value('artifacts',
349                                                            parent=True)
350         if not artifacts:
351             artifacts = {}
352         tpl_artifacts = nodetemplate.entity_tpl.get('artifacts')
353         if tpl_artifacts:
354             artifacts.update(tpl_artifacts)
355
356         return artifacts
357
358     def _get_attachment_node(self, node, suffix, volume_name):
359         attach = False
360         ntpl = self.nodetemplates
361         for key, value in node.relationships.items():
362             if key.is_derived_from('tosca.relationships.AttachesTo'):
363                 if value.is_derived_from('tosca.nodes.BlockStorage'):
364                     attach = True
365             if attach:
366                 relationship_tpl = None
367                 for req in node.requirements:
368                     for key, val in req.items():
369                         attach = val
370                         relship = val.get('relationship')
371                         for rkey, rval in val.items():
372                             if relship and isinstance(relship, dict):
373                                 for rkey, rval in relship.items():
374                                     if rkey == 'type':
375                                         relationship_tpl = val
376                                         attach = rval
377                                     elif rkey == 'template':
378                                         rel_tpl_list = \
379                                             (self.tosca.topology_template.
380                                              _tpl_relationship_templates())
381                                         relationship_tpl = rel_tpl_list[rval]
382                                         attach = rval
383                                     else:
384                                         continue
385                             elif isinstance(relship, str):
386                                 attach = relship
387                                 relationship_tpl = val
388                                 relationship_templates = \
389                                     self.tosca._tpl_relationship_templates()
390                                 if 'relationship' in relationship_tpl and \
391                                    attach not in \
392                                    self.tosca._tpl_relationship_types() and \
393                                    attach in relationship_templates:
394                                     relationship_tpl['relationship'] = \
395                                         relationship_templates[attach]
396                                 break
397                         if relationship_tpl:
398                             rval_new = attach + "_" + str(suffix)
399                             att = RelationshipTemplate(
400                                 relationship_tpl, rval_new,
401                                 self.tosca._tpl_relationship_types())
402                             hot_node = ToscaBlockStorageAttachment(att, ntpl,
403                                                                    node.name,
404                                                                    volume_name
405                                                                    )
406                             return hot_node
407
408     def find_hot_resource(self, name):
409         for resource in self.hot_resources:
410             if resource.name == name:
411                 return resource
412
413     def _find_tosca_node(self, tosca_name):
414         for node in self.nodetemplates:
415             if node.name == tosca_name:
416                 return node
417
418     def _find_hot_resource_for_tosca(self, tosca_name,
419                                      current_hot_resource=None):
420         if tosca_name == 'SELF':
421             return current_hot_resource
422         if tosca_name == 'HOST' and current_hot_resource is not None:
423             for req in current_hot_resource.nodetemplate.requirements:
424                 if 'host' in req:
425                     return self._find_hot_resource_for_tosca(req['host'])
426
427         for node in self.nodetemplates:
428             if node.name == tosca_name:
429                 return self.hot_lookup[node]
430
431         return None
432
433     def _create_connect_configs(self, source_node, target_name,
434                                 connect_interfaces):
435         connectsto_resources = []
436         if connect_interfaces:
437             for iname, interface in six.iteritems(connect_interfaces):
438                 connectsto_resources += \
439                     self._create_connect_config(source_node, target_name,
440                                                 interface)
441         return connectsto_resources
442
443     def _create_connect_config(self, source_node, target_name,
444                                connect_interface):
445         connectsto_resources = []
446         target_node = self._find_tosca_node(target_name)
447         # the configuration can occur on the source or the target
448         connect_config = connect_interface.get('pre_configure_target')
449         if connect_config is not None:
450             config_location = 'target'
451         else:
452             connect_config = connect_interface.get('pre_configure_source')
453             if connect_config is not None:
454                 config_location = 'source'
455             else:
456                 msg = _("Template error:  "
457                         "no configuration found for ConnectsTo "
458                         "in {1}").format(self.nodetemplate.name)
459                 log.error(msg)
460                 raise Exception(msg)
461         config_name = source_node.name + '_' + target_name + '_connect_config'
462         implement = connect_config.get('implementation')
463         if config_location == 'target':
464             hot_config = HotResource(target_node,
465                                      config_name,
466                                      'OS::Heat::SoftwareConfig',
467                                      {'config': {'get_file': implement}})
468         elif config_location == 'source':
469             hot_config = HotResource(source_node,
470                                      config_name,
471                                      'OS::Heat::SoftwareConfig',
472                                      {'config': {'get_file': implement}})
473         connectsto_resources.append(hot_config)
474         hot_target = self._find_hot_resource_for_tosca(target_name)
475         hot_source = self._find_hot_resource_for_tosca(source_node.name)
476         connectsto_resources.append(hot_config.
477                                     handle_connectsto(source_node,
478                                                       target_node,
479                                                       hot_source,
480                                                       hot_target,
481                                                       config_location,
482                                                       connect_interface))
483         return connectsto_resources