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')
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']
35 log = logging.getLogger('heat-translator')
38 class HotResource(object):
39 '''Base class for TOSCA node type translation to Heat resource type.'''
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
49 self.name = nodetemplate.name
51 self.properties = properties or {}
53 self.csar_dir = csar_dir
54 # special case for HOT softwareconfig
56 if type == 'OS::Heat::SoftwareConfig':
57 config = self.properties.get('config')
58 if isinstance(config, dict):
60 os.chdir(self.csar_dir)
61 implementation_artifact = os.path.abspath(config.get(
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'
79 if self.properties.get('group') is None:
80 self.properties['group'] = 'script'
82 self.metadata = metadata
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
95 self.depends_on = depends_on
96 self.depends_on_nodes = 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
107 def handle_properties(self):
108 # the property can hold a value or the intrinsic function get_input
110 # for get_input, convert to get_param
111 for prop in self.nodetemplate.get_properties_objects():
114 def handle_life_cycle(self):
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']
121 operations = HotResource.get_all_operations(self.nodetemplate)
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
129 # hold the original name since it will be changed during
131 node_name = self.name
132 reserve_current = 'NONE'
134 for operation in operations_deploy_sequence:
135 if operation in operations.keys():
136 reserve_current = operation
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()
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
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}
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'
165 os.chdir(self.csar_dir)
166 get_file = os.path.abspath(operation.implementation)
168 get_file = operation.implementation
169 hot_resources.append(
170 HotResource(self.nodetemplate,
172 'OS::Heat::SoftwareConfig',
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},
183 'signal_transport': 'HEAT_SIGNAL'}
184 deploy_lookup[operation] = self
186 sd_config = {'config': {'get_resource': config_name},
188 'signal_transport': 'HEAT_SIGNAL'}
190 HotResource(self.nodetemplate,
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)
198 deploy_resource.properties['input_values'] = \
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
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))
220 hot.depends_on.append(preceding_hot)
221 hot.depends_on_nodes.append(preceding_hot)
222 group[preceding_hot] = hot
225 if op_index_max >= 0:
226 last_deploy = deploy_lookup.get(operations.get(
227 operations_deploy_sequence[op_index_max]))
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)
236 roles_deploy_resource = self._handle_ansiblegalaxy_roles(
237 hot_resources, node_name, servers)
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)
247 return hot_resources, deploy_lookup, last_deploy
249 def _handle_ansiblegalaxy_roles(self, hot_resources, initial_node_name,
251 artifacts = self.get_all_artifacts(self.nodetemplate)
252 install_roles_script = ''
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)
263 install_roles_script += 'ansible-galaxy install ' + role \
266 if install_roles_script:
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
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'}
283 HotResource(self.nodetemplate, deploy_name,
285 sd_config, csar_dir=self.csar_dir)
286 hot_resources.append(deploy_resource)
288 return deploy_resource
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
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
308 deploy_name = tosca_source.name + '_' + tosca_target.name + \
310 sd_config = {'config': {'get_resource': self.name},
312 'signal_transport': 'HEAT_SIGNAL'}
314 HotResource(self.nodetemplate,
318 depends_on=[hot_depends], csar_dir=self.csar_dir)
319 connect_inputs = self._get_connect_inputs(config_location, operation)
321 deploy_resource.properties['input_values'] = connect_inputs
323 return deploy_resource
325 def handle_expansion(self):
328 def handle_hosting(self):
329 # handle hosting server for the OS:HEAT::SoftwareDeployment
330 # from the TOSCA nodetemplate, traverse the relationship chain
333 HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_GROUP_RESOURCE
334 sw_deploy = HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_RESOURCE
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"))
348 elif isinstance(host_server.get('get_resource'), NodeTemplate):
349 self.properties['server']['get_resource'] = \
350 host_server['get_resource'].name
352 elif isinstance(host_server, dict) and \
353 not host_server.get('get_resource'):
354 self.properties['servers'] = \
357 def top_of_chain(self):
358 dependent = self.group_dependencies.get(self)
359 if dependent is None:
362 return dependent.top_of_chain()
364 # this function allows to provides substacks as external files
365 # those files will be dumped along the output file.
367 # return a dict of filename-content
368 def extract_substack_templates(self, base_filename, hot_template_version):
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):
377 def get_dict_output(self):
378 resource_sections = OrderedDict()
379 resource_sections[TYPE] = self.type
381 resource_sections[PROPERTIES] = self.properties
383 resource_sections[MEDADATA] = self.metadata
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
393 return {self.name: resource_sections}
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
402 # the operation is a dict {'implemenation': xxx, 'input': yyy}
403 inputs = operation.value.get('inputs')
406 for name, value in inputs.items():
407 deploy_inputs[name] = value
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')
417 for name, value in inputs.items():
418 deploy_inputs[name] = value
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
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')
435 node_name = assignment
436 if node_name and node_name == check_node.name:
437 if self._is_container_type(requirement_name,
439 hosting_servers.append(check_node.name)
441 elif check_node.related_nodes and not host_exists:
442 return self._get_hosting_server(check_node)
444 return hosting_servers
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':
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))
463 def get_tosca_props(self):
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}
469 tosca_props[prop.name] = prop.value
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:
479 artifacts = nodetemplate.type_definition.get_value('artifacts',
483 tpl_artifacts = nodetemplate.entity_tpl.get('artifacts')
485 artifacts.update(tpl_artifacts)
490 def get_all_operations(node):
492 for operation in node.interfaces:
493 operations[operation.name] = operation
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":
504 type_operations = HotResource._get_interface_operations_from_type(
505 node_type, node, 'Standard')
506 type_operations.update(operations)
507 operations = type_operations
509 if node_type.parent_type is not None:
510 node_type = node_type.parent_type
515 def _get_interface_operations_from_type(node_type, node, lifecycle_name):
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":
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,
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":
542 return HotResource.get_base_type(node_type.parent_type)
543 return node_type.type
546 def get_base_type_str(node_type):
547 if isinstance(node_type, six.string_types):
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
554 parent_type_str = node_type.parent_type.type
556 if parent_type_str and parent_type_str.endswith('.Root'):
557 return node_type.type
559 return HotResource.get_base_type_str(node_type.parent_type)
561 return node_type.type
564 class HOTSoftwareDeploymentResources(object):
565 """Provides HOT Software Deployment resources
567 SoftwareDeployment or SoftwareDeploymentGroup Resource
570 HOT_SW_DEPLOYMENT_RESOURCE = 'OS::Heat::SoftwareDeployment'
571 HOT_SW_DEPLOYMENT_GROUP_RESOURCE = 'OS::Heat::SoftwareDeploymentGroup'
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
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]
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'