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',
34 'tosca.policies.Monitoring']
36 log = logging.getLogger('heat-translator')
39 class HotResource(object):
40 '''Base class for TOSCA node type translation to Heat resource type.'''
42 def __init__(self, nodetemplate, name=None, type=None, properties=None,
43 metadata=None, depends_on=None,
44 update_policy=None, deletion_policy=None, csar_dir=None):
45 log.debug(_('Translating TOSCA node type to HOT resource type.'))
46 self.nodetemplate = nodetemplate
50 self.name = nodetemplate.name
52 self.properties = properties or {}
54 self.csar_dir = csar_dir
55 # special case for HOT softwareconfig
57 if type == 'OS::Heat::SoftwareConfig':
58 config = self.properties.get('config')
59 if isinstance(config, dict):
61 os.chdir(self.csar_dir)
62 implementation_artifact = os.path.abspath(config.get(
65 implementation_artifact = config.get('get_file')
66 if implementation_artifact:
67 filename, file_extension = os.path.splitext(
68 implementation_artifact)
69 file_extension = file_extension.lower()
70 # artifact_types should be read to find the exact script
71 # type, unfortunately artifact_types doesn't seem to be
72 # supported by the parser
73 if file_extension == '.ansible' \
74 or file_extension == '.yaml' \
75 or file_extension == '.yml':
76 self.properties['group'] = 'ansible'
77 if file_extension == '.pp':
78 self.properties['group'] = 'puppet'
80 if self.properties.get('group') is None:
81 self.properties['group'] = 'script'
83 self.metadata = metadata
85 # The difference between depends_on and depends_on_nodes is
86 # that depends_on defines dependency in the context of the
87 # HOT template and it is used during the template output.
88 # Depends_on_nodes defines the direct dependency between the
89 # tosca nodes and is not used during the output of the
90 # HOT template but for internal processing only. When a tosca
91 # node depends on another node it will be always added to
92 # depends_on_nodes but not always to depends_on. For example
93 # if the source of dependency is a server, the dependency will
94 # be added as properties.get_resource and not depends_on
96 self.depends_on = depends_on
97 self.depends_on_nodes = depends_on
100 self.depends_on_nodes = []
101 self.update_policy = update_policy
102 self.deletion_policy = deletion_policy
103 self.group_dependencies = {}
104 # if hide_resource is set to true, then this resource will not be
105 # generated in the output yaml.
106 self.hide_resource = False
108 def handle_properties(self):
109 # the property can hold a value or the intrinsic function get_input
111 # for get_input, convert to get_param
112 for prop in self.nodetemplate.get_properties_objects():
115 def handle_life_cycle(self):
118 # TODO(anyone): sequence for life cycle needs to cover different
119 # scenarios and cannot be fixed or hard coded here
120 operations_deploy_sequence = ['create', 'configure', 'start']
122 operations = HotResource.get_all_operations(self.nodetemplate)
124 # create HotResource for each operation used for deployment:
125 # create, start, configure
126 # ignore the other operations
127 # observe the order: create, start, configure
128 # use the current HotResource for the first operation in this order
130 # hold the original name since it will be changed during
132 node_name = self.name
133 reserve_current = 'NONE'
135 for operation in operations_deploy_sequence:
136 if operation in operations.keys():
137 reserve_current = operation
140 # create the set of SoftwareDeployment and SoftwareConfig for
141 # the interface operations
142 hosting_server = None
143 if self.nodetemplate.requirements is not None:
144 hosting_server = self._get_hosting_server()
146 sw_deployment_resouce = HOTSoftwareDeploymentResources(hosting_server)
147 server_key = sw_deployment_resouce.server_key
148 servers = sw_deployment_resouce.servers
149 sw_deploy_res = sw_deployment_resouce.software_deployment
151 # hosting_server is None if requirements is None
152 hosting_on_server = hosting_server if hosting_server else None
153 base_type = HotResource.get_base_type_str(
154 self.nodetemplate.type_definition)
155 # if we are on a compute node the host is self
156 if hosting_on_server is None and base_type == 'tosca.nodes.Compute':
157 hosting_on_server = self.name
158 servers = {'get_resource': self.name}
161 for operation in operations.values():
162 if operation.name in operations_deploy_sequence:
163 config_name = node_name + '_' + operation.name + '_config'
164 deploy_name = node_name + '_' + operation.name + '_deploy'
166 os.chdir(self.csar_dir)
167 get_file = os.path.abspath(operation.implementation)
169 get_file = operation.implementation
170 hot_resources.append(
171 HotResource(self.nodetemplate,
173 'OS::Heat::SoftwareConfig',
175 {'get_file': get_file}},
176 csar_dir=self.csar_dir))
177 if operation.name == reserve_current and \
178 base_type != 'tosca.nodes.Compute':
179 deploy_resource = self
180 self.name = deploy_name
181 self.type = sw_deploy_res
182 self.properties = {'config': {'get_resource': config_name},
184 'signal_transport': 'HEAT_SIGNAL'}
185 deploy_lookup[operation] = self
187 sd_config = {'config': {'get_resource': config_name},
189 'signal_transport': 'HEAT_SIGNAL'}
191 HotResource(self.nodetemplate,
194 sd_config, csar_dir=self.csar_dir)
195 hot_resources.append(deploy_resource)
196 deploy_lookup[operation] = deploy_resource
197 lifecycle_inputs = self._get_lifecycle_inputs(operation)
199 deploy_resource.properties['input_values'] = \
203 # Add dependencies for the set of HOT resources in the sequence defined
204 # in operations_deploy_sequence
205 # TODO(anyone): find some better way to encode this implicit sequence
209 for op, hot in deploy_lookup.items():
210 # position to determine potential preceding nodes
211 op_index = operations_deploy_sequence.index(op.name)
212 if op_index_min is None or op_index < op_index_min:
213 op_index_min = op_index
214 if op_index > op_index_max:
215 op_index_max = op_index
216 for preceding_op_name in \
217 reversed(operations_deploy_sequence[:op_index]):
218 preceding_hot = deploy_lookup.get(
219 operations.get(preceding_op_name))
221 hot.depends_on.append(preceding_hot)
222 hot.depends_on_nodes.append(preceding_hot)
223 group[preceding_hot] = hot
226 if op_index_max >= 0:
227 last_deploy = deploy_lookup.get(operations.get(
228 operations_deploy_sequence[op_index_max]))
232 # save this dependency chain in the set of HOT resources
233 self.group_dependencies.update(group)
234 for hot in hot_resources:
235 hot.group_dependencies.update(group)
237 roles_deploy_resource = self._handle_ansiblegalaxy_roles(
238 hot_resources, node_name, servers)
240 # add a dependency to this ansible roles deploy to
241 # the first "classic" deploy generated for this node
242 if roles_deploy_resource and op_index_min:
243 first_deploy = deploy_lookup.get(operations.get(
244 operations_deploy_sequence[op_index_min]))
245 first_deploy.depends_on.append(roles_deploy_resource)
246 first_deploy.depends_on_nodes.append(roles_deploy_resource)
248 return hot_resources, deploy_lookup, last_deploy
250 def _handle_ansiblegalaxy_roles(self, hot_resources, initial_node_name,
252 artifacts = self.get_all_artifacts(self.nodetemplate)
253 install_roles_script = ''
255 sw_deployment_resouce = \
256 HOTSoftwareDeploymentResources(hosting_on_server)
257 server_key = sw_deployment_resouce.server_key
258 sw_deploy_res = sw_deployment_resouce.software_deployment
259 for artifact_name, artifact in artifacts.items():
260 artifact_type = artifact.get('type', '').lower()
261 if artifact_type == 'tosca.artifacts.ansiblegalaxy.role':
262 role = artifact.get('file', None)
264 install_roles_script += 'ansible-galaxy install ' + role \
267 if install_roles_script:
269 install_roles_script = install_roles_script[:-1]
270 # add shebang and | to use literal scalar type (for multiline)
271 install_roles_script = '|\n#!/bin/bash\n' + install_roles_script
273 config_name = initial_node_name + '_install_roles_config'
274 deploy_name = initial_node_name + '_install_roles_deploy'
275 hot_resources.append(
276 HotResource(self.nodetemplate, config_name,
277 'OS::Heat::SoftwareConfig',
278 {'config': install_roles_script},
279 csar_dir=self.csar_dir))
280 sd_config = {'config': {'get_resource': config_name},
281 server_key: hosting_on_server,
282 'signal_transport': 'HEAT_SIGNAL'}
284 HotResource(self.nodetemplate, deploy_name,
286 sd_config, csar_dir=self.csar_dir)
287 hot_resources.append(deploy_resource)
289 return deploy_resource
291 def handle_connectsto(self, tosca_source, tosca_target, hot_source,
292 hot_target, config_location, operation):
293 # The ConnectsTo relationship causes a configuration operation in
295 # This hot resource is the software config portion in the HOT template
296 # This method adds the matching software deployment with the proper
297 # target server and dependency
298 if config_location == 'target':
299 hosting_server = hot_target._get_hosting_server()
300 hot_depends = hot_target
301 elif config_location == 'source':
302 hosting_server = self._get_hosting_server()
303 hot_depends = hot_source
304 sw_deployment_resouce = HOTSoftwareDeploymentResources(hosting_server)
305 server_key = sw_deployment_resouce.server_key
306 servers = sw_deployment_resouce.servers
307 sw_deploy_res = sw_deployment_resouce.software_deployment
309 deploy_name = tosca_source.name + '_' + tosca_target.name + \
311 sd_config = {'config': {'get_resource': self.name},
313 'signal_transport': 'HEAT_SIGNAL'}
315 HotResource(self.nodetemplate,
319 depends_on=[hot_depends], csar_dir=self.csar_dir)
320 connect_inputs = self._get_connect_inputs(config_location, operation)
322 deploy_resource.properties['input_values'] = connect_inputs
324 return deploy_resource
326 def handle_expansion(self):
329 def handle_hosting(self):
330 # handle hosting server for the OS:HEAT::SoftwareDeployment
331 # from the TOSCA nodetemplate, traverse the relationship chain
334 HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_GROUP_RESOURCE
335 sw_deploy = HOTSoftwareDeploymentResources.HOT_SW_DEPLOYMENT_RESOURCE
337 if self.properties.get('servers') and \
338 self.properties.get('server'):
339 del self.properties['server']
340 if self.type == sw_deploy_group or self.type == sw_deploy:
341 # skip if already have hosting
342 # If type is NodeTemplate, look up corresponding HotResrouce
343 host_server = self.properties.get('servers') \
344 or self.properties.get('server')
345 if host_server is None:
346 raise Exception(_("Internal Error: expecting host "
347 "in software deployment"))
349 elif isinstance(host_server.get('get_resource'), NodeTemplate):
350 self.properties['server']['get_resource'] = \
351 host_server['get_resource'].name
353 elif isinstance(host_server, dict) and \
354 not host_server.get('get_resource'):
355 self.properties['servers'] = \
358 def top_of_chain(self):
359 dependent = self.group_dependencies.get(self)
360 if dependent is None:
363 return dependent.top_of_chain()
365 # this function allows to provides substacks as external files
366 # those files will be dumped along the output file.
368 # return a dict of filename-content
369 def extract_substack_templates(self, base_filename, hot_template_version):
372 # this function asks the resource to embed substacks
373 # into the main template, if any.
374 # this is used when the final output is stdout
375 def embed_substack_templates(self, hot_template_version):
378 def get_dict_output(self):
379 resource_sections = OrderedDict()
380 resource_sections[TYPE] = self.type
382 resource_sections[PROPERTIES] = self.properties
384 resource_sections[MEDADATA] = self.metadata
386 resource_sections[DEPENDS_ON] = []
387 for depend in self.depends_on:
388 resource_sections[DEPENDS_ON].append(depend.name)
389 if self.update_policy:
390 resource_sections[UPDATE_POLICY] = self.update_policy
391 if self.deletion_policy:
392 resource_sections[DELETION_POLICY] = self.deletion_policy
394 return {self.name: resource_sections}
396 def _get_lifecycle_inputs(self, operation):
397 # check if this lifecycle operation has input values specified
398 # extract and convert to HOT format
399 if isinstance(operation.value, six.string_types):
400 # the operation has a static string
403 # the operation is a dict {'implemenation': xxx, 'input': yyy}
404 inputs = operation.value.get('inputs')
407 for name, value in inputs.items():
408 deploy_inputs[name] = value
411 def _get_connect_inputs(self, config_location, operation):
412 if config_location == 'target':
413 inputs = operation.get('pre_configure_target').get('inputs')
414 elif config_location == 'source':
415 inputs = operation.get('pre_configure_source').get('inputs')
418 for name, value in inputs.items():
419 deploy_inputs[name] = value
422 def _get_hosting_server(self, node_template=None):
423 # find the server that hosts this software by checking the
424 # requirements and following the hosting chain
427 this_node_template = self.nodetemplate \
428 if node_template is None else node_template
429 for requirement in this_node_template.requirements:
430 for requirement_name, assignment in requirement.items():
431 for check_node in this_node_template.related_nodes:
432 # check if the capability is Container
433 if isinstance(assignment, dict):
434 node_name = assignment.get('node')
436 node_name = assignment
437 if node_name and node_name == check_node.name:
438 if self._is_container_type(requirement_name,
440 hosting_servers.append(check_node.name)
442 elif check_node.related_nodes and not host_exists:
443 return self._get_hosting_server(check_node)
445 return hosting_servers
448 def _is_container_type(self, requirement_name, node):
449 # capability is a list of dict
450 # For now just check if it's type tosca.nodes.Compute
451 # TODO(anyone): match up requirement and capability
452 base_type = HotResource.get_base_type_str(node.type_definition)
453 if base_type == 'tosca.nodes.Compute':
458 def get_hot_attribute(self, attribute, args):
459 # this is a place holder and should be implemented by the subclass
460 # if translation is needed for the particular attribute
461 raise Exception(_("No translation in TOSCA type {0} for attribute "
462 "{1}").format(self.nodetemplate.type, attribute))
464 def get_tosca_props(self):
466 for prop in self.nodetemplate.get_properties_objects():
467 if isinstance(prop.value, GetInput):
468 tosca_props[prop.name] = {'get_param': prop.value.input_name}
470 tosca_props[prop.name] = prop.value
474 def get_all_artifacts(nodetemplate):
475 # workaround bug in the parser
476 base_type = HotResource.get_base_type_str(nodetemplate.type_definition)
477 if base_type in policy_type:
480 artifacts = nodetemplate.type_definition.get_value('artifacts',
484 tpl_artifacts = nodetemplate.entity_tpl.get('artifacts')
486 artifacts.update(tpl_artifacts)
491 def get_all_operations(node):
493 for operation in node.interfaces:
494 operations[operation.name] = operation
496 node_type = node.type_definition
497 if isinstance(node_type, str) or \
498 node_type.is_derived_from("tosca.policies.Root"):
499 # node_type.type == "tosca.policies.Placement" or \
500 # node_type.type == "tosca.policies.Placement.Colocate" or \
501 # node_type.type == "tosca.policies.Placement.Antilocate":
505 type_operations = HotResource._get_interface_operations_from_type(
506 node_type, node, 'Standard')
507 type_operations.update(operations)
508 operations = type_operations
510 if node_type.parent_type is not None:
511 node_type = node_type.parent_type
516 def _get_interface_operations_from_type(node_type, node, lifecycle_name):
518 if isinstance(node_type, str) or \
519 node_type.is_derived_from("tosca.policies.Root"):
520 # node_type.type == "tosca.policies.Placement" or \
521 # node_type.type == "tosca.policies.Placement.Colocate" or \
522 # node_type.type == "tosca.policies.Placement.Antilocate":
524 if node_type.interfaces and lifecycle_name in node_type.interfaces:
525 for name, elems in node_type.interfaces[lifecycle_name].items():
526 # ignore empty operations (only type)
527 # ignore global interface inputs,
528 # concrete inputs are on the operations themselves
529 if name != 'type' and name != 'inputs':
530 operations[name] = InterfacesDef(node_type,
536 def get_base_type(node_type):
537 if node_type.parent_type is not None:
538 if node_type.parent_type.type.endswith('.Root') or \
539 node_type.type == "tosca.policies.Placement.Colocate" or \
540 node_type.type == "tosca.policies.Placement.Antilocate":
543 return HotResource.get_base_type(node_type.parent_type)
544 return node_type.type
547 def get_base_type_str(node_type):
548 if isinstance(node_type, six.string_types):
550 if node_type.parent_type is not None:
551 parent_type_str = None
552 if isinstance(node_type.parent_type, six.string_types):
553 parent_type_str = node_type.parent_type
555 parent_type_str = node_type.parent_type.type
557 if parent_type_str and parent_type_str.endswith('.Root'):
558 return node_type.type
560 return HotResource.get_base_type_str(node_type.parent_type)
562 return node_type.type
565 class HOTSoftwareDeploymentResources(object):
566 """Provides HOT Software Deployment resources
568 SoftwareDeployment or SoftwareDeploymentGroup Resource
571 HOT_SW_DEPLOYMENT_RESOURCE = 'OS::Heat::SoftwareDeployment'
572 HOT_SW_DEPLOYMENT_GROUP_RESOURCE = 'OS::Heat::SoftwareDeploymentGroup'
574 def __init__(self, hosting_server=None):
575 self.software_deployment = self.HOT_SW_DEPLOYMENT_RESOURCE
576 self.software_deployment_group = self.HOT_SW_DEPLOYMENT_GROUP_RESOURCE
577 self.server_key = 'server'
578 self.hosting_server = hosting_server
580 if hosting_server is not None:
581 if len(self.hosting_server) == 1:
582 if isinstance(hosting_server, list):
583 self.servers['get_resource'] = self.hosting_server[0]
585 for server in self.hosting_server:
586 self.servers[server] = {'get_resource': server}
587 self.software_deployment = self.software_deployment_group
588 self.server_key = 'servers'