Merge "Provide file list about the difference between parser and upstreams."
[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                 if operation.name == reserve_current:
144                     deploy_resource = self
145                     self.name = deploy_name
146                     self.type = 'OS::Heat::SoftwareDeployment'
147                     self.properties = {'config': {'get_resource': config_name},
148                                        'server': {'get_resource':
149                                                   hosting_on_server},
150                                        'signal_transport': 'HEAT_SIGNAL'}
151                     deploy_lookup[operation.name] = self
152                 else:
153                     sd_config = {'config': {'get_resource': config_name},
154                                  'server': {'get_resource':
155                                             hosting_on_server},
156                                  'signal_transport': 'HEAT_SIGNAL'}
157                     deploy_resource = \
158                         HotResource(self.nodetemplate,
159                                     deploy_name,
160                                     'OS::Heat::SoftwareDeployment',
161                                     sd_config)
162                     hot_resources.append(deploy_resource)
163                     deploy_lookup[operation.name] = deploy_resource
164                 lifecycle_inputs = self._get_lifecycle_inputs(operation)
165                 if lifecycle_inputs:
166                     deploy_resource.properties['input_values'] = \
167                         lifecycle_inputs
168
169         # Add dependencies for the set of HOT resources in the sequence defined
170         # in operations_deploy_sequence
171         # TODO(anyone): find some better way to encode this implicit sequence
172         group = {}
173         for op, hot in deploy_lookup.items():
174             # position to determine potential preceding nodes
175             op_index = operations_deploy_sequence.index(op)
176             for preceding_op in \
177                     reversed(operations_deploy_sequence[:op_index]):
178                 preceding_hot = deploy_lookup.get(preceding_op)
179                 if preceding_hot:
180                     hot.depends_on.append(preceding_hot)
181                     hot.depends_on_nodes.append(preceding_hot)
182                     group[preceding_hot] = hot
183                     break
184
185         # save this dependency chain in the set of HOT resources
186         self.group_dependencies.update(group)
187         for hot in hot_resources:
188             hot.group_dependencies.update(group)
189
190         return hot_resources
191
192     def handle_connectsto(self, tosca_source, tosca_target, hot_source,
193                           hot_target, config_location, operation):
194         # The ConnectsTo relationship causes a configuration operation in
195         # the target.
196         # This hot resource is the software config portion in the HOT template
197         # This method adds the matching software deployment with the proper
198         # target server and dependency
199         if config_location == 'target':
200             hosting_server = hot_target._get_hosting_server()
201             hot_depends = hot_target
202         elif config_location == 'source':
203             hosting_server = self._get_hosting_server()
204             hot_depends = hot_source
205         deploy_name = tosca_source.name + '_' + tosca_target.name + \
206             '_connect_deploy'
207         sd_config = {'config': {'get_resource': self.name},
208                      'server': {'get_resource': hosting_server.name},
209                      'signal_transport': 'HEAT_SIGNAL'}
210         deploy_resource = \
211             HotResource(self.nodetemplate,
212                         deploy_name,
213                         'OS::Heat::SoftwareDeployment',
214                         sd_config,
215                         depends_on=[hot_depends])
216         connect_inputs = self._get_connect_inputs(config_location, operation)
217         if connect_inputs:
218             deploy_resource.properties['input_values'] = connect_inputs
219
220         return deploy_resource
221
222     def handle_expansion(self):
223         pass
224
225     def handle_hosting(self):
226         # handle hosting server for the OS:HEAT::SoftwareDeployment
227         # from the TOSCA nodetemplate, traverse the relationship chain
228         # down to the server
229         if self.type == 'OS::Heat::SoftwareDeployment':
230             # skip if already have hosting
231             # If type is NodeTemplate, look up corresponding HotResrouce
232             host_server = self.properties.get('server')
233             if host_server is None or not host_server['get_resource']:
234                 raise Exception(_("Internal Error: expecting host "
235                                   "in software deployment"))
236             elif isinstance(host_server['get_resource'], NodeTemplate):
237                 self.properties['server']['get_resource'] = \
238                     host_server['get_resource'].name
239
240     def top_of_chain(self):
241         dependent = self.group_dependencies.get(self)
242         if dependent is None:
243             return self
244         else:
245             return dependent.top_of_chain()
246
247     def get_dict_output(self):
248         resource_sections = OrderedDict()
249         resource_sections[TYPE] = self.type
250         if self.properties:
251             resource_sections[PROPERTIES] = self.properties
252         if self.metadata:
253             resource_sections[MEDADATA] = self.metadata
254         if self.depends_on:
255             resource_sections[DEPENDS_ON] = []
256             for depend in self.depends_on:
257                 resource_sections[DEPENDS_ON].append(depend.name)
258         if self.update_policy:
259             resource_sections[UPDATE_POLICY] = self.update_policy
260         if self.deletion_policy:
261             resource_sections[DELETION_POLICY] = self.deletion_policy
262
263         return {self.name: resource_sections}
264
265     def _get_lifecycle_inputs(self, operation):
266         # check if this lifecycle operation has input values specified
267         # extract and convert to HOT format
268         if isinstance(operation.value, six.string_types):
269             # the operation has a static string
270             return {}
271         else:
272             # the operation is a dict {'implemenation': xxx, 'input': yyy}
273             inputs = operation.value.get('inputs')
274             deploy_inputs = {}
275             if inputs:
276                 for name, value in six.iteritems(inputs):
277                     deploy_inputs[name] = value
278             return deploy_inputs
279
280     def _get_connect_inputs(self, config_location, operation):
281         if config_location == 'target':
282             inputs = operation.get('pre_configure_target').get('inputs')
283         elif config_location == 'source':
284             inputs = operation.get('pre_configure_source').get('inputs')
285         deploy_inputs = {}
286         if inputs:
287             for name, value in six.iteritems(inputs):
288                 deploy_inputs[name] = value
289         return deploy_inputs
290
291     def _get_hosting_server(self, node_template=None):
292         # find the server that hosts this software by checking the
293         # requirements and following the hosting chain
294         this_node_template = self.nodetemplate \
295             if node_template is None else node_template
296         for requirement in this_node_template.requirements:
297             for requirement_name, assignment in six.iteritems(requirement):
298                 for check_node in this_node_template.related_nodes:
299                     # check if the capability is Container
300                     if isinstance(assignment, dict):
301                         node_name = assignment.get('node')
302                     else:
303                         node_name = assignment
304                     if node_name and node_name == check_node.name:
305                         if self._is_container_type(requirement_name,
306                                                    check_node):
307                             return check_node
308                         elif check_node.related_nodes:
309                             return self._get_hosting_server(check_node)
310         return None
311
312     def _is_container_type(self, requirement_name, node):
313         # capability is a list of dict
314         # For now just check if it's type tosca.nodes.Compute
315         # TODO(anyone): match up requirement and capability
316         base_type = HotResource.get_base_type(node.type_definition)
317         if base_type.type == 'tosca.nodes.Compute':
318             return True
319         else:
320             return False
321
322     def get_hot_attribute(self, attribute, args):
323         # this is a place holder and should be implemented by the subclass
324         # if translation is needed for the particular attribute
325         raise Exception(_("No translation in TOSCA type {0} for attribute "
326                           "{1}").format(self.nodetemplate.type, attribute))
327
328     def get_tosca_props(self):
329         tosca_props = {}
330         for prop in self.nodetemplate.get_properties_objects():
331             if isinstance(prop.value, GetInput):
332                 tosca_props[prop.name] = {'get_param': prop.value.input_name}
333             else:
334                 tosca_props[prop.name] = prop.value
335         return tosca_props
336
337     @staticmethod
338     def _get_all_operations(node):
339         operations = {}
340         for operation in node.interfaces:
341             operations[operation.name] = operation
342
343         node_type = node.type_definition
344         if isinstance(node_type, str) or \
345             node_type.is_derived_from("tosca.policies.Root"):
346             # node_type.type == "tosca.policies.Placement" or \
347             # node_type.type == "tosca.policies.Placement.Colocate" or \
348             # node_type.type == "tosca.policies.Placement.Antilocate":
349                 return operations
350
351         while True:
352             type_operations = HotResource._get_interface_operations_from_type(
353                 node_type, node, 'Standard')
354             type_operations.update(operations)
355             operations = type_operations
356
357             if node_type.parent_type is not None:
358                 node_type = node_type.parent_type
359             else:
360                 return operations
361
362     @staticmethod
363     def _get_interface_operations_from_type(node_type, node, lifecycle_name):
364         operations = {}
365         if isinstance(node_type, str) or \
366             node_type.is_derived_from("tosca.policies.Root"):
367             # node_type.type == "tosca.policies.Placement" or \
368             # node_type.type == "tosca.policies.Placement.Colocate" or \
369             # node_type.type == "tosca.policies.Placement.Antilocate":
370                 return operations
371         if node_type.interfaces and lifecycle_name in node_type.interfaces:
372             for name, elems in node_type.interfaces[lifecycle_name].items():
373                 # ignore empty operations (only type)
374                 # ignore global interface inputs,
375                 # concrete inputs are on the operations themselves
376                 if name != 'type' and name != 'inputs':
377                     operations[name] = InterfacesDef(node_type,
378                                                      lifecycle_name,
379                                                      node, name, elems)
380         return operations
381
382     @staticmethod
383     def get_base_type(node_type):
384         if node_type.parent_type is not None:
385             if node_type.parent_type.type.endswith('.Root') or \
386                node_type.type == "tosca.policies.Placement.Colocate" or \
387                node_type.type == "tosca.policies.Placement.Antilocate":
388                 return node_type
389             else:
390                 return HotResource.get_base_type(node_type.parent_type)
391         else:
392             return node_type