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