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
5 # http://www.apache.org/licenses/LICENSE-2.0
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
13 from collections import OrderedDict
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 _
24 SECTIONS = (TYPE, PROPERTIES, MEDADATA, DEPENDS_ON, UPDATE_POLICY,
26 ('type', 'properties', 'metadata',
27 'depends_on', 'update_policy', 'deletion_policy')
28 log = logging.getLogger('heat-translator')
31 class HotResource(object):
32 '''Base class for TOSCA node type translation to Heat resource type.'''
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
42 self.name = nodetemplate.name
44 self.properties = properties or {}
45 # special case for HOT softwareconfig
46 if type == 'OS::Heat::SoftwareConfig':
47 config = self.properties.get('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'
64 if self.properties.get('group') is None:
65 self.properties['group'] = 'script'
67 self.metadata = metadata
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
80 self.depends_on = depends_on
81 self.depends_on_nodes = 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
92 def handle_properties(self):
93 # the property can hold a value or the intrinsic function get_input
95 # for get_input, convert to get_param
96 for prop in self.nodetemplate.get_properties_objects():
99 def handle_life_cycle(self):
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']
106 operations = HotResource.get_all_operations(self.nodetemplate)
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
114 # hold the original name since it will be changed during
116 node_name = self.name
117 reserve_current = 'NONE'
119 for operation in operations_deploy_sequence:
120 if operation in operations.keys():
121 reserve_current = operation
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,
136 'OS::Heat::SoftwareConfig',
138 {'get_file': operation.implementation}}))
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
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':
158 'signal_transport': 'HEAT_SIGNAL'}
159 deploy_lookup[operation] = self
161 sd_config = {'config': {'get_resource': config_name},
162 'server': {'get_resource':
164 'signal_transport': 'HEAT_SIGNAL'}
166 HotResource(self.nodetemplate,
168 'OS::Heat::SoftwareDeployment',
170 hot_resources.append(deploy_resource)
171 deploy_lookup[operation] = deploy_resource
172 lifecycle_inputs = self._get_lifecycle_inputs(operation)
174 deploy_resource.properties['input_values'] = \
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
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))
192 hot.depends_on.append(preceding_hot)
193 hot.depends_on_nodes.append(preceding_hot)
194 group[preceding_hot] = hot
197 if op_index_max >= 0:
198 last_deploy = deploy_lookup.get(operations.get(
199 operations_deploy_sequence[op_index_max]))
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)
208 return hot_resources, deploy_lookup, last_deploy
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
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 + \
225 sd_config = {'config': {'get_resource': self.name},
226 'server': {'get_resource': hosting_server.name},
227 'signal_transport': 'HEAT_SIGNAL'}
229 HotResource(self.nodetemplate,
231 'OS::Heat::SoftwareDeployment',
233 depends_on=[hot_depends])
234 connect_inputs = self._get_connect_inputs(config_location, operation)
236 deploy_resource.properties['input_values'] = connect_inputs
238 return deploy_resource
240 def handle_expansion(self):
243 def handle_hosting(self):
244 # handle hosting server for the OS:HEAT::SoftwareDeployment
245 # from the TOSCA nodetemplate, traverse the relationship chain
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
258 def top_of_chain(self):
259 dependent = self.group_dependencies.get(self)
260 if dependent is None:
263 return dependent.top_of_chain()
265 def get_dict_output(self):
266 resource_sections = OrderedDict()
267 resource_sections[TYPE] = self.type
269 resource_sections[PROPERTIES] = self.properties
271 resource_sections[MEDADATA] = self.metadata
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
281 return {self.name: resource_sections}
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
290 # the operation is a dict {'implemenation': xxx, 'input': yyy}
291 inputs = operation.value.get('inputs')
294 for name, value in six.iteritems(inputs):
295 deploy_inputs[name] = value
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')
305 for name, value in six.iteritems(inputs):
306 deploy_inputs[name] = value
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')
321 node_name = assignment
322 if node_name and node_name == check_node.name:
323 if self._is_container_type(requirement_name,
326 elif check_node.related_nodes:
327 return self._get_hosting_server(check_node)
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':
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))
346 def get_tosca_props(self):
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}
352 tosca_props[prop.name] = prop.value
356 def get_all_operations(node):
358 for operation in node.interfaces:
359 operations[operation.name] = operation
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":
370 type_operations = HotResource._get_interface_operations_from_type(
371 node_type, node, 'Standard')
372 type_operations.update(operations)
373 operations = type_operations
375 if node_type.parent_type is not None:
376 node_type = node_type.parent_type
381 def _get_interface_operations_from_type(node_type, node, lifecycle_name):
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":
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,
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":
408 return HotResource.get_base_type(node_type.parent_type)