Add signal_transport in software deployment resource
[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 log = logging.getLogger('heat-translator')
29
30
31 class HotResource(object):
32     '''Base class for TOSCA node type translation to Heat resource type.'''
33
34     def __init__(self, nodetemplate, name=None, type=None, properties=None,
35                  metadata=None, depends_on=None,
36                  update_policy=None, deletion_policy=None):
37         log.debug(_('Translating TOSCA node type to HOT resource type.'))
38         self.nodetemplate = nodetemplate
39         if name:
40             self.name = name
41         else:
42             self.name = nodetemplate.name
43         self.type = type
44         self.properties = properties or {}
45         # special case for HOT softwareconfig
46         if type == 'OS::Heat::SoftwareConfig':
47             config = self.properties.get('config')
48             if config:
49                 implementation_artifact = config.get('get_file')
50                 if implementation_artifact:
51                     filename, file_extension = os.path.splitext(
52                         implementation_artifact)
53                     file_extension = file_extension.lower()
54                     # artifact_types should be read to find the exact script
55                     # type, unfortunately artifact_types doesn't seem to be
56                     # supported by the parser
57                     if file_extension == '.ansible' \
58                             or file_extension == '.yaml' \
59                             or file_extension == '.yml':
60                         self.properties['group'] = 'ansible'
61                     if file_extension == '.pp':
62                         self.properties['group'] = 'puppet'
63
64             if self.properties.get('group') is None:
65                 self.properties['group'] = 'script'
66
67         self.metadata = metadata
68
69         # The difference between depends_on and depends_on_nodes is
70         # that depends_on defines dependency in the context of the
71         # HOT template and it is used during the template output.
72         # Depends_on_nodes defines the direct dependency between the
73         # tosca nodes and is not used during the output of the
74         # HOT template but for internal processing only. When a tosca
75         # node depends on another node it will be always added to
76         # depends_on_nodes but not always to depends_on. For example
77         # if the source of dependency is a server, the dependency will
78         # be added as properties.get_resource and not depends_on
79         if depends_on:
80             self.depends_on = depends_on
81             self.depends_on_nodes = depends_on
82         else:
83             self.depends_on = []
84             self.depends_on_nodes = []
85         self.update_policy = update_policy
86         self.deletion_policy = deletion_policy
87         self.group_dependencies = {}
88         # if hide_resource is set to true, then this resource will not be
89         # generated in the output yaml.
90         self.hide_resource = False
91
92     def handle_properties(self):
93         # the property can hold a value or the intrinsic function get_input
94         # for value, copy it
95         # for get_input, convert to get_param
96         for prop in self.nodetemplate.get_properties_objects():
97             pass
98
99     def handle_life_cycle(self):
100         hot_resources = []
101         deploy_lookup = {}
102         # TODO(anyone):  sequence for life cycle needs to cover different
103         # scenarios and cannot be fixed or hard coded here
104         operations_deploy_sequence = ['create', 'configure', 'start']
105
106         operations = HotResource.get_all_operations(self.nodetemplate)
107
108         # create HotResource for each operation used for deployment:
109         # create, start, configure
110         # ignore the other operations
111         # observe the order:  create, start, configure
112         # use the current HotResource for the first operation in this order
113
114         # hold the original name since it will be changed during
115         # the transformation
116         node_name = self.name
117         reserve_current = 'NONE'
118
119         for operation in operations_deploy_sequence:
120             if operation in operations.keys():
121                 reserve_current = operation
122                 break
123
124         # create the set of SoftwareDeployment and SoftwareConfig for
125         # the interface operations
126         hosting_server = None
127         if self.nodetemplate.requirements is not None:
128             hosting_server = self._get_hosting_server()
129         for operation in operations.values():
130             if operation.name in operations_deploy_sequence:
131                 config_name = node_name + '_' + operation.name + '_config'
132                 deploy_name = node_name + '_' + operation.name + '_deploy'
133                 hot_resources.append(
134                     HotResource(self.nodetemplate,
135                                 config_name,
136                                 'OS::Heat::SoftwareConfig',
137                                 {'config':
138                                     {'get_file': operation.implementation}}))
139
140                 # hosting_server is None if requirements is None
141                 hosting_on_server = (hosting_server.name if
142                                      hosting_server else None)
143                 base_type = HotResource.get_base_type(
144                     self.nodetemplate.type_definition).type
145                 # handle interfaces directly defined on a compute
146                 if hosting_on_server is None \
147                     and base_type == 'tosca.nodes.Compute':
148                     hosting_on_server = self.name
149
150                 if operation.name == reserve_current and \
151                     base_type != 'tosca.nodes.Compute':
152                     deploy_resource = self
153                     self.name = deploy_name
154                     self.type = 'OS::Heat::SoftwareDeployment'
155                     self.properties = {'config': {'get_resource': config_name},
156                                        'server': {'get_resource':
157                                                   hosting_on_server},
158                                        'signal_transport': 'HEAT_SIGNAL'}
159                     deploy_lookup[operation] = self
160                 else:
161                     sd_config = {'config': {'get_resource': config_name},
162                                  'server': {'get_resource':
163                                             hosting_on_server},
164                                  'signal_transport': 'HEAT_SIGNAL'}
165                     deploy_resource = \
166                         HotResource(self.nodetemplate,
167                                     deploy_name,
168                                     'OS::Heat::SoftwareDeployment',
169                                     sd_config)
170                     hot_resources.append(deploy_resource)
171                     deploy_lookup[operation] = deploy_resource
172                 lifecycle_inputs = self._get_lifecycle_inputs(operation)
173                 if lifecycle_inputs:
174                     deploy_resource.properties['input_values'] = \
175                         lifecycle_inputs
176
177         # Add dependencies for the set of HOT resources in the sequence defined
178         # in operations_deploy_sequence
179         # TODO(anyone): find some better way to encode this implicit sequence
180         group = {}
181         op_index_max = -1
182         for op, hot in deploy_lookup.items():
183             # position to determine potential preceding nodes
184             op_index = operations_deploy_sequence.index(op.name)
185             if op_index > op_index_max:
186                 op_index_max = op_index
187             for preceding_op_name in \
188                     reversed(operations_deploy_sequence[:op_index]):
189                 preceding_hot = deploy_lookup.get(
190                     operations.get(preceding_op_name))
191                 if preceding_hot:
192                     hot.depends_on.append(preceding_hot)
193                     hot.depends_on_nodes.append(preceding_hot)
194                     group[preceding_hot] = hot
195                     break
196
197         if op_index_max >= 0:
198             last_deploy = deploy_lookup.get(operations.get(
199                 operations_deploy_sequence[op_index_max]))
200         else:
201             last_deploy = None
202
203         # save this dependency chain in the set of HOT resources
204         self.group_dependencies.update(group)
205         for hot in hot_resources:
206             hot.group_dependencies.update(group)
207
208         return hot_resources, deploy_lookup, last_deploy
209
210     def handle_connectsto(self, tosca_source, tosca_target, hot_source,
211                           hot_target, config_location, operation):
212         # The ConnectsTo relationship causes a configuration operation in
213         # the target.
214         # This hot resource is the software config portion in the HOT template
215         # This method adds the matching software deployment with the proper
216         # target server and dependency
217         if config_location == 'target':
218             hosting_server = hot_target._get_hosting_server()
219             hot_depends = hot_target
220         elif config_location == 'source':
221             hosting_server = self._get_hosting_server()
222             hot_depends = hot_source
223         deploy_name = tosca_source.name + '_' + tosca_target.name + \
224             '_connect_deploy'
225         sd_config = {'config': {'get_resource': self.name},
226                      'server': {'get_resource': hosting_server.name},
227                      'signal_transport': 'HEAT_SIGNAL'}
228         deploy_resource = \
229             HotResource(self.nodetemplate,
230                         deploy_name,
231                         'OS::Heat::SoftwareDeployment',
232                         sd_config,
233                         depends_on=[hot_depends])
234         connect_inputs = self._get_connect_inputs(config_location, operation)
235         if connect_inputs:
236             deploy_resource.properties['input_values'] = connect_inputs
237
238         return deploy_resource
239
240     def handle_expansion(self):
241         pass
242
243     def handle_hosting(self):
244         # handle hosting server for the OS:HEAT::SoftwareDeployment
245         # from the TOSCA nodetemplate, traverse the relationship chain
246         # down to the server
247         if self.type == 'OS::Heat::SoftwareDeployment':
248             # skip if already have hosting
249             # If type is NodeTemplate, look up corresponding HotResrouce
250             host_server = self.properties.get('server')
251             if host_server is None or not host_server['get_resource']:
252                 raise Exception(_("Internal Error: expecting host "
253                                   "in software deployment"))
254             elif isinstance(host_server['get_resource'], NodeTemplate):
255                 self.properties['server']['get_resource'] = \
256                     host_server['get_resource'].name
257
258     def top_of_chain(self):
259         dependent = self.group_dependencies.get(self)
260         if dependent is None:
261             return self
262         else:
263             return dependent.top_of_chain()
264
265     def get_dict_output(self):
266         resource_sections = OrderedDict()
267         resource_sections[TYPE] = self.type
268         if self.properties:
269             resource_sections[PROPERTIES] = self.properties
270         if self.metadata:
271             resource_sections[MEDADATA] = self.metadata
272         if self.depends_on:
273             resource_sections[DEPENDS_ON] = []
274             for depend in self.depends_on:
275                 resource_sections[DEPENDS_ON].append(depend.name)
276         if self.update_policy:
277             resource_sections[UPDATE_POLICY] = self.update_policy
278         if self.deletion_policy:
279             resource_sections[DELETION_POLICY] = self.deletion_policy
280
281         return {self.name: resource_sections}
282
283     def _get_lifecycle_inputs(self, operation):
284         # check if this lifecycle operation has input values specified
285         # extract and convert to HOT format
286         if isinstance(operation.value, six.string_types):
287             # the operation has a static string
288             return {}
289         else:
290             # the operation is a dict {'implemenation': xxx, 'input': yyy}
291             inputs = operation.value.get('inputs')
292             deploy_inputs = {}
293             if inputs:
294                 for name, value in six.iteritems(inputs):
295                     deploy_inputs[name] = value
296             return deploy_inputs
297
298     def _get_connect_inputs(self, config_location, operation):
299         if config_location == 'target':
300             inputs = operation.get('pre_configure_target').get('inputs')
301         elif config_location == 'source':
302             inputs = operation.get('pre_configure_source').get('inputs')
303         deploy_inputs = {}
304         if inputs:
305             for name, value in six.iteritems(inputs):
306                 deploy_inputs[name] = value
307         return deploy_inputs
308
309     def _get_hosting_server(self, node_template=None):
310         # find the server that hosts this software by checking the
311         # requirements and following the hosting chain
312         this_node_template = self.nodetemplate \
313             if node_template is None else node_template
314         for requirement in this_node_template.requirements:
315             for requirement_name, assignment in six.iteritems(requirement):
316                 for check_node in this_node_template.related_nodes:
317                     # check if the capability is Container
318                     if isinstance(assignment, dict):
319                         node_name = assignment.get('node')
320                     else:
321                         node_name = assignment
322                     if node_name and node_name == check_node.name:
323                         if self._is_container_type(requirement_name,
324                                                    check_node):
325                             return check_node
326                         elif check_node.related_nodes:
327                             return self._get_hosting_server(check_node)
328         return None
329
330     def _is_container_type(self, requirement_name, node):
331         # capability is a list of dict
332         # For now just check if it's type tosca.nodes.Compute
333         # TODO(anyone): match up requirement and capability
334         base_type = HotResource.get_base_type(node.type_definition)
335         if base_type.type == 'tosca.nodes.Compute':
336             return True
337         else:
338             return False
339
340     def get_hot_attribute(self, attribute, args):
341         # this is a place holder and should be implemented by the subclass
342         # if translation is needed for the particular attribute
343         raise Exception(_("No translation in TOSCA type {0} for attribute "
344                           "{1}").format(self.nodetemplate.type, attribute))
345
346     def get_tosca_props(self):
347         tosca_props = {}
348         for prop in self.nodetemplate.get_properties_objects():
349             if isinstance(prop.value, GetInput):
350                 tosca_props[prop.name] = {'get_param': prop.value.input_name}
351             else:
352                 tosca_props[prop.name] = prop.value
353         return tosca_props
354
355     @staticmethod
356     def get_all_operations(node):
357         operations = {}
358         for operation in node.interfaces:
359             operations[operation.name] = operation
360
361         node_type = node.type_definition
362         if isinstance(node_type, str) or \
363             node_type.is_derived_from("tosca.policies.Root"):
364             # node_type.type == "tosca.policies.Placement" or \
365             # node_type.type == "tosca.policies.Placement.Colocate" or \
366             # node_type.type == "tosca.policies.Placement.Antilocate":
367                 return operations
368
369         while True:
370             type_operations = HotResource._get_interface_operations_from_type(
371                 node_type, node, 'Standard')
372             type_operations.update(operations)
373             operations = type_operations
374
375             if node_type.parent_type is not None:
376                 node_type = node_type.parent_type
377             else:
378                 return operations
379
380     @staticmethod
381     def _get_interface_operations_from_type(node_type, node, lifecycle_name):
382         operations = {}
383         if isinstance(node_type, str) or \
384             node_type.is_derived_from("tosca.policies.Root"):
385             # node_type.type == "tosca.policies.Placement" or \
386             # node_type.type == "tosca.policies.Placement.Colocate" or \
387             # node_type.type == "tosca.policies.Placement.Antilocate":
388                 return operations
389         if node_type.interfaces and lifecycle_name in node_type.interfaces:
390             for name, elems in node_type.interfaces[lifecycle_name].items():
391                 # ignore empty operations (only type)
392                 # ignore global interface inputs,
393                 # concrete inputs are on the operations themselves
394                 if name != 'type' and name != 'inputs':
395                     operations[name] = InterfacesDef(node_type,
396                                                      lifecycle_name,
397                                                      node, name, elems)
398         return operations
399
400     @staticmethod
401     def get_base_type(node_type):
402         if node_type.parent_type is not None:
403             if node_type.parent_type.type.endswith('.Root') or \
404                node_type.type == "tosca.policies.Placement.Colocate" or \
405                node_type.type == "tosca.policies.Placement.Antilocate":
406                 return node_type
407             else:
408                 return HotResource.get_base_type(node_type.parent_type)
409         else:
410             return node_type