Sync heat-translator code
[parser.git] / tosca2heat / heat-translator / translator / hot / syntax / hot_resource.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 from collections import OrderedDict
14 import logging
15 import os
16 import six
17
18 from toscaparser.elements.interfaces import InterfacesDef
19 from toscaparser.functions import GetInput
20 from toscaparser.nodetemplate import NodeTemplate
21 from toscaparser.utils.gettextutils import _
22
23
24 SECTIONS = (TYPE, PROPERTIES, MEDADATA, DEPENDS_ON, UPDATE_POLICY,
25             DELETION_POLICY) = \
26            ('type', 'properties', 'metadata',
27             'depends_on', 'update_policy', 'deletion_policy')
28
29 policy_type = ['tosca.policies.Placement',
30                'tosca.policies.Scaling',
31                'tosca.policies.Scaling.Cluster']
32 log = logging.getLogger('heat-translator')
33
34
35 class HotResource(object):
36     '''Base class for TOSCA node type translation to Heat resource type.'''
37
38     def __init__(self, nodetemplate, name=None, type=None, properties=None,
39                  metadata=None, depends_on=None,
40                  update_policy=None, deletion_policy=None, csar_dir=None):
41         log.debug(_('Translating TOSCA node type to HOT resource type.'))
42         self.nodetemplate = nodetemplate
43         if name:
44             self.name = name
45         else:
46             self.name = nodetemplate.name
47         self.type = type
48         self.properties = properties or {}
49
50         self.csar_dir = csar_dir
51         # special case for HOT softwareconfig
52         cwd = os.getcwd()
53         if type == 'OS::Heat::SoftwareConfig':
54             config = self.properties.get('config')
55             if isinstance(config, dict):
56                 if self.csar_dir:
57                     os.chdir(self.csar_dir)
58                     implementation_artifact = os.path.abspath(config.get(
59                         'get_file'))
60                 else:
61                     implementation_artifact = config.get('get_file')
62                 if implementation_artifact:
63                     filename, file_extension = os.path.splitext(
64                         implementation_artifact)
65                     file_extension = file_extension.lower()
66                     # artifact_types should be read to find the exact script
67                     # type, unfortunately artifact_types doesn't seem to be
68                     # supported by the parser
69                     if file_extension == '.ansible' \
70                             or file_extension == '.yaml' \
71                             or file_extension == '.yml':
72                         self.properties['group'] = 'ansible'
73                     if file_extension == '.pp':
74                         self.properties['group'] = 'puppet'
75
76             if self.properties.get('group') is None:
77                 self.properties['group'] = 'script'
78         os.chdir(cwd)
79         self.metadata = metadata
80
81         # The difference between depends_on and depends_on_nodes is
82         # that depends_on defines dependency in the context of the
83         # HOT template and it is used during the template output.
84         # Depends_on_nodes defines the direct dependency between the
85         # tosca nodes and is not used during the output of the
86         # HOT template but for internal processing only. When a tosca
87         # node depends on another node it will be always added to
88         # depends_on_nodes but not always to depends_on. For example
89         # if the source of dependency is a server, the dependency will
90         # be added as properties.get_resource and not depends_on
91         if depends_on:
92             self.depends_on = depends_on
93             self.depends_on_nodes = depends_on
94         else:
95             self.depends_on = []
96             self.depends_on_nodes = []
97         self.update_policy = update_policy
98         self.deletion_policy = deletion_policy
99         self.group_dependencies = {}
100         # if hide_resource is set to true, then this resource will not be
101         # generated in the output yaml.
102         self.hide_resource = False
103
104     def handle_properties(self):
105         # the property can hold a value or the intrinsic function get_input
106         # for value, copy it
107         # for get_input, convert to get_param
108         for prop in self.nodetemplate.get_properties_objects():
109             pass
110
111     def handle_life_cycle(self):
112         hot_resources = []
113         deploy_lookup = {}
114         # TODO(anyone):  sequence for life cycle needs to cover different
115         # scenarios and cannot be fixed or hard coded here
116         operations_deploy_sequence = ['create', 'configure', 'start']
117
118         operations = HotResource.get_all_operations(self.nodetemplate)
119
120         # create HotResource for each operation used for deployment:
121         # create, start, configure
122         # ignore the other operations
123         # observe the order:  create, start, configure
124         # use the current HotResource for the first operation in this order
125
126         # hold the original name since it will be changed during
127         # the transformation
128         node_name = self.name
129         reserve_current = 'NONE'
130
131         for operation in operations_deploy_sequence:
132             if operation in operations.keys():
133                 reserve_current = operation
134                 break
135
136         # create the set of SoftwareDeployment and SoftwareConfig for
137         # the interface operations
138         hosting_server = None
139         if self.nodetemplate.requirements is not None:
140             hosting_server = self._get_hosting_server()
141
142         sw_deployment_resouce = HOTSoftwareDeploymentResources(hosting_server)
143         server_key = sw_deployment_resouce.server_key
144         servers = sw_deployment_resouce.servers
145         sw_deploy_res = sw_deployment_resouce.software_deployment
146
147         # hosting_server is None if requirements is None
148         hosting_on_server = hosting_server if hosting_server else None
149         base_type = HotResource.get_base_type_str(
150             self.nodetemplate.type_definition)
151         # if we are on a compute node the host is self
152         if hosting_on_server is None and base_type == 'tosca.nodes.Compute':
153             hosting_on_server = self.name
154             servers = {'get_resource': self.name}
155
156         cwd = os.getcwd()
157         for operation in operations.values():
158             if operation.name in operations_deploy_sequence:
159                 config_name = node_name + '_' + operation.name + '_config'
160                 deploy_name = node_name + '_' + operation.name + '_deploy'
161                 if self.csar_dir:
162                     os.chdir(self.csar_dir)
163                     get_file = os.path.abspath(operation.implementation)
164                 else:
165                     get_file = operation.implementation
166                 hot_resources.append(
167                     HotResource(self.nodetemplate,
168                                 config_name,
169                                 'OS::Heat::SoftwareConfig',
170                                 {'config':
171                                     {'get_file': get_file}},
172                                 csar_dir=self.csar_dir))
173                 if operation.name == reserve_current and \
174                     base_type != 'tosca.nodes.Compute':
175                     deploy_resource = self
176                     self.name = deploy_name
177                     self.type = sw_deploy_res
178                     self.properties = {'config': {'get_resource': config_name},
179                                        server_key: servers,
180                                        'signal_transport': 'HEAT_SIGNAL'}
181                     deploy_lookup[operation] = self
182                 else:
183                     sd_config = {'config': {'get_resource': config_name},
184                                  server_key: servers,
185                                  'signal_transport': 'HEAT_SIGNAL'}
186                     deploy_resource = \
187                         HotResource(self.nodetemplate,
188                                     deploy_name,
189                                     sw_deploy_res,
190                                     sd_config, csar_dir=self.csar_dir)
191                     hot_resources.append(deploy_resource)
192                     deploy_lookup[operation] = deploy_resource
193                 lifecycle_inputs = self._get_lifecycle_inputs(operation)
194                 if lifecycle_inputs:
195                     deploy_resource.properties['input_values'] = \
196                         lifecycle_inputs
197         os.chdir(cwd)
198
199         # Add dependencies for the set of HOT resources in the sequence defined
200         # in operations_deploy_sequence
201         # TODO(anyone): find some better way to encode this implicit sequence
202         group = {}
203         op_index_min = None
204         op_index_max = -1
205         for op, hot in deploy_lookup.items():
206             # position to determine potential preceding nodes
207             op_index = operations_deploy_sequence.index(op.name)
208             if op_index_min is None or op_index < op_index_min:
209                 op_index_min = op_index
210             if op_index > op_index_max:
211                 op_index_max = op_index
212             for preceding_op_name in \
213                     reversed(operations_deploy_sequence[:op_index]):
214                 preceding_hot = deploy_lookup.get(
215                     operations.get(preceding_op_name))
216                 if preceding_hot:
217                     hot.depends_on.append(preceding_hot)
218                     hot.depends_on_nodes.append(preceding_hot)
219                     group[preceding_hot] = hot
220                     break
221
222         if op_index_max >= 0:
223             last_deploy = deploy_lookup.get(operations.get(
224                 operations_deploy_sequence[op_index_max]))
225         else:
226             last_deploy = None
227
228         # save this dependency chain in the set of HOT resources
229         self.group_dependencies.update(group)
230         for hot in hot_resources:
231             hot.group_dependencies.update(group)
232
233         roles_deploy_resource = self._handle_ansiblegalaxy_roles(
234             hot_resources, node_name, servers)
235
236         # add a dependency to this ansible roles deploy to
237         # the first "classic" deploy generated for this node
238         if roles_deploy_resource and op_index_min:
239             first_deploy = deploy_lookup.get(operations.get(
240                 operations_deploy_sequence[op_index_min]))
241             first_deploy.depends_on.append(roles_deploy_resource)
242             first_deploy.depends_on_nodes.append(roles_deploy_resource)
243
244         return hot_resources, deploy_lookup, last_deploy
245
246     def _handle_ansiblegalaxy_roles(self, hot_resources, initial_node_name,
247                                     hosting_on_server):
248         artifacts = self.get_all_artifacts(self.nodetemplate)
249         install_roles_script = ''
250
251         sw_deployment_resouce = \
252             HOTSoftwareDeploymentResources(hosting_on_server)
253         server_key = sw_deployment_resouce.server_key
254         sw_deploy_res = sw_deployment_resouce.software_deployment
255         for artifact_name, artifact in artifacts.items():
256             artifact_type = artifact.get('type', '').lower()
257             if artifact_type == 'tosca.artifacts.ansiblegalaxy.role':
258                 role = artifact.get('file', None)
259                 if role:
260                     install_roles_script += 'ansible-galaxy install ' + role \
261                                             + '\n'
262
263         if install_roles_script:
264             # remove trailing \n
265             install_roles_script = install_roles_script[:-1]
266             # add shebang and | to use literal scalar type (for multiline)
267             install_roles_script = '|\n#!/bin/bash\n' + install_roles_script
268
269             config_name = initial_node_name + '_install_roles_config'
270             deploy_name = initial_node_name + '_install_roles_deploy'
271             hot_resources.append(
272                 HotResource(self.nodetemplate, config_name,
273                             'OS::Heat::SoftwareConfig',
274                             {'config': install_roles_script},
275                             csar_dir=self.csar_dir))
276             sd_config = {'config': {'get_resource': config_name},
277                          server_key: hosting_on_server,
278                          'signal_transport': 'HEAT_SIGNAL'}
279             deploy_resource = \
280                 HotResource(self.nodetemplate, deploy_name,
281                             sw_deploy_res,
282                             sd_config, csar_dir=self.csar_dir)
283             hot_resources.append(deploy_resource)
284
285             return deploy_resource
286
287     def handle_connectsto(self, tosca_source, tosca_target, hot_source,
288                           hot_target, config_location, operation):
289         # The ConnectsTo relationship causes a configuration operation in
290         # the target.
291         # This hot resource is the software config portion in the HOT template
292         # This method adds the matching software deployment with the proper
293         # target server and dependency
294         if config_location == 'target':
295             hosting_server = hot_target._get_hosting_server()
296             hot_depends = hot_target
297         elif config_location == 'source':
298             hosting_server = self._get_hosting_server()
299             hot_depends = hot_source
300         sw_deployment_resouce = HOTSoftwareDeploymentResources(hosting_server)
301         server_key = sw_deployment_resouce.server_key
302         servers = sw_deployment_resouce.servers
303         sw_deploy_res = sw_deployment_resouce.software_deployment
304
305         deploy_name = tosca_source.name + '_' + tosca_target.name + \
306             '_connect_deploy'
307         sd_config = {'config': {'get_resource': self.name},
308                      server_key: servers,
309                      'signal_transport': 'HEAT_SIGNAL'}
310         deploy_resource = \
311             HotResource(self.nodetemplate,
312                         deploy_name,
313                         sw_deploy_res,
314                         sd_config,
315                         depends_on=[hot_depends], csar_dir=self.csar_dir)
316         connect_inputs = self._get_connect_inputs(config_location, operation)
317         if connect_inputs:
318             deploy_resource.properties['input_values'] = connect_inputs
319
320         return deploy_resource
321
322     def handle_expansion(self):
323         pass
324
325     def handle_hosting(self):
326         # handle hosting server for the OS:HEAT::SoftwareDeployment
327         # from the TOSCA nodetemplate, traverse the relationship chain
328         # down to the server
329         sw_deploy_group = \
330             HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_GROUP_RESOURCE
331         sw_deploy = HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_RESOURCE
332
333         if self.properties.get('servers') and \
334                 self.properties.get('server'):
335             del self.properties['server']
336         if self.type == sw_deploy_group or self.type == sw_deploy:
337             # skip if already have hosting
338             # If type is NodeTemplate, look up corresponding HotResrouce
339             host_server = self.properties.get('servers') \
340                 or self.properties.get('server')
341             if host_server is None:
342                 raise Exception(_("Internal Error: expecting host "
343                                   "in software deployment"))
344
345             elif isinstance(host_server.get('get_resource'), NodeTemplate):
346                 self.properties['server']['get_resource'] = \
347                     host_server['get_resource'].name
348
349             elif isinstance(host_server, dict) and \
350                 not host_server.get('get_resource'):
351                 self.properties['servers'] = \
352                     host_server
353
354     def top_of_chain(self):
355         dependent = self.group_dependencies.get(self)
356         if dependent is None:
357             return self
358         else:
359             return dependent.top_of_chain()
360
361     # this function allows to provides substacks as external files
362     # those files will be dumped along the output file.
363     #
364     # return a dict of filename-content
365     def extract_substack_templates(self, base_filename, hot_template_version):
366         return {}
367
368     # this function asks the resource to embed substacks
369     # into the main template, if any.
370     # this is used when the final output is stdout
371     def embed_substack_templates(self, hot_template_version):
372         pass
373
374     def get_dict_output(self):
375         resource_sections = OrderedDict()
376         resource_sections[TYPE] = self.type
377         if self.properties:
378             resource_sections[PROPERTIES] = self.properties
379         if self.metadata:
380             resource_sections[MEDADATA] = self.metadata
381         if self.depends_on:
382             resource_sections[DEPENDS_ON] = []
383             for depend in self.depends_on:
384                 resource_sections[DEPENDS_ON].append(depend.name)
385         if self.update_policy:
386             resource_sections[UPDATE_POLICY] = self.update_policy
387         if self.deletion_policy:
388             resource_sections[DELETION_POLICY] = self.deletion_policy
389
390         return {self.name: resource_sections}
391
392     def _get_lifecycle_inputs(self, operation):
393         # check if this lifecycle operation has input values specified
394         # extract and convert to HOT format
395         if isinstance(operation.value, six.string_types):
396             # the operation has a static string
397             return {}
398         else:
399             # the operation is a dict {'implemenation': xxx, 'input': yyy}
400             inputs = operation.value.get('inputs')
401             deploy_inputs = {}
402             if inputs:
403                 for name, value in inputs.items():
404                     deploy_inputs[name] = value
405             return deploy_inputs
406
407     def _get_connect_inputs(self, config_location, operation):
408         if config_location == 'target':
409             inputs = operation.get('pre_configure_target').get('inputs')
410         elif config_location == 'source':
411             inputs = operation.get('pre_configure_source').get('inputs')
412         deploy_inputs = {}
413         if inputs:
414             for name, value in inputs.items():
415                 deploy_inputs[name] = value
416         return deploy_inputs
417
418     def _get_hosting_server(self, node_template=None):
419         # find the server that hosts this software by checking the
420         # requirements and following the hosting chain
421         hosting_servers = []
422         host_exists = False
423         this_node_template = self.nodetemplate \
424             if node_template is None else node_template
425         for requirement in this_node_template.requirements:
426             for requirement_name, assignment in requirement.items():
427                 for check_node in this_node_template.related_nodes:
428                     # check if the capability is Container
429                     if isinstance(assignment, dict):
430                         node_name = assignment.get('node')
431                     else:
432                         node_name = assignment
433                     if node_name and node_name == check_node.name:
434                         if self._is_container_type(requirement_name,
435                                                    check_node):
436                             hosting_servers.append(check_node.name)
437                             host_exists = True
438                         elif check_node.related_nodes and not host_exists:
439                             return self._get_hosting_server(check_node)
440         if hosting_servers:
441             return hosting_servers
442         return None
443
444     def _is_container_type(self, requirement_name, node):
445         # capability is a list of dict
446         # For now just check if it's type tosca.nodes.Compute
447         # TODO(anyone): match up requirement and capability
448         base_type = HotResource.get_base_type_str(node.type_definition)
449         if base_type == 'tosca.nodes.Compute':
450             return True
451         else:
452             return False
453
454     def get_hot_attribute(self, attribute, args):
455         # this is a place holder and should be implemented by the subclass
456         # if translation is needed for the particular attribute
457         raise Exception(_("No translation in TOSCA type {0} for attribute "
458                           "{1}").format(self.nodetemplate.type, attribute))
459
460     def get_tosca_props(self):
461         tosca_props = {}
462         for prop in self.nodetemplate.get_properties_objects():
463             if isinstance(prop.value, GetInput):
464                 tosca_props[prop.name] = {'get_param': prop.value.input_name}
465             else:
466                 tosca_props[prop.name] = prop.value
467         return tosca_props
468
469     @staticmethod
470     def get_all_artifacts(nodetemplate):
471         # workaround bug in the parser
472         base_type = HotResource.get_base_type_str(nodetemplate.type_definition)
473         if base_type in policy_type:
474             artifacts = {}
475         else:
476             artifacts = nodetemplate.type_definition.get_value('artifacts',
477                                                                parent=True)
478         if not artifacts:
479             artifacts = {}
480         tpl_artifacts = nodetemplate.entity_tpl.get('artifacts')
481         if tpl_artifacts:
482             artifacts.update(tpl_artifacts)
483
484         return artifacts
485
486     @staticmethod
487     def get_all_operations(node):
488         operations = {}
489         for operation in node.interfaces:
490             operations[operation.name] = operation
491
492         node_type = node.type_definition
493         if isinstance(node_type, str) or \
494             node_type.is_derived_from("tosca.policies.Root"):
495             # node_type.type == "tosca.policies.Placement" or \
496             # node_type.type == "tosca.policies.Placement.Colocate" or \
497             # node_type.type == "tosca.policies.Placement.Antilocate":
498                 return operations
499
500         while True:
501             type_operations = HotResource._get_interface_operations_from_type(
502                 node_type, node, 'Standard')
503             type_operations.update(operations)
504             operations = type_operations
505
506             if node_type.parent_type is not None:
507                 node_type = node_type.parent_type
508             else:
509                 return operations
510
511     @staticmethod
512     def _get_interface_operations_from_type(node_type, node, lifecycle_name):
513         operations = {}
514         if isinstance(node_type, str) or \
515             node_type.is_derived_from("tosca.policies.Root"):
516             # node_type.type == "tosca.policies.Placement" or \
517             # node_type.type == "tosca.policies.Placement.Colocate" or \
518             # node_type.type == "tosca.policies.Placement.Antilocate":
519                 return operations
520         if node_type.interfaces and lifecycle_name in node_type.interfaces:
521             for name, elems in node_type.interfaces[lifecycle_name].items():
522                 # ignore empty operations (only type)
523                 # ignore global interface inputs,
524                 # concrete inputs are on the operations themselves
525                 if name != 'type' and name != 'inputs':
526                     operations[name] = InterfacesDef(node_type,
527                                                      lifecycle_name,
528                                                      node, name, elems)
529         return operations
530
531     @staticmethod
532     def get_base_type(node_type):
533         if node_type.parent_type is not None:
534             if node_type.parent_type.type.endswith('.Root') or \
535                node_type.type == "tosca.policies.Placement.Colocate" or \
536                node_type.type == "tosca.policies.Placement.Antilocate":
537                 return node_type
538             else:
539                 return HotResource.get_base_type(node_type.parent_type)
540         return node_type.type
541
542     @staticmethod
543     def get_base_type_str(node_type):
544         if isinstance(node_type, six.string_types):
545             return node_type
546         if node_type.parent_type is not None:
547             parent_type_str = None
548             if isinstance(node_type.parent_type, six.string_types):
549                 parent_type_str = node_type.parent_type
550             else:
551                 parent_type_str = node_type.parent_type.type
552
553             if parent_type_str and parent_type_str.endswith('.Root'):
554                 return node_type.type
555             else:
556                 return HotResource.get_base_type_str(node_type.parent_type)
557
558         return node_type.type
559
560
561 class HOTSoftwareDeploymentResources(object):
562     """Provides HOT Software Deployment resources
563
564     SoftwareDeployment or SoftwareDeploymentGroup Resource
565     """
566
567     HOT_SW_DEPLOYMENT_RESOURCE = 'OS::Heat::SoftwareDeployment'
568     HOT_SW_DEPLOYMENT_GROUP_RESOURCE = 'OS::Heat::SoftwareDeploymentGroup'
569
570     def __init__(self, hosting_server=None):
571         self.software_deployment = self.HOT_SW_DEPLOYMENT_RESOURCE
572         self.software_deployment_group = self.HOT_SW_DEPLOYMENT_GROUP_RESOURCE
573         self.server_key = 'server'
574         self.hosting_server = hosting_server
575         self.servers = {}
576         if hosting_server is not None:
577             if len(self.hosting_server) == 1:
578                 if isinstance(hosting_server, list):
579                     self.servers['get_resource'] = self.hosting_server[0]
580             else:
581                 for server in self.hosting_server:
582                     self.servers[server] = {'get_resource': server}
583                 self.software_deployment = self.software_deployment_group
584                 self.server_key = 'servers'