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