2 # Licensed under the Apache License, Version 2.0 (the "License"); you may
3 # not use this file except in compliance with the License. You may obtain
4 # a copy of the License at
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11 # License for the specific language governing permissions and limitations
19 from toscaparser.functions import GetAttribute
20 from toscaparser.functions import GetInput
21 from toscaparser.functions import GetProperty
22 from toscaparser.properties import Property
23 from toscaparser.relationship_template import RelationshipTemplate
24 from toscaparser.utils.gettextutils import _
25 from translator.common.exception import ToscaClassAttributeError
26 from translator.common.exception import ToscaClassImportError
27 from translator.common.exception import ToscaModImportError
28 from translator.conf.config import ConfigProvider as translatorConfig
29 from translator.hot.syntax.hot_resource import HotResource
30 from translator.hot.tosca.tosca_block_storage_attachment import (
31 ToscaBlockStorageAttachment
34 ###########################
35 # Module utility Functions
36 # for dynamic class loading
37 ###########################
40 def _generate_type_map():
41 '''Generate TOSCA translation types map.
43 Load user defined classes from location path specified in conf file.
44 Base classes are located within the tosca directory.
48 # Base types directory
49 BASE_PATH = 'translator/hot/tosca'
51 # Custom types directory defined in conf file
52 custom_path = translatorConfig.get_value('DEFAULT',
53 'custom_types_location')
55 # First need to load the parent module, for example 'contrib.hot',
56 # for all of the dynamically loaded classes.
58 _load_classes((BASE_PATH, custom_path), classes)
60 types_map = {clazz.toscatype: clazz for clazz in classes}
61 except AttributeError as e:
62 raise ToscaClassAttributeError(message=e.message)
67 def _load_classes(locations, classes):
68 '''Dynamically load all the classes from the given locations.'''
70 for cls_path in locations:
71 # Use the absolute path of the class path
72 abs_path = os.path.dirname(os.path.abspath(__file__))
73 abs_path = abs_path.replace('translator/hot', cls_path)
75 # Grab all the tosca type module files in the given path
76 mod_files = [f for f in os.listdir(abs_path) if f.endswith('.py')
77 and not f.startswith('__init__')
78 and f.startswith('tosca_')]
80 # For each module, pick out the target translation class
82 # NOTE: For some reason the existing code does not use the map to
83 # instantiate ToscaBlockStorageAttachment. Don't add it to the map
84 # here until the dependent code is fixed to use the map.
85 if f == 'tosca_block_storage_attachment.py':
88 mod_name = cls_path + '/' + f.strip('.py')
89 mod_name = mod_name.replace('/', '.')
91 mod = importlib.import_module(mod_name)
92 target_name = getattr(mod, 'TARGET_CLASS_NAME')
93 clazz = getattr(mod, target_name)
96 raise ToscaModImportError(mod_name=mod_name)
97 except AttributeError:
99 raise ToscaClassImportError(name=target_name,
102 # TARGET_CLASS_NAME is not defined in module.
103 # Re-raise the exception
110 SECTIONS = (TYPE, PROPERTIES, REQUIREMENTS, INTERFACES, LIFECYCLE, INPUT) = \
111 ('type', 'properties', 'requirements',
112 'interfaces', 'lifecycle', 'input')
114 # TODO(anyone): the following requirement names should not be hard-coded
115 # in the translator. Since they are basically arbitrary names, we have to get
116 # them from TOSCA type definitions.
117 # To be fixed with the blueprint:
118 # https://blueprints.launchpad.net/heat-translator/+spec/tosca-custom-types
119 REQUIRES = (CONTAINER, DEPENDENCY, DATABASE_ENDPOINT, CONNECTION, HOST) = \
120 ('container', 'dependency', 'database_endpoint',
121 'connection', 'host')
123 INTERFACES_STATE = (CREATE, START, CONFIGURE, START, DELETE) = \
124 ('create', 'stop', 'configure', 'start', 'delete')
127 TOSCA_TO_HOT_REQUIRES = {'container': 'server', 'host': 'server',
128 'dependency': 'depends_on', "connects": 'depends_on'}
130 TOSCA_TO_HOT_PROPERTIES = {'properties': 'input'}
131 log = logging.getLogger('heat-translator')
133 TOSCA_TO_HOT_TYPE = _generate_type_map()
136 class TranslateNodeTemplates(object):
137 '''Translate TOSCA NodeTemplates to Heat Resources.'''
139 def __init__(self, tosca, hot_template):
141 self.nodetemplates = self.tosca.nodetemplates
142 self.hot_template = hot_template
143 # list of all HOT resources generated
144 self.hot_resources = []
145 # mapping between TOSCA nodetemplate and HOT resource
146 log.debug(_('Mapping between TOSCA nodetemplate and HOT resource.'))
148 self.policies = self.tosca.topology_template.policies
151 return self._translate_nodetemplates()
153 def _recursive_handle_properties(self, resource):
154 '''Recursively handle the properties of the depends_on_nodes nodes.'''
155 # Use of hashtable (dict) here should be faster?
156 if resource in self.processed_resources:
158 self.processed_resources.append(resource)
159 for depend_on in resource.depends_on_nodes:
160 self._recursive_handle_properties(depend_on)
162 if resource.type == "OS::Nova::ServerGroup":
163 resource.handle_properties(self.hot_resources)
165 resource.handle_properties()
167 def _translate_nodetemplates(self):
169 log.debug(_('Translating the node templates.'))
171 # Copy the TOSCA graph: nodetemplate
172 for node in self.nodetemplates:
173 base_type = HotResource.get_base_type(node.type_definition)
174 hot_node = TOSCA_TO_HOT_TYPE[base_type.type](node)
175 self.hot_resources.append(hot_node)
176 self.hot_lookup[node] = hot_node
178 # BlockStorage Attachment is a special case,
179 # which doesn't match to Heat Resources 1 to 1.
180 if base_type.type == "tosca.nodes.Compute":
182 requirements = node.requirements
184 # Find the name of associated BlockStorage node
185 for requires in requirements:
186 for value in requires.values():
187 if isinstance(value, dict):
188 for node_name in value.values():
189 for n in self.nodetemplates:
190 if n.name == node_name:
191 volume_name = node_name
193 else: # unreachable code !
194 for n in self.nodetemplates:
195 if n.name == node_name:
196 volume_name = node_name
200 attachment_node = self._get_attachment_node(node,
204 self.hot_resources.append(attachment_node)
205 for i in self.tosca.inputs:
206 if (i.name == 'key_name' and
207 node.get_property_value('key_name') is None):
208 schema = {'type': i.type, 'default': i.default}
209 value = {"get_param": "key_name"}
210 prop = Property(i.name, value, schema)
211 node._properties.append(prop)
213 for policy in self.policies:
214 policy_type = policy.type_definition
215 policy_node = TOSCA_TO_HOT_TYPE[policy_type.type](policy)
216 self.hot_resources.append(policy_node)
218 # Handle life cycle operations: this may expand each node
219 # into multiple HOT resources and may change their name
220 lifecycle_resources = []
221 for resource in self.hot_resources:
222 expanded = resource.handle_life_cycle()
224 lifecycle_resources += expanded
225 self.hot_resources += lifecycle_resources
227 # Handle configuration from ConnectsTo relationship in the TOSCA node:
228 # this will generate multiple HOT resources, set of 2 for each
230 connectsto_resources = []
231 for node in self.nodetemplates:
232 for requirement in node.requirements:
233 for endpoint, details in six.iteritems(requirement):
235 if isinstance(details, dict):
236 target = details.get('node')
237 relation = details.get('relationship')
240 if (target and relation and
241 not isinstance(relation, six.string_types)):
242 interfaces = relation.get('interfaces')
243 connectsto_resources += \
244 self._create_connect_configs(node,
247 self.hot_resources += connectsto_resources
249 # Copy the initial dependencies based on the relationship in
251 for node in self.nodetemplates:
252 for node_depend in node.related_nodes:
253 # if the source of dependency is a server and the
254 # relationship type is 'tosca.relationships.HostedOn',
255 # add dependency as properties.server
256 if node_depend.type == 'tosca.nodes.Compute' and \
257 node.related[node_depend].type == \
258 node.type_definition.HOSTEDON:
259 self.hot_lookup[node].properties['server'] = \
260 {'get_resource': self.hot_lookup[node_depend].name}
261 # for all others, add dependency as depends_on
263 self.hot_lookup[node].depends_on.append(
264 self.hot_lookup[node_depend].top_of_chain())
266 self.hot_lookup[node].depends_on_nodes.append(
267 self.hot_lookup[node_depend].top_of_chain())
269 # handle hosting relationship
270 for resource in self.hot_resources:
271 resource.handle_hosting()
273 # handle built-in properties of HOT resources
274 # if a resource depends on other resources,
275 # their properties need to be handled first.
276 # Use recursion to handle the properties of the
277 # dependent nodes in correct order
278 self.processed_resources = []
279 for resource in self.hot_resources:
280 self._recursive_handle_properties(resource)
282 # handle resources that need to expand to more than one HOT resource
283 expansion_resources = []
284 for resource in self.hot_resources:
285 expanded = resource.handle_expansion()
287 expansion_resources += expanded
288 self.hot_resources += expansion_resources
290 # Resolve function calls: GetProperty, GetAttribute, GetInput
291 # at this point, all the HOT resources should have been created
293 for resource in self.hot_resources:
294 # traverse the reference chain to get the actual value
295 inputs = resource.properties.get('input_values')
297 for name, value in six.iteritems(inputs):
298 inputs[name] = self._translate_input(value, resource)
300 return self.hot_resources
302 def _translate_input(self, input_value, resource):
303 get_property_args = None
304 if isinstance(input_value, GetProperty):
305 get_property_args = input_value.args
306 # to remove when the parser is fixed to return GetProperty
307 if isinstance(input_value, dict) and 'get_property' in input_value:
308 get_property_args = input_value['get_property']
309 if get_property_args is not None:
310 hot_target = self._find_hot_resource_for_tosca(
311 get_property_args[0], resource)
313 props = hot_target.get_tosca_props()
314 prop_name = get_property_args[1]
315 if prop_name in props:
316 return props[prop_name]
317 elif isinstance(input_value, GetAttribute):
319 # get the proper target type to perform the translation
320 args = input_value.result()
321 hot_target = self._find_hot_resource_for_tosca(args[0], resource)
323 return hot_target.get_hot_attribute(args[1], args)
324 # most of artifacts logic should move to the parser
325 elif isinstance(input_value, dict) and 'get_artifact' in input_value:
326 get_artifact_args = input_value['get_artifact']
328 hot_target = self._find_hot_resource_for_tosca(
329 get_artifact_args[0], resource)
330 artifacts = TranslateNodeTemplates.get_all_artifacts(
331 hot_target.nodetemplate)
333 if get_artifact_args[1] in artifacts:
334 artifact = artifacts[get_artifact_args[1]]
335 if artifact.get('type', None) == 'tosca.artifacts.File':
336 return {'get_file': artifact.get('file')}
337 elif isinstance(input_value, GetInput):
338 if isinstance(input_value.args, list) \
339 and len(input_value.args) == 1:
340 return {'get_param': input_value.args[0]}
342 return {'get_param': input_value.args}
347 def get_all_artifacts(nodetemplate):
348 artifacts = nodetemplate.type_definition.get_value('artifacts',
352 tpl_artifacts = nodetemplate.entity_tpl.get('artifacts')
354 artifacts.update(tpl_artifacts)
358 def _get_attachment_node(self, node, suffix, volume_name):
360 ntpl = self.nodetemplates
361 for key, value in node.relationships.items():
362 if key.is_derived_from('tosca.relationships.AttachesTo'):
363 if value.is_derived_from('tosca.nodes.BlockStorage'):
366 relationship_tpl = None
367 for req in node.requirements:
368 for key, val in req.items():
370 relship = val.get('relationship')
371 for rkey, rval in val.items():
372 if relship and isinstance(relship, dict):
373 for rkey, rval in relship.items():
375 relationship_tpl = val
377 elif rkey == 'template':
379 (self.tosca.topology_template.
380 _tpl_relationship_templates())
381 relationship_tpl = rel_tpl_list[rval]
385 elif isinstance(relship, str):
387 relationship_tpl = val
388 relationship_templates = \
389 self.tosca._tpl_relationship_templates()
390 if 'relationship' in relationship_tpl and \
392 self.tosca._tpl_relationship_types() and \
393 attach in relationship_templates:
394 relationship_tpl['relationship'] = \
395 relationship_templates[attach]
398 rval_new = attach + "_" + str(suffix)
399 att = RelationshipTemplate(
400 relationship_tpl, rval_new,
401 self.tosca._tpl_relationship_types())
402 hot_node = ToscaBlockStorageAttachment(att, ntpl,
408 def find_hot_resource(self, name):
409 for resource in self.hot_resources:
410 if resource.name == name:
413 def _find_tosca_node(self, tosca_name):
414 for node in self.nodetemplates:
415 if node.name == tosca_name:
418 def _find_hot_resource_for_tosca(self, tosca_name,
419 current_hot_resource=None):
420 if tosca_name == 'SELF':
421 return current_hot_resource
422 if tosca_name == 'HOST' and current_hot_resource is not None:
423 for req in current_hot_resource.nodetemplate.requirements:
425 return self._find_hot_resource_for_tosca(req['host'])
427 for node in self.nodetemplates:
428 if node.name == tosca_name:
429 return self.hot_lookup[node]
433 def _create_connect_configs(self, source_node, target_name,
435 connectsto_resources = []
436 if connect_interfaces:
437 for iname, interface in six.iteritems(connect_interfaces):
438 connectsto_resources += \
439 self._create_connect_config(source_node, target_name,
441 return connectsto_resources
443 def _create_connect_config(self, source_node, target_name,
445 connectsto_resources = []
446 target_node = self._find_tosca_node(target_name)
447 # the configuration can occur on the source or the target
448 connect_config = connect_interface.get('pre_configure_target')
449 if connect_config is not None:
450 config_location = 'target'
452 connect_config = connect_interface.get('pre_configure_source')
453 if connect_config is not None:
454 config_location = 'source'
456 msg = _("Template error: "
457 "no configuration found for ConnectsTo "
458 "in {1}").format(self.nodetemplate.name)
461 config_name = source_node.name + '_' + target_name + '_connect_config'
462 implement = connect_config.get('implementation')
463 if config_location == 'target':
464 hot_config = HotResource(target_node,
466 'OS::Heat::SoftwareConfig',
467 {'config': {'get_file': implement}})
468 elif config_location == 'source':
469 hot_config = HotResource(source_node,
471 'OS::Heat::SoftwareConfig',
472 {'config': {'get_file': implement}})
473 connectsto_resources.append(hot_config)
474 hot_target = self._find_hot_resource_for_tosca(target_name)
475 hot_source = self._find_hot_resource_for_tosca(source_node.name)
476 connectsto_resources.append(hot_config.
477 handle_connectsto(source_node,
483 return connectsto_resources