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