diff options
Diffstat (limited to 'tosca2heat/heat-translator/translator/hot')
32 files changed, 3128 insertions, 0 deletions
diff --git a/tosca2heat/heat-translator/translator/hot/__init__.py b/tosca2heat/heat-translator/translator/hot/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/__init__.py diff --git a/tosca2heat/heat-translator/translator/hot/syntax/__init__.py b/tosca2heat/heat-translator/translator/hot/syntax/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/syntax/__init__.py diff --git a/tosca2heat/heat-translator/translator/hot/syntax/hot_output.py b/tosca2heat/heat-translator/translator/hot/syntax/hot_output.py new file mode 100644 index 0000000..ad77fb3 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/syntax/hot_output.py @@ -0,0 +1,25 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class HotOutput(object): + '''Attributes for HOT output section.''' + + def __init__(self, name, value, description=None): + self.name = name + self.value = value + self.description = description + + def get_dict_output(self): + return {self.name: {'value': self.value, + 'description': self.description}} diff --git a/tosca2heat/heat-translator/translator/hot/syntax/hot_parameter.py b/tosca2heat/heat-translator/translator/hot/syntax/hot_parameter.py new file mode 100644 index 0000000..1ecb2ce --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/syntax/hot_parameter.py @@ -0,0 +1,52 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections import OrderedDict +import logging +from toscaparser.utils.gettextutils import _ + +KEYS = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS, HIDDEN, LABEL) = \ + ('type', 'description', 'default', 'constraints', 'hidden', 'label') + +log = logging.getLogger('heat-translator') + + +class HotParameter(object): + '''Attributes for HOT parameter section.''' + + def __init__(self, name, type, label=None, description=None, default=None, + hidden=None, constraints=None): + self.name = name + self.type = type + self.label = label + self.description = description + self.default = default + self.hidden = hidden + self.constraints = constraints + log.info(_('Initialized the input parameters.')) + + def get_dict_output(self): + param_sections = OrderedDict() + param_sections[TYPE] = self.type + if self.label: + param_sections[LABEL] = self.label + if self.description: + param_sections[DESCRIPTION] = self.description + if self.default: + param_sections[DEFAULT] = self.default + if self.hidden: + param_sections[HIDDEN] = self.hidden + if self.constraints: + param_sections[CONSTRAINTS] = self.constraints + + return {self.name: param_sections} diff --git a/tosca2heat/heat-translator/translator/hot/syntax/hot_resource.py b/tosca2heat/heat-translator/translator/hot/syntax/hot_resource.py new file mode 100644 index 0000000..d7d0100 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/syntax/hot_resource.py @@ -0,0 +1,362 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections import OrderedDict +import logging +import six + +from toscaparser.elements.interfaces import InterfacesDef +from toscaparser.functions import GetInput +from toscaparser.nodetemplate import NodeTemplate +from toscaparser.utils.gettextutils import _ + + +SECTIONS = (TYPE, PROPERTIES, MEDADATA, DEPENDS_ON, UPDATE_POLICY, + DELETION_POLICY) = \ + ('type', 'properties', 'metadata', + 'depends_on', 'update_policy', 'deletion_policy') +log = logging.getLogger('heat-translator') + + +class HotResource(object): + '''Base class for TOSCA node type translation to Heat resource type.''' + + def __init__(self, nodetemplate, name=None, type=None, properties=None, + metadata=None, depends_on=None, + update_policy=None, deletion_policy=None): + log.debug(_('Translating TOSCA node type to HOT resource type.')) + self.nodetemplate = nodetemplate + if name: + self.name = name + else: + self.name = nodetemplate.name + self.type = type + self.properties = properties or {} + # special case for HOT softwareconfig + if type == 'OS::Heat::SoftwareConfig': + self.properties['group'] = 'script' + self.metadata = metadata + + # The difference between depends_on and depends_on_nodes is + # that depends_on defines dependency in the context of the + # HOT template and it is used during the template output. + # Depends_on_nodes defines the direct dependency between the + # tosca nodes and is not used during the output of the + # HOT template but for internal processing only. When a tosca + # node depends on another node it will be always added to + # depends_on_nodes but not always to depends_on. For example + # if the source of dependency is a server, the dependency will + # be added as properties.get_resource and not depends_on + if depends_on: + self.depends_on = depends_on + self.depends_on_nodes = depends_on + else: + self.depends_on = [] + self.depends_on_nodes = [] + self.update_policy = update_policy + self.deletion_policy = deletion_policy + self.group_dependencies = {} + # if hide_resource is set to true, then this resource will not be + # generated in the output yaml. + self.hide_resource = False + + def handle_properties(self): + # the property can hold a value or the intrinsic function get_input + # for value, copy it + # for get_input, convert to get_param + for prop in self.nodetemplate.get_properties_objects(): + pass + + def handle_life_cycle(self): + hot_resources = [] + deploy_lookup = {} + # TODO(anyone): sequence for life cycle needs to cover different + # scenarios and cannot be fixed or hard coded here + operations_deploy_sequence = ['create', 'configure', 'start'] + + operations = HotResource._get_all_operations(self.nodetemplate) + + # create HotResource for each operation used for deployment: + # create, start, configure + # ignore the other operations + # observe the order: create, start, configure + # use the current HotResource for the first operation in this order + + # hold the original name since it will be changed during + # the transformation + node_name = self.name + reserve_current = 'NONE' + + for operation in operations_deploy_sequence: + if operation in operations.keys(): + reserve_current = operation + break + + # create the set of SoftwareDeployment and SoftwareConfig for + # the interface operations + hosting_server = None + if self.nodetemplate.requirements is not None: + hosting_server = self._get_hosting_server() + for operation in operations.values(): + if operation.name in operations_deploy_sequence: + config_name = node_name + '_' + operation.name + '_config' + deploy_name = node_name + '_' + operation.name + '_deploy' + hot_resources.append( + HotResource(self.nodetemplate, + config_name, + 'OS::Heat::SoftwareConfig', + {'config': + {'get_file': operation.implementation}})) + + # hosting_server is None if requirements is None + hosting_on_server = (hosting_server.name if + hosting_server else None) + if operation.name == reserve_current: + deploy_resource = self + self.name = deploy_name + self.type = 'OS::Heat::SoftwareDeployment' + self.properties = {'config': {'get_resource': config_name}, + 'server': {'get_resource': + hosting_on_server}} + deploy_lookup[operation.name] = self + else: + sd_config = {'config': {'get_resource': config_name}, + 'server': {'get_resource': + hosting_on_server}} + deploy_resource = \ + HotResource(self.nodetemplate, + deploy_name, + 'OS::Heat::SoftwareDeployment', + sd_config) + hot_resources.append(deploy_resource) + deploy_lookup[operation.name] = deploy_resource + lifecycle_inputs = self._get_lifecycle_inputs(operation) + if lifecycle_inputs: + deploy_resource.properties['input_values'] = \ + lifecycle_inputs + + # Add dependencies for the set of HOT resources in the sequence defined + # in operations_deploy_sequence + # TODO(anyone): find some better way to encode this implicit sequence + group = {} + for op, hot in deploy_lookup.items(): + # position to determine potential preceding nodes + op_index = operations_deploy_sequence.index(op) + for preceding_op in \ + reversed(operations_deploy_sequence[:op_index]): + preceding_hot = deploy_lookup.get(preceding_op) + if preceding_hot: + hot.depends_on.append(preceding_hot) + hot.depends_on_nodes.append(preceding_hot) + group[preceding_hot] = hot + break + + # save this dependency chain in the set of HOT resources + self.group_dependencies.update(group) + for hot in hot_resources: + hot.group_dependencies.update(group) + + return hot_resources + + def handle_connectsto(self, tosca_source, tosca_target, hot_source, + hot_target, config_location, operation): + # The ConnectsTo relationship causes a configuration operation in + # the target. + # This hot resource is the software config portion in the HOT template + # This method adds the matching software deployment with the proper + # target server and dependency + if config_location == 'target': + hosting_server = hot_target._get_hosting_server() + hot_depends = hot_target + elif config_location == 'source': + hosting_server = self._get_hosting_server() + hot_depends = hot_source + deploy_name = tosca_source.name + '_' + tosca_target.name + \ + '_connect_deploy' + sd_config = {'config': {'get_resource': self.name}, + 'server': {'get_resource': hosting_server.name}} + deploy_resource = \ + HotResource(self.nodetemplate, + deploy_name, + 'OS::Heat::SoftwareDeployment', + sd_config, + depends_on=[hot_depends]) + connect_inputs = self._get_connect_inputs(config_location, operation) + if connect_inputs: + deploy_resource.properties['input_values'] = connect_inputs + + return deploy_resource + + def handle_expansion(self): + pass + + def handle_hosting(self): + # handle hosting server for the OS:HEAT::SoftwareDeployment + # from the TOSCA nodetemplate, traverse the relationship chain + # down to the server + if self.type == 'OS::Heat::SoftwareDeployment': + # skip if already have hosting + # If type is NodeTemplate, look up corresponding HotResrouce + host_server = self.properties.get('server') + if host_server is None or not host_server['get_resource']: + raise Exception(_("Internal Error: expecting host " + "in software deployment")) + elif isinstance(host_server['get_resource'], NodeTemplate): + self.properties['server']['get_resource'] = \ + host_server['get_resource'].name + + def top_of_chain(self): + dependent = self.group_dependencies.get(self) + if dependent is None: + return self + else: + return dependent.top_of_chain() + + def get_dict_output(self): + resource_sections = OrderedDict() + resource_sections[TYPE] = self.type + if self.properties: + resource_sections[PROPERTIES] = self.properties + if self.metadata: + resource_sections[MEDADATA] = self.metadata + if self.depends_on: + resource_sections[DEPENDS_ON] = [] + for depend in self.depends_on: + resource_sections[DEPENDS_ON].append(depend.name) + if self.update_policy: + resource_sections[UPDATE_POLICY] = self.update_policy + if self.deletion_policy: + resource_sections[DELETION_POLICY] = self.deletion_policy + + return {self.name: resource_sections} + + def _get_lifecycle_inputs(self, operation): + # check if this lifecycle operation has input values specified + # extract and convert to HOT format + if isinstance(operation.value, six.string_types): + # the operation has a static string + return {} + else: + # the operation is a dict {'implemenation': xxx, 'input': yyy} + inputs = operation.value.get('inputs') + deploy_inputs = {} + if inputs: + for name, value in six.iteritems(inputs): + deploy_inputs[name] = value + return deploy_inputs + + def _get_connect_inputs(self, config_location, operation): + if config_location == 'target': + inputs = operation.get('pre_configure_target').get('inputs') + elif config_location == 'source': + inputs = operation.get('pre_configure_source').get('inputs') + deploy_inputs = {} + if inputs: + for name, value in six.iteritems(inputs): + deploy_inputs[name] = value + return deploy_inputs + + def _get_hosting_server(self, node_template=None): + # find the server that hosts this software by checking the + # requirements and following the hosting chain + this_node_template = self.nodetemplate \ + if node_template is None else node_template + for requirement in this_node_template.requirements: + for requirement_name, assignment in six.iteritems(requirement): + for check_node in this_node_template.related_nodes: + # check if the capability is Container + if isinstance(assignment, dict): + node_name = assignment.get('node') + else: + node_name = assignment + if node_name and node_name == check_node.name: + if self._is_container_type(requirement_name, + check_node): + return check_node + elif check_node.related_nodes: + return self._get_hosting_server(check_node) + return None + + def _is_container_type(self, requirement_name, node): + # capability is a list of dict + # For now just check if it's type tosca.nodes.Compute + # TODO(anyone): match up requirement and capability + base_type = HotResource.get_base_type(node.type_definition) + if base_type.type == 'tosca.nodes.Compute': + return True + else: + return False + + def get_hot_attribute(self, attribute, args): + # this is a place holder and should be implemented by the subclass + # if translation is needed for the particular attribute + raise Exception(_("No translation in TOSCA type {0} for attribute " + "{1}").format(self.nodetemplate.type, attribute)) + + def get_tosca_props(self): + tosca_props = {} + for prop in self.nodetemplate.get_properties_objects(): + if isinstance(prop.value, GetInput): + tosca_props[prop.name] = {'get_param': prop.value.input_name} + else: + tosca_props[prop.name] = prop.value + return tosca_props + + @staticmethod + def _get_all_operations(node): + operations = {} + for operation in node.interfaces: + operations[operation.name] = operation + + node_type = node.type_definition + if isinstance(node_type, str) or \ + node_type.type == "tosca.policies.Placement": + return operations + + while True: + type_operations = HotResource._get_interface_operations_from_type( + node_type, node, 'Standard') + type_operations.update(operations) + operations = type_operations + + if node_type.parent_type is not None: + node_type = node_type.parent_type + else: + return operations + + @staticmethod + def _get_interface_operations_from_type(node_type, node, lifecycle_name): + operations = {} + if isinstance(node_type, str) or \ + node_type.type == "tosca.policies.Placement": + return operations + if node_type.interfaces and lifecycle_name in node_type.interfaces: + for name, elems in node_type.interfaces[lifecycle_name].items(): + # ignore empty operations (only type) + # ignore global interface inputs, + # concrete inputs are on the operations themselves + if name != 'type' and name != 'inputs': + operations[name] = InterfacesDef(node_type, + lifecycle_name, + node, name, elems) + return operations + + @staticmethod + def get_base_type(node_type): + if node_type.parent_type is not None: + if node_type.parent_type.type.endswith('.Root'): + return node_type + else: + return HotResource.get_base_type(node_type.parent_type) + else: + return node_type diff --git a/tosca2heat/heat-translator/translator/hot/syntax/hot_template.py b/tosca2heat/heat-translator/translator/hot/syntax/hot_template.py new file mode 100644 index 0000000..4263c4d --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/syntax/hot_template.py @@ -0,0 +1,83 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections import OrderedDict +import logging +import textwrap +from toscaparser.utils.gettextutils import _ +import yaml + +log = logging.getLogger('heat-translator') + + +class HotTemplate(object): + '''Container for full Heat Orchestration template.''' + + SECTIONS = (VERSION, DESCRIPTION, PARAMETER_GROUPS, PARAMETERS, + RESOURCES, OUTPUTS, MAPPINGS) = \ + ('heat_template_version', 'description', 'parameter_groups', + 'parameters', 'resources', 'outputs', '__undefined__') + + VERSIONS = (LATEST,) = ('2013-05-23',) + + def __init__(self): + self.resources = [] + self.outputs = [] + self.parameters = [] + self.description = "" + + def represent_ordereddict(self, dumper, data): + nodes = [] + for key, value in data.items(): + node_key = dumper.represent_data(key) + node_value = dumper.represent_data(value) + nodes.append((node_key, node_value)) + return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', nodes) + + def output_to_yaml(self): + log.debug(_('Converting translated output to yaml format.')) + dict_output = OrderedDict() + # Version + version_string = self.VERSION + ": " + self.LATEST + "\n\n" + + # Description + desc_str = "" + if self.description: + # Wrap the text to a new line if the line exceeds 80 characters. + wrapped_txt = "\n ".join(textwrap.wrap(self.description, 80)) + desc_str = self.DESCRIPTION + ": >\n " + wrapped_txt + "\n\n" + + # Parameters + all_params = OrderedDict() + for parameter in self.parameters: + all_params.update(parameter.get_dict_output()) + dict_output.update({self.PARAMETERS: all_params}) + + # Resources + all_resources = OrderedDict() + for resource in self.resources: + if not resource.hide_resource: + all_resources.update(resource.get_dict_output()) + dict_output.update({self.RESOURCES: all_resources}) + + # Outputs + all_outputs = OrderedDict() + for output in self.outputs: + all_outputs.update(output.get_dict_output()) + dict_output.update({self.OUTPUTS: all_outputs}) + + yaml.add_representer(OrderedDict, self.represent_ordereddict) + yaml_string = yaml.dump(dict_output, default_flow_style=False) + # get rid of the '' from yaml.dump around numbers + yaml_string = yaml_string.replace('\'', '') + return version_string + desc_str + yaml_string diff --git a/tosca2heat/heat-translator/translator/hot/tests/__init__.py b/tosca2heat/heat-translator/translator/hot/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tests/__init__.py diff --git a/tosca2heat/heat-translator/translator/hot/tests/test_hot_parameter.py b/tosca2heat/heat-translator/translator/hot/tests/test_hot_parameter.py new file mode 100644 index 0000000..8d3f535 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tests/test_hot_parameter.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections import OrderedDict + +from toscaparser.tests.base import TestCase +from translator.hot.syntax.hot_parameter import CONSTRAINTS +from translator.hot.syntax.hot_parameter import DEFAULT +from translator.hot.syntax.hot_parameter import DESCRIPTION +from translator.hot.syntax.hot_parameter import HIDDEN +from translator.hot.syntax.hot_parameter import HotParameter +from translator.hot.syntax.hot_parameter import LABEL +from translator.hot.syntax.hot_parameter import TYPE + +TEST_CONSTRAINTS = {'equal': 'allowed_values', 'greater_than': 'range'} + + +class HotParameterTest(TestCase): + + # This test ensures the variables set during the creation of a HotParameter + # object are returned in an OrderedDict when calling get_dict_output(). + def test_dict_output(self): + name = 'HotParameterTest' + hot_parameter = HotParameter(name, 'Type', + label='Label', + description='Description', + default='Default', + hidden=True, + constraints=TEST_CONSTRAINTS) + expected_dict = OrderedDict([(TYPE, 'Type'), (LABEL, 'Label'), + (DESCRIPTION, 'Description'), + (DEFAULT, 'Default'), (HIDDEN, True), + (CONSTRAINTS, TEST_CONSTRAINTS)]) + + self.assertEqual(hot_parameter.get_dict_output()[name], expected_dict) diff --git a/tosca2heat/heat-translator/translator/hot/tests/test_translate_inputs.py b/tosca2heat/heat-translator/translator/hot/tests/test_translate_inputs.py new file mode 100644 index 0000000..2b302ab --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tests/test_translate_inputs.py @@ -0,0 +1,351 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from collections import OrderedDict +from toscaparser.parameters import Input +from toscaparser.tests.base import TestCase +from toscaparser.utils.gettextutils import _ +import toscaparser.utils.yamlparser +from translator.common.utils import CompareUtils +from translator.hot.translate_inputs import TranslateInputs + + +class ToscaTemplateInputValidationTest(TestCase): + + def _translate_input_test(self, tpl_snippet, input_params, + expectedmessage=None, + expected_hot_params=None): + inputs_dict = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['inputs']) + inputs = [] + for name, attrs in inputs_dict.items(): + input = Input(name, attrs) + inputs.append(input) + + translateinput = TranslateInputs(inputs, input_params) + try: + resulted_hot_params = translateinput.translate() + if expected_hot_params: + self._compare_hot_params(resulted_hot_params, + expected_hot_params) + except Exception as err: + self.assertEqual(expectedmessage, err.__str__()) + + def _compare_hot_params(self, resulted_hot_params, + expected_hot_params): + for expected_param in expected_hot_params: + for resulted_param_obj in resulted_hot_params: + resulted_param = resulted_param_obj.get_dict_output() + result = CompareUtils.compare_dicts(expected_param, + resulted_param) + if not result: + raise Exception(_("hot input and resulted input " + "params are not equal.")) + + def test_invalid_input_type(self): + tpl_snippet = ''' + inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + ''' + + input_params = {'cpus': '0.3'} + expectedmessage = _('"0.3" is not an integer.') + self._translate_input_test(tpl_snippet, input_params, + expectedmessage) + + def test_invalid_input_constraints_for_equal(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - equal: 1 + ''' + + input_params = {'num_cpus': '0'} + expectedmessage = _('The value "0" of property "num_cpus" is not ' + 'equal to "1".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_greater_or_equal(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - greater_or_equal: 1 + ''' + + input_params = {'num_cpus': '0'} + expectedmessage = _('The value "0" of property "num_cpus" must be ' + 'greater than or equal to "1".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_greater_than(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - greater_than: 1 + ''' + + input_params = {'num_cpus': '0'} + expectedmessage = _('The value "0" of property "num_cpus" must be ' + 'greater than "1".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_less_than(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - less_than: 8 + ''' + + input_params = {'num_cpus': '8'} + expectedmessage = _('The value "8" of property "num_cpus" must be ' + 'less than "8".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_less_or_equal(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - less_or_equal: 8 + ''' + + input_params = {'num_cpus': '9'} + expectedmessage = _('The value "9" of property "num_cpus" must be ' + 'less than or equal to "8".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_valid_values(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + ''' + + input_params = {'num_cpus': '3'} + expectedmessage = _('The value "3" of property "num_cpus" is not ' + 'valid. Expected a value from "[1, 2, 4, 8]".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_in_range(self): + tpl_snippet = ''' + inputs: + num_cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - in_range: [ 1, 8 ] + ''' + + input_params = {'num_cpus': '10'} + expectedmessage = _('The value "10" of property "num_cpus" is out of ' + 'range "(min:1, max:8)".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_min_length(self): + tpl_snippet = ''' + inputs: + user_name: + type: string + description: Name of the user. + constraints: + - min_length: 8 + ''' + + input_params = {'user_name': 'abcd'} + expectedmessage = _('Length of value "abcd" of property "user_name" ' + 'must be at least "8".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_max_length(self): + tpl_snippet = ''' + inputs: + user_name: + type: string + description: Name of the user. + constraints: + - max_length: 6 + ''' + + input_params = {'user_name': 'abcdefg'} + expectedmessage = _('Length of value "abcdefg" of property ' + '"user_name" must be no greater than "6".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_invalid_input_constraints_for_pattern(self): + tpl_snippet = ''' + inputs: + user_name: + type: string + description: Name of the user. + constraints: + - pattern: '^\w+$' + ''' + + input_params = {'user_name': '1-abc'} + expectedmessage = _('The value "1-abc" of property "user_name" does ' + 'not match pattern "^\\w+$".') + self._translate_input_test(tpl_snippet, input_params, expectedmessage) + + def test_valid_input_storage_size(self): + tpl_snippet = ''' + inputs: + storage_size: + type: scalar-unit.size + description: size of the storage volume. + ''' + + expectedmessage = _('both equal.') + input_params = {'storage_size': '2 GB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 2)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + """ TOSCA 2000 MB => 2 GB HOT conversion""" + input_params = {'storage_size': '2000 MB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 2)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + """ TOSCA 2048 MB => 2 GB HOT conversion""" + input_params = {'storage_size': '2048 MB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 2)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + """ TOSCA 2 MB => 1 GB HOT conversion""" + input_params = {'storage_size': '2 MB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 1)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + """ TOSCA 1024 MB => 1 GB HOT conversion""" + input_params = {'storage_size': '1024 MB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 1)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + """ TOSCA 1024 MiB => 1 GB HOT conversion""" + input_params = {'storage_size': '1024 MiB'} + expected_hot_params = [{'storage_size': + OrderedDict([('type', 'number'), + ('description', + 'size of the storage volume.'), + ('default', 1)])}] + self._translate_input_test(tpl_snippet, input_params, + expectedmessage, expected_hot_params) + + def test_invalid_input_storage_size(self): + tpl_snippet = ''' + inputs: + storage_size: + type: scalar-unit.size + description: size of the storage volume. + ''' + + input_params = {'storage_size': '0 MB'} + expectedmsg = _("Unit value should be > 0.") + self._translate_input_test(tpl_snippet, input_params, expectedmsg) + + input_params = {'storage_size': '-2 MB'} + expectedmsg = _('"-2 MB" is not a valid scalar-unit.') + self._translate_input_test(tpl_snippet, input_params, expectedmsg) + + def test_invalid_input_type_version(self): + tpl_snippet = ''' + inputs: + version: + type: version + ''' + + input_params = {'version': '0.a'} + expectedmessage = _('Value of TOSCA version property ' + '"0.a" is invalid.') + self._translate_input_test(tpl_snippet, input_params, + expectedmessage) + + input_params = {'version': '0.0.0.abc'} + expectedmessage = _('Value of TOSCA version property ' + '"0.0.0.abc" is invalid.') + self._translate_input_test(tpl_snippet, input_params, + expectedmessage) + + def test_valid_input_type_version(self): + tpl_snippet = ''' + inputs: + version: + type: version + default: 12 + ''' + + expectedmessage = _('both equal.') + input_params = {'version': '18'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) + + input_params = {'version': '18.0'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) + + input_params = {'version': '18.0.1'} + expected_hot_params = [{'version': + OrderedDict([('type', 'string'), + ('default', '18.0.1')])}] + self._translate_input_test(tpl_snippet, input_params, expectedmessage, + expected_hot_params) diff --git a/tosca2heat/heat-translator/translator/hot/tests/test_translate_outputs.py b/tosca2heat/heat-translator/translator/hot/tests/test_translate_outputs.py new file mode 100644 index 0000000..955150e --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tests/test_translate_outputs.py @@ -0,0 +1,50 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +from toscaparser.tests.base import TestCase +from toscaparser.tosca_template import ToscaTemplate +import toscaparser.utils.yamlparser +from translator.hot.tosca_translator import TOSCATranslator + + +class ToscaTemplateOutputTest(TestCase): + + def test_translate_output(self): + tosca_tpl = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../tests/data/" + "tosca_nodejs_mongodb_two_instances.yaml") + tosca = ToscaTemplate(tosca_tpl) + translate = TOSCATranslator(tosca, []) + hot_translation = translate.translate() + + expected_output = {'nodejs_url': + {'description': 'URL for the nodejs ' + 'server, http://<IP>:3000', + 'value': + {'get_attr': + ['app_server', 'networks', 'private', 0]}}, + 'mongodb_url': + {'description': 'URL for the mongodb server.', + 'value': + {'get_attr': + ['mongo_server', 'networks', 'private', 0]}}} + + hot_translation_dict = \ + toscaparser.utils.yamlparser.simple_parse(hot_translation) + + outputs = hot_translation_dict.get('outputs') + for resource_name in outputs: + translated_value = outputs.get(resource_name) + expected_value = expected_output.get(resource_name) + self.assertEqual(translated_value, expected_value) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/__init__.py b/tosca2heat/heat-translator/translator/hot/tosca/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/__init__.py diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tests/__init__.py b/tosca2heat/heat-translator/translator/hot/tosca/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tests/__init__.py diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_blockstorage.py b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_blockstorage.py new file mode 100644 index 0000000..d4fffe1 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_blockstorage.py @@ -0,0 +1,84 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.common.exception import InvalidPropertyValueError +from toscaparser.nodetemplate import NodeTemplate +from toscaparser.tests.base import TestCase +from toscaparser.utils.gettextutils import _ +import toscaparser.utils.yamlparser +from translator.hot.tosca.tosca_block_storage import ToscaBlockStorage + + +class ToscaBlockStoreTest(TestCase): + + def _tosca_blockstore_test(self, tpl_snippet, expectedprops): + nodetemplates = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['node_templates']) + name = list(nodetemplates.keys())[0] + try: + nodetemplate = NodeTemplate(name, nodetemplates) + tosca_block_store = ToscaBlockStorage(nodetemplate) + tosca_block_store.handle_properties() + if not self._compare_properties(tosca_block_store.properties, + expectedprops): + raise Exception(_("Hot Properties are not" + " same as expected properties")) + except Exception: + # for time being rethrowing. Will be handled future based + # on new development + raise + + def _compare_properties(self, hotprops, expectedprops): + return all(item in hotprops.items() for item in expectedprops.items()) + + def test_node_blockstorage_with_properties(self): + tpl_snippet = ''' + node_templates: + my_storage: + type: tosca.nodes.BlockStorage + properties: + size: 1024 MiB + snapshot_id: abc + ''' + expectedprops = {'snapshot_id': 'abc', + 'size': 1} + self._tosca_blockstore_test( + tpl_snippet, + expectedprops) + + tpl_snippet = ''' + node_templates: + my_storage: + type: tosca.nodes.BlockStorage + properties: + size: 124 MB + snapshot_id: abc + ''' + expectedprops = {'snapshot_id': 'abc', + 'size': 1} + self._tosca_blockstore_test( + tpl_snippet, + expectedprops) + + def test_node_blockstorage_with_invalid_size_property(self): + tpl_snippet = ''' + node_templates: + my_storage: + type: tosca.nodes.BlockStorage + properties: + size: 0 MB + snapshot_id: abc + ''' + expectedprops = {} + self.assertRaises(InvalidPropertyValueError, + lambda: self._tosca_blockstore_test(tpl_snippet, + expectedprops)) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_compute.py b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_compute.py new file mode 100644 index 0000000..e0cdbb6 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_compute.py @@ -0,0 +1,286 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import mock +from mock import patch + +from toscaparser.nodetemplate import NodeTemplate +from toscaparser.tests.base import TestCase +from toscaparser.utils.gettextutils import _ +import toscaparser.utils.yamlparser +from translator.hot.tosca.tosca_compute import ToscaCompute + + +class ToscaComputeTest(TestCase): + + def _tosca_compute_test(self, tpl_snippet, expectedprops): + nodetemplates = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['node_templates']) + name = list(nodetemplates.keys())[0] + try: + nodetemplate = NodeTemplate(name, nodetemplates) + nodetemplate.validate() + toscacompute = ToscaCompute(nodetemplate) + toscacompute.handle_properties() + if not self._compare_properties(toscacompute.properties, + expectedprops): + raise Exception(_("Hot Properties are not" + " same as expected properties")) + except Exception: + # for time being rethrowing. Will be handled future based + # on new development in Glance and Graffiti + raise + + def _compare_properties(self, hotprops, expectedprops): + return all(item in hotprops.items() for item in expectedprops.items()) + + def test_node_compute_with_host_and_os_capabilities(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + disk_size: 10 GB + num_cpus: 4 + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: Linux + distribution: Fedora + version: 18.0 + ''' + expectedprops = {'flavor': 'm1.large', + 'image': 'fedora-amd64-heat-config'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_without_os_capabilities(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + disk_size: 10 GB + num_cpus: 4 + mem_size: 4 GB + #left intentionally + ''' + expectedprops = {'flavor': 'm1.large', + 'image': None} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_without_host_capabilities(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + os: + properties: + architecture: x86_64 + type: Linux + distribution: Fedora + version: 18.0 + ''' + expectedprops = {'flavor': None, + 'image': 'fedora-amd64-heat-config'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_without_properties_and_os_capabilities(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + properties: + #left intentionally + capabilities: + #left intentionally + ''' + expectedprops = {'flavor': None, + 'image': None} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_with_only_type(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + ''' + expectedprops = {'flavor': None, + 'image': None} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_host_capabilities_without_properties(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + #left intentionally + ''' + expectedprops = {'flavor': 'm1.nano'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_host_capabilities_without_disk_size(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + num_cpus: 4 + mem_size: 4 GB + ''' + expectedprops = {'flavor': 'm1.large'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_host_capabilities_without_mem_size(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + num_cpus: 4 + disk_size: 10 GB + ''' + expectedprops = {'flavor': 'm1.large'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + def test_node_compute_host_capabilities_without_mem_size_disk_size(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + num_cpus: 4 + ''' + expectedprops = {'flavor': 'm1.large'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + @patch('requests.post') + @patch('requests.get') + @patch('os.getenv') + def test_node_compute_with_nova_flavor(self, mock_os_getenv, + mock_get, mock_post): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + num_cpus: 1 + disk_size: 1 GB + mem_size: 1 GB + ''' + with patch('translator.common.utils.' + 'check_for_env_variables') as mock_check_env: + mock_check_env.return_value = True + mock_os_getenv.side_effect = ['demo', 'demo', + 'demo', 'http://abc.com/5000/', + 'demo', 'demo', + 'demo', 'http://abc.com/5000/'] + mock_ks_response = mock.MagicMock() + mock_ks_response.status_code = 200 + mock_ks_content = { + 'access': { + 'token': { + 'id': 'd1dfa603-3662-47e0-b0b6-3ae7914bdf76' + }, + 'serviceCatalog': [{ + 'type': 'compute', + 'endpoints': [{ + 'publicURL': 'http://abc.com' + }] + }] + } + } + mock_ks_response.content = json.dumps(mock_ks_content) + mock_nova_response = mock.MagicMock() + mock_nova_response.status_code = 200 + mock_flavor_content = { + 'flavors': [{ + 'name': 'm1.mock_flavor', + 'ram': 1024, + 'disk': 1, + 'vcpus': 1 + }] + } + mock_nova_response.content = \ + json.dumps(mock_flavor_content) + mock_post.return_value = mock_ks_response + mock_get.return_value = mock_nova_response + expectedprops = {'flavor': 'm1.mock_flavor'} + self._tosca_compute_test( + tpl_snippet, + expectedprops) + + @patch('requests.post') + @patch('requests.get') + @patch('os.getenv') + def test_node_compute_without_nova_flavor(self, mock_os_getenv, + mock_get, mock_post): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + num_cpus: 1 + disk_size: 1 GB + mem_size: 1 GB + ''' + with patch('translator.common.utils.' + 'check_for_env_variables') as mock_check_env: + mock_check_env.return_value = True + mock_os_getenv.side_effect = ['demo', 'demo', + 'demo', 'http://abc.com/5000/'] + mock_ks_response = mock.MagicMock() + mock_ks_content = {} + mock_ks_response.content = json.dumps(mock_ks_content) + expectedprops = {'flavor': 'm1.small', + 'user_data_format': 'SOFTWARE_CONFIG', + 'image': None} + self._tosca_compute_test( + tpl_snippet, + expectedprops) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_objectstore.py b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_objectstore.py new file mode 100644 index 0000000..4c42794 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_objectstore.py @@ -0,0 +1,71 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.nodetemplate import NodeTemplate +from toscaparser.tests.base import TestCase +from toscaparser.utils.gettextutils import _ +import toscaparser.utils.yamlparser +from translator.hot.tosca.tosca_object_storage import ToscaObjectStorage + + +class ToscaObjectStoreTest(TestCase): + + def _tosca_objectstore_test(self, tpl_snippet, expectedprops): + nodetemplates = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['node_templates']) + name = list(nodetemplates.keys())[0] + try: + nodetemplate = NodeTemplate(name, nodetemplates) + tosca_object_store = ToscaObjectStorage(nodetemplate) + tosca_object_store.handle_properties() + if not self._compare_properties(tosca_object_store.properties, + expectedprops): + raise Exception(_("Hot Properties are not" + " same as expected properties")) + except Exception: + # for time being rethrowing. Will be handled future based + # on new development + raise + + def _compare_properties(self, hotprops, expectedprops): + return all(item in hotprops.items() for item in expectedprops.items()) + + def test_node_objectstorage_with_properties(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.ObjectStorage + properties: + name: test + size: 1024 KB + maxsize: 1 MB + ''' + expectedprops = {'name': 'test', + 'X-Container-Meta': {'Quota-Bytes': 1000000}} + self._tosca_objectstore_test( + tpl_snippet, + expectedprops) + + def test_node_objectstorage_with_few_properties(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.ObjectStorage + properties: + name: test + size: 1024 B + ''' + expectedprops = {'name': 'test', + 'X-Container-Meta': {'Quota-Bytes': 1024}} + self._tosca_objectstore_test( + tpl_snippet, + expectedprops) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_policies.py b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_policies.py new file mode 100644 index 0000000..24368ab --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tests/test_tosca_policies.py @@ -0,0 +1,80 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.nodetemplate import NodeTemplate +from toscaparser.policy import Policy +from toscaparser.tests.base import TestCase +import toscaparser.utils.yamlparser +from translator.hot.tosca.tosca_compute import ToscaCompute +from translator.hot.tosca.tosca_policies import ToscaPolicies + + +class ToscaPoicyTest(TestCase): + + def _tosca_policy_test(self, tpl_snippet, expectedprops): + nodetemplates = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['node_templates']) + policies = (toscaparser.utils.yamlparser. + simple_parse(tpl_snippet)['policies']) + name = list(nodetemplates.keys())[0] + policy_name = list(policies[0].keys())[0] + for policy in policies: + tpl = policy[policy_name] + targets = tpl["targets"] + try: + nodetemplate = NodeTemplate(name, nodetemplates) + toscacompute = ToscaCompute(nodetemplate) + toscacompute.handle_properties() + + policy = Policy(policy_name, tpl, targets, + "node_templates") + toscapolicy = ToscaPolicies(policy) + nodetemplate = [toscacompute] + toscapolicy.handle_properties(nodetemplate) + + self.assertEqual(toscacompute.properties, expectedprops) + except Exception: + raise + + def test_compute_with_policies(self): + tpl_snippet = ''' + node_templates: + server: + type: tosca.nodes.Compute + capabilities: + host: + properties: + disk_size: 10 GB + num_cpus: 4 + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: Linux + distribution: Fedora + version: 18.0 + policies: + - my_compute_placement_policy: + type: tosca.policies.Placement + description: Apply my placement policy to my application servers + targets: [ server ] + ''' + expectedprops = {'flavor': 'm1.large', + 'image': 'fedora-amd64-heat-config', + 'scheduler_hints': { + 'group': { + 'get_resource': + 'my_compute_placement_policy'}}, + 'user_data_format': 'SOFTWARE_CONFIG'} + self._tosca_policy_test( + tpl_snippet, + expectedprops) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage.py new file mode 100644 index 0000000..d4b2f44 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage.py @@ -0,0 +1,71 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +from toscaparser.common.exception import InvalidPropertyValueError +from toscaparser.elements.scalarunit import ScalarUnit_Size +from toscaparser.functions import GetInput +from toscaparser.utils.gettextutils import _ +from translator.hot.syntax.hot_resource import HotResource + +log = logging.getLogger('heat-translator') + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaBlockStorage' + + +class ToscaBlockStorage(HotResource): + '''Translate TOSCA node type tosca.nodes.BlockStorage.''' + + toscatype = 'tosca.nodes.BlockStorage' + + def __init__(self, nodetemplate): + super(ToscaBlockStorage, self).__init__(nodetemplate, + type='OS::Cinder::Volume') + pass + + def handle_properties(self): + tosca_props = {} + for prop in self.nodetemplate.get_properties_objects(): + if isinstance(prop.value, GetInput): + tosca_props[prop.name] = {'get_param': prop.value.input_name} + else: + if prop.name == "size": + size_value = (ScalarUnit_Size(prop.value). + get_num_from_scalar_unit('GiB')) + if size_value == 0: + # OpenStack Heat expects size in GB + msg = _('Cinder Volume Size unit should be in GB.') + log.error(msg) + raise InvalidPropertyValueError( + what=msg) + elif int(size_value) < size_value: + size_value = int(size_value) + 1 + log.warning(_("Cinder unit value should be in " + "multiples of GBs. so corrected " + " %(prop_val)s to %(size_value)s GB.") + % {'prop_val': prop.value, + 'size_value': size_value}) + tosca_props[prop.name] = int(size_value) + else: + tosca_props[prop.name] = prop.value + self.properties = tosca_props + + def get_hot_attribute(self, attribute, args): + attr = {} + # Convert from a TOSCA attribute for a nodetemplate to a HOT + # attribute for the matching resource. Unless there is additional + # runtime support, this should be a one to one mapping. + if attribute == 'volume_id': + attr['get_resource'] = args[0] + return attr diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage_attachment.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage_attachment.py new file mode 100644 index 0000000..715d5b3 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_block_storage_attachment.py @@ -0,0 +1,48 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.functions import GetInput +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaBlockStorageAttachment' + + +class ToscaBlockStorageAttachment(HotResource): + '''Translate TOSCA relationship AttachesTo for Compute and BlockStorage.''' + + toscatype = 'tosca.nodes.BlockStorageAttachment' + + def __init__(self, template, nodetemplates, instance_uuid, volume_id): + super(ToscaBlockStorageAttachment, + self).__init__(template, type='OS::Cinder::VolumeAttachment') + self.nodetemplates = nodetemplates + self.instance_uuid = {'get_resource': instance_uuid} + self.volume_id = {'get_resource': volume_id} + + def handle_properties(self): + tosca_props = {} + for prop in self.nodetemplate.get_properties_objects(): + if isinstance(prop.value, GetInput): + tosca_props[prop.name] = {'get_param': prop.value.input_name} + else: + tosca_props[prop.name] = prop.value + self.properties = tosca_props + # instance_uuid and volume_id for Cinder volume attachment + self.properties['instance_uuid'] = self.instance_uuid + self.properties['volume_id'] = self.volume_id + if 'location' in self.properties: + self.properties['mountpoint'] = self.properties.pop('location') + + def handle_life_cycle(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_compute.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_compute.py new file mode 100644 index 0000000..e2ac130 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_compute.py @@ -0,0 +1,280 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import logging +import requests + +from toscaparser.utils.gettextutils import _ +import translator.common.utils +from translator.hot.syntax.hot_resource import HotResource + +log = logging.getLogger('heat-translator') + + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaCompute' + +# A design issue to be resolved is how to translate the generic TOSCA server +# properties to OpenStack flavors and images. At the Atlanta design summit, +# there was discussion on using Glance to store metadata and Graffiti to +# describe artifacts. We will follow these projects to see if they can be +# leveraged for this TOSCA translation. +# For development purpose at this time, we temporarily hardcode a list of +# flavors and images here +FLAVORS = {'m1.xlarge': {'mem_size': 16384, 'disk_size': 160, 'num_cpus': 8}, + 'm1.large': {'mem_size': 8192, 'disk_size': 80, 'num_cpus': 4}, + 'm1.medium': {'mem_size': 4096, 'disk_size': 40, 'num_cpus': 2}, + 'm1.small': {'mem_size': 2048, 'disk_size': 20, 'num_cpus': 1}, + 'm1.tiny': {'mem_size': 512, 'disk_size': 1, 'num_cpus': 1}, + 'm1.micro': {'mem_size': 128, 'disk_size': 0, 'num_cpus': 1}, + 'm1.nano': {'mem_size': 64, 'disk_size': 0, 'num_cpus': 1}} + +IMAGES = {'ubuntu-software-config-os-init': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Ubuntu', + 'version': '14.04'}, + 'ubuntu-12.04-software-config-os-init': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Ubuntu', + 'version': '12.04'}, + 'fedora-amd64-heat-config': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '18.0'}, + 'F18-x86_64-cfntools': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '19'}, + 'Fedora-x86_64-20-20131211.1-sda': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'Fedora', + 'version': '20'}, + 'cirros-0.3.1-x86_64-uec': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'CirrOS', + 'version': '0.3.1'}, + 'cirros-0.3.2-x86_64-uec': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'CirrOS', + 'version': '0.3.2'}, + 'rhel-6.5-test-image': {'architecture': 'x86_64', + 'type': 'Linux', + 'distribution': 'RHEL', + 'version': '6.5'}} + + +class ToscaCompute(HotResource): + '''Translate TOSCA node type tosca.nodes.Compute.''' + + COMPUTE_HOST_PROP = (DISK_SIZE, MEM_SIZE, NUM_CPUS) = \ + ('disk_size', 'mem_size', 'num_cpus') + + COMPUTE_OS_PROP = (ARCHITECTURE, DISTRIBUTION, TYPE, VERSION) = \ + ('architecture', 'distribution', 'type', 'version') + toscatype = 'tosca.nodes.Compute' + + def __init__(self, nodetemplate): + super(ToscaCompute, self).__init__(nodetemplate, + type='OS::Nova::Server') + # List with associated hot port resources with this server + self.assoc_port_resources = [] + pass + + def handle_properties(self): + self.properties = self.translate_compute_flavor_and_image( + self.nodetemplate.get_capability('host'), + self.nodetemplate.get_capability('os')) + self.properties['user_data_format'] = 'SOFTWARE_CONFIG' + tosca_props = self.get_tosca_props() + for key, value in tosca_props.items(): + self.properties[key] = value + + # To be reorganized later based on new development in Glance and Graffiti + def translate_compute_flavor_and_image(self, + host_capability, + os_capability): + hot_properties = {} + host_cap_props = {} + os_cap_props = {} + image = None + flavor = None + if host_capability: + for prop in host_capability.get_properties_objects(): + host_cap_props[prop.name] = prop.value + flavor = self._best_flavor(host_cap_props) + if os_capability: + for prop in os_capability.get_properties_objects(): + os_cap_props[prop.name] = prop.value + image = self._best_image(os_cap_props) + hot_properties['flavor'] = flavor + hot_properties['image'] = image + return hot_properties + + def _create_nova_flavor_dict(self): + '''Populates and returns the flavors dict using Nova ReST API''' + try: + access_dict = translator.common.utils.get_ks_access_dict() + access_token = translator.common.utils.get_token_id(access_dict) + if access_token is None: + return None + nova_url = translator.common.utils.get_url_for(access_dict, + 'compute') + if not nova_url: + return None + nova_response = requests.get(nova_url + '/flavors/detail', + headers={'X-Auth-Token': + access_token}) + if nova_response.status_code != 200: + return None + flavors = json.loads(nova_response.content)['flavors'] + flavor_dict = dict() + for flavor in flavors: + flavor_name = str(flavor['name']) + flavor_dict[flavor_name] = { + 'mem_size': flavor['ram'], + 'disk_size': flavor['disk'], + 'num_cpus': flavor['vcpus'], + } + except Exception as e: + # Handles any exception coming from openstack + log.warn(_('Choosing predefined flavors since received ' + 'Openstack Exception: %s') % str(e)) + return None + return flavor_dict + + def _best_flavor(self, properties): + log.info(_('Choosing the best flavor for given attributes.')) + # Check whether user exported all required environment variables. + flavors = FLAVORS + if translator.common.utils.check_for_env_variables(): + resp = self._create_nova_flavor_dict() + if resp: + flavors = resp + + # start with all flavors + match_all = flavors.keys() + + # TODO(anyone): Handle the case where the value contains something like + # get_input instead of a value. + # flavors that fit the CPU count + cpu = properties.get(self.NUM_CPUS) + if cpu is None: + self._log_compute_msg(self.NUM_CPUS, 'flavor') + match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu) + + # flavors that fit the mem size + mem = properties.get(self.MEM_SIZE) + if mem: + mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num( + mem, 'MB') + else: + self._log_compute_msg(self.MEM_SIZE, 'flavor') + match_cpu_mem = self._match_flavors(match_cpu, flavors, + self.MEM_SIZE, mem) + # flavors that fit the disk size + disk = properties.get(self.DISK_SIZE) + if disk: + disk = translator.common.utils.MemoryUnit.\ + convert_unit_size_to_num(disk, 'GB') + else: + self._log_compute_msg(self.DISK_SIZE, 'flavor') + match_cpu_mem_disk = self._match_flavors(match_cpu_mem, flavors, + self.DISK_SIZE, disk) + # if multiple match, pick the flavor with the least memory + # the selection can be based on other heuristic, e.g. pick one with the + # least total resource + if len(match_cpu_mem_disk) > 1: + return self._least_flavor(match_cpu_mem_disk, flavors, 'mem_size') + elif len(match_cpu_mem_disk) == 1: + return match_cpu_mem_disk[0] + else: + return None + + def _best_image(self, properties): + match_all = IMAGES.keys() + architecture = properties.get(self.ARCHITECTURE) + if architecture is None: + self._log_compute_msg(self.ARCHITECTURE, 'image') + match_arch = self._match_images(match_all, IMAGES, + self.ARCHITECTURE, architecture) + type = properties.get(self.TYPE) + if type is None: + self._log_compute_msg(self.TYPE, 'image') + match_type = self._match_images(match_arch, IMAGES, self.TYPE, type) + distribution = properties.get(self.DISTRIBUTION) + if distribution is None: + self._log_compute_msg(self.DISTRIBUTION, 'image') + match_distribution = self._match_images(match_type, IMAGES, + self.DISTRIBUTION, + distribution) + version = properties.get(self.VERSION) + if version is None: + self._log_compute_msg(self.VERSION, 'image') + match_version = self._match_images(match_distribution, IMAGES, + self.VERSION, version) + + if len(match_version): + return list(match_version)[0] + + def _match_flavors(self, this_list, this_dict, attr, size): + '''Return from this list all flavors matching the attribute size.''' + if not size: + return list(this_list) + matching_flavors = [] + for flavor in this_list: + if isinstance(size, int): + if this_dict[flavor][attr] >= size: + matching_flavors.append(flavor) + log.debug(_('Returning list of flavors matching the attribute size.')) + return matching_flavors + + def _least_flavor(self, this_list, this_dict, attr): + '''Return from this list the flavor with the smallest attr.''' + least_flavor = this_list[0] + for flavor in this_list: + if this_dict[flavor][attr] < this_dict[least_flavor][attr]: + least_flavor = flavor + return least_flavor + + def _match_images(self, this_list, this_dict, attr, prop): + if not prop: + return this_list + matching_images = [] + for image in this_list: + if this_dict[image][attr].lower() == str(prop).lower(): + matching_images.append(image) + return matching_images + + def get_hot_attribute(self, attribute, args): + attr = {} + # Convert from a TOSCA attribute for a nodetemplate to a HOT + # attribute for the matching resource. Unless there is additional + # runtime support, this should be a one to one mapping. + + # Note: We treat private and public IP addresses equally, but + # this will change in the future when TOSCA starts to support + # multiple private/public IP addresses. + log.debug(_('Converting TOSCA attribute for a nodetemplate to a HOT \ + attriute.')) + if attribute == 'private_address' or \ + attribute == 'public_address': + attr['get_attr'] = [self.name, 'networks', 'private', 0] + + return attr + + def _log_compute_msg(self, prop, what): + msg = _('No value is provided for Compute capability ' + 'property "%(prop)s". This may set an undesired "%(what)s" ' + 'in the template.') % {'prop': prop, 'what': what} + log.warn(msg) diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_database.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_database.py new file mode 100644 index 0000000..26c9d4d --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_database.py @@ -0,0 +1,30 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaDatabase' + + +class ToscaDatabase(HotResource): + '''Translate TOSCA node type tosca.nodes.Database.''' + + toscatype = 'tosca.nodes.Database' + + def __init__(self, nodetemplate): + super(ToscaDatabase, self).__init__(nodetemplate) + pass + + def handle_properties(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_dbms.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_dbms.py new file mode 100644 index 0000000..38c31bd --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_dbms.py @@ -0,0 +1,30 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaDbms' + + +class ToscaDbms(HotResource): + '''Translate TOSCA node type tosca.nodes.DBMS.''' + + toscatype = 'tosca.nodes.DBMS' + + def __init__(self, nodetemplate): + super(ToscaDbms, self).__init__(nodetemplate) + pass + + def handle_properties(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_network.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_network.py new file mode 100644 index 0000000..2b80313 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_network.py @@ -0,0 +1,118 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.common.exception import InvalidPropertyValueError +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaNetwork' + + +class ToscaNetwork(HotResource): + '''Translate TOSCA node type tosca.nodes.network.Network.''' + + toscatype = 'tosca.nodes.network.Network' + SUBNET_SUFFIX = '_subnet' + NETWORK_PROPS = ['network_name', 'network_id', 'segmentation_id'] + SUBNET_PROPS = ['ip_version', 'cidr', 'start_ip', 'end_ip', 'gateway_ip'] + + existing_resource_id = None + + def __init__(self, nodetemplate): + super(ToscaNetwork, self).__init__(nodetemplate, + type='OS::Neutron::Net') + pass + + def handle_properties(self): + tosca_props = self.get_tosca_props() + + net_props = {} + for key, value in tosca_props.items(): + if key in self.NETWORK_PROPS: + if key == 'network_name': + # If CIDR is specified network_name should + # be used as the name for the new network. + if 'cidr' in tosca_props.keys(): + net_props['name'] = value + # If CIDR is not specified network_name will be used + # to lookup existing network. If network_id is specified + # together with network_name then network_id should be + # used to lookup the network instead + elif 'network_id' not in tosca_props.keys(): + self.hide_resource = True + self.existing_resource_id = value + break + elif key == 'network_id': + self.hide_resource = True + self.existing_resource_id = value + break + elif key == 'segmentation_id': + net_props['segmentation_id'] = \ + tosca_props['segmentation_id'] + # Hardcode to vxlan for now until we add the network type + # and physical network to the spec. + net_props['value_specs'] = {'provider:segmentation_id': + value, 'provider:network_type': + 'vxlan'} + self.properties = net_props + + def handle_expansion(self): + # If the network resource should not be output (they are hidden), + # there is no need to generate subnet resource + if self.hide_resource: + return + + tosca_props = self.get_tosca_props() + + subnet_props = {} + + ip_pool_start = None + ip_pool_end = None + + for key, value in tosca_props.items(): + if key in self.SUBNET_PROPS: + if key == 'start_ip': + ip_pool_start = value + elif key == 'end_ip': + ip_pool_end = value + elif key == 'dhcp_enabled': + subnet_props['enable_dhcp'] = value + else: + subnet_props[key] = value + + if 'network_id' in tosca_props: + subnet_props['network'] = tosca_props['network_id'] + else: + subnet_props['network'] = '{ get_resource: %s }' % (self.name) + + # Handle allocation pools + # Do this only if both start_ip and end_ip are provided + # If one of them is missing throw an exception. + if ip_pool_start and ip_pool_end: + allocation_pool = {} + allocation_pool['start'] = ip_pool_start + allocation_pool['end'] = ip_pool_end + allocation_pools = [allocation_pool] + subnet_props['allocation_pools'] = allocation_pools + elif ip_pool_start: + raise InvalidPropertyValueError(what=_('start_ip')) + elif ip_pool_end: + raise InvalidPropertyValueError(what=_('end_ip')) + + subnet_resource_name = self.name + self.SUBNET_SUFFIX + + hot_resources = [HotResource(self.nodetemplate, + type='OS::Neutron::Subnet', + name=subnet_resource_name, + properties=subnet_props)] + return hot_resources diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_port.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_port.py new file mode 100644 index 0000000..4fd2d70 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_network_port.py @@ -0,0 +1,114 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaNetworkPort' +TOSCA_LINKS_TO = 'tosca.relationships.network.LinksTo' +TOSCA_BINDS_TO = 'tosca.relationships.network.BindsTo' + + +class ToscaNetworkPort(HotResource): + '''Translate TOSCA node type tosca.nodes.network.Port.''' + + toscatype = 'tosca.nodes.network.Port' + + def __init__(self, nodetemplate): + super(ToscaNetworkPort, self).__init__(nodetemplate, + type='OS::Neutron::Port') + # Default order + self.order = 0 + pass + + def _generate_networks_for_compute(self, port_resources): + '''Generate compute networks property list from the port resources.''' + networks = [] + for resource in port_resources: + networks.append({'port': '{ get_resource: %s }' % (resource.name)}) + return networks + + def _insert_sorted_resource(self, resources, resource): + '''Insert a resource in the list of resources and keep the order.''' + lo = 0 + hi = len(resources) + while lo < hi: + mid = (lo + hi) // 2 + if resource.order < resources[mid].order: + hi = mid + else: + lo = mid + 1 + resources.insert(lo, resource) + + def handle_properties(self): + tosca_props = self.get_tosca_props() + port_props = {} + for key, value in tosca_props.items(): + if key == 'ip_address': + fixed_ip = {} + fixed_ip['ip_address'] = value + port_props['fixed_ips'] = [fixed_ip] + elif key == 'order': + self.order = value + # TODO(sdmonov): Need to implement the properties below + elif key == 'is_default': + pass + elif key == 'ip_range_start': + pass + elif key == 'ip_range_end': + pass + else: + port_props[key] = value + + links_to = None + binds_to = None + for rel, node in self.nodetemplate.relationships.items(): + # Check for LinksTo relations. If found add a network property with + # the network name into the port + if not links_to and rel.is_derived_from(TOSCA_LINKS_TO): + links_to = node + + network_resource = None + for hot_resource in self.depends_on_nodes: + if links_to.name == hot_resource.name: + network_resource = hot_resource + self.depends_on.remove(hot_resource) + break + + if network_resource.existing_resource_id: + port_props['network'] =\ + str(network_resource.existing_resource_id) + else: + port_props['network'] = '{ get_resource: %s }'\ + % (links_to.name) + + # Check for BindsTo relationship. If found add network to the + # network property of the corresponding compute resource + elif not binds_to and rel.is_derived_from(TOSCA_BINDS_TO): + binds_to = node + compute_resource = None + for hot_resource in self.depends_on_nodes: + if binds_to.name == hot_resource.name: + compute_resource = hot_resource + self.depends_on.remove(hot_resource) + break + if compute_resource: + port_rsrcs = compute_resource.assoc_port_resources + self._insert_sorted_resource(port_rsrcs, self) + # TODO(sdmonov): Using generate networks every time we add + # a network is not the fastest way to do the things. We + # should do this only once at the end. + networks = self._generate_networks_for_compute(port_rsrcs) + compute_resource.properties['networks'] = networks + + self.properties = port_props diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_object_storage.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_object_storage.py new file mode 100644 index 0000000..177503f --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_object_storage.py @@ -0,0 +1,57 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from toscaparser.elements.scalarunit import ScalarUnit_Size +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaObjectStorage' + + +class ToscaObjectStorage(HotResource): + '''Translate TOSCA node type tosca.nodes.ObjectStorage.''' + + toscatype = 'tosca.nodes.ObjectStorage' + + def __init__(self, nodetemplate): + super(ToscaObjectStorage, self).__init__(nodetemplate, + type='OS::Swift::Container') + pass + + def handle_properties(self): + tosca_props = self.get_tosca_props() + objectstore_props = {} + container_quota = {} + skip_check = False + + for key, value in tosca_props.items(): + if key == "name": + objectstore_props["name"] = value + elif key == "size" or key == "maxsize": + # currently heat is not supporting dynamically increase + # the container quota-size. + # if both defined in tosca template, consider store_maxsize. + if skip_check: + continue + quota_size = None + if "maxsize" in tosca_props.keys(): + quota_size = tosca_props["maxsize"] + else: + quota_size = tosca_props["size"] + container_quota["Quota-Bytes"] = \ + ScalarUnit_Size(quota_size).get_num_from_scalar_unit() + objectstore_props["X-Container-Meta"] = container_quota + skip_check = True + + objectstore_props["X-Container-Read"] = '".r:*"' + self.properties = objectstore_props diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_policies.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_policies.py new file mode 100644 index 0000000..b32fc1d --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_policies.py @@ -0,0 +1,36 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaPolicies' + + +class ToscaPolicies(HotResource): + '''Translate TOSCA policy type tosca.poicies.Placement.''' + + toscatype = 'tosca.policies.Placement' + + def __init__(self, policy): + super(ToscaPolicies, self).__init__(policy, + type='OS::Nova::ServerGroup') + self.policy = policy + + def handle_properties(self, resources): + self.properties["name"] = self.name + self.properties["policies"] = ["affinity"] + for resource in resources: + if resource.name in self.policy.targets: + resource.properties["scheduler_hints"] = { + "group": {"get_resource": self.name}} diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_software_component.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_software_component.py new file mode 100644 index 0000000..044de43 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_software_component.py @@ -0,0 +1,30 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaSoftwareComponent' + + +class ToscaSoftwareComponent(HotResource): + '''Translate TOSCA node type tosca.nodes.SoftwareComponent.''' + + toscatype = 'tosca.nodes.SoftwareComponent' + + def __init__(self, nodetemplate): + super(ToscaSoftwareComponent, self).__init__(nodetemplate) + pass + + def handle_properties(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_web_application.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_web_application.py new file mode 100644 index 0000000..d0a9c5d --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_web_application.py @@ -0,0 +1,30 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaWebApplication' + + +class ToscaWebApplication(HotResource): + '''Translate TOSCA node type tosca.nodes.WebApplication.''' + + toscatype = 'tosca.nodes.WebApplication' + + def __init__(self, nodetemplate): + super(ToscaWebApplication, self).__init__(nodetemplate) + pass + + def handle_properties(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca/tosca_webserver.py b/tosca2heat/heat-translator/translator/hot/tosca/tosca_webserver.py new file mode 100644 index 0000000..83bda80 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca/tosca_webserver.py @@ -0,0 +1,30 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from translator.hot.syntax.hot_resource import HotResource + +# Name used to dynamically load appropriate map class. +TARGET_CLASS_NAME = 'ToscaWebserver' + + +class ToscaWebserver(HotResource): + '''Translate TOSCA node type tosca.nodes.WebServer.''' + + toscatype = 'tosca.nodes.WebServer' + + def __init__(self, nodetemplate): + super(ToscaWebserver, self).__init__(nodetemplate) + pass + + def handle_properties(self): + pass diff --git a/tosca2heat/heat-translator/translator/hot/tosca_translator.py b/tosca2heat/heat-translator/translator/hot/tosca_translator.py new file mode 100644 index 0000000..14ef8a1 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/tosca_translator.py @@ -0,0 +1,68 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +from toscaparser.utils.gettextutils import _ +from translator.hot.syntax.hot_template import HotTemplate +from translator.hot.translate_inputs import TranslateInputs +from translator.hot.translate_node_templates import TranslateNodeTemplates +from translator.hot.translate_outputs import TranslateOutputs + +log = logging.getLogger('heat-translator') + + +class TOSCATranslator(object): + '''Invokes translation methods.''' + + def __init__(self, tosca, parsed_params, deploy=None): + super(TOSCATranslator, self).__init__() + self.tosca = tosca + self.hot_template = HotTemplate() + self.parsed_params = parsed_params + self.deploy = deploy + self.node_translator = None + log.info(_('Initialized parmaters for translation.')) + + def translate(self): + self._resolve_input() + self.hot_template.description = self.tosca.description + self.hot_template.parameters = self._translate_inputs() + self.node_translator = TranslateNodeTemplates(self.tosca, + self.hot_template) + self.hot_template.resources = self.node_translator.translate() + self.hot_template.outputs = self._translate_outputs() + return self.hot_template.output_to_yaml() + + def _translate_inputs(self): + translator = TranslateInputs(self.tosca.inputs, self.parsed_params, + self.deploy) + return translator.translate() + + def _translate_outputs(self): + translator = TranslateOutputs(self.tosca.outputs, self.node_translator) + return translator.translate() + + # check all properties for all node and ensure they are resolved + # to actual value + def _resolve_input(self): + for n in self.tosca.nodetemplates: + for node_prop in n.get_properties_objects(): + if isinstance(node_prop.value, dict): + try: + self.parsed_params[node_prop.value['get_input']] + except Exception: + msg = (_('Must specify all input values in \ + TOSCA template, missing %s.') % + node_prop.value['get_input']) + log.error(msg) + raise ValueError(msg) diff --git a/tosca2heat/heat-translator/translator/hot/translate_inputs.py b/tosca2heat/heat-translator/translator/hot/translate_inputs.py new file mode 100644 index 0000000..6d677d1 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/translate_inputs.py @@ -0,0 +1,167 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +from toscaparser.dataentity import DataEntity +from toscaparser.elements.scalarunit import ScalarUnit_Size +from toscaparser.parameters import Input +from toscaparser.utils.gettextutils import _ +from toscaparser.utils.validateutils import TOSCAVersionProperty +from translator.hot.syntax.hot_parameter import HotParameter + + +INPUT_CONSTRAINTS = (CONSTRAINTS, DESCRIPTION, LENGTH, RANGE, + MIN, MAX, ALLOWED_VALUES, ALLOWED_PATTERN) = \ + ('constraints', 'description', 'length', 'range', + 'min', 'max', 'allowed_values', 'allowed_pattern') + +TOSCA_CONSTRAINT_OPERATORS = (EQUAL, GREATER_THAN, GREATER_OR_EQUAL, LESS_THAN, + LESS_OR_EQUAL, IN_RANGE, VALID_VALUES, LENGTH, + MIN_LENGTH, MAX_LENGTH, PATTERN) = \ + ('equal', 'greater_than', 'greater_or_equal', + 'less_than', 'less_or_equal', 'in_range', + 'valid_values', 'length', 'min_length', + 'max_length', 'pattern') + +TOSCA_TO_HOT_CONSTRAINTS_ATTRS = {'equal': 'allowed_values', + 'greater_than': 'range', + 'greater_or_equal': 'range', + 'less_than': 'range', + 'less_or_equal': 'range', + 'in_range': 'range', + 'valid_values': 'allowed_values', + 'length': 'length', + 'min_length': 'length', + 'max_length': 'length', + 'pattern': 'allowed_pattern'} + +TOSCA_TO_HOT_INPUT_TYPES = {'string': 'string', + 'integer': 'number', + 'float': 'number', + 'boolean': 'boolean', + 'timestamp': 'string', + 'scalar-unit.size': 'number', + 'version': 'string', + 'null': 'string', + 'PortDef': 'number'} + +log = logging.getLogger('heat-translator') + + +class TranslateInputs(object): + + '''Translate TOSCA Inputs to Heat Parameters.''' + + def __init__(self, inputs, parsed_params, deploy=None): + self.inputs = inputs + self.parsed_params = parsed_params + self.deploy = deploy + + def translate(self): + return self._translate_inputs() + + def _translate_inputs(self): + hot_inputs = [] + if 'key_name' in self.parsed_params and 'key_name' not in self.inputs: + name = 'key_name' + type = 'string' + default = self.parsed_params[name] + schema_dict = {'type': type, 'default': default} + input = Input(name, schema_dict) + self.inputs.append(input) + + log.info(_('Translating TOSCA input type to HOT input type.')) + for input in self.inputs: + hot_default = None + hot_input_type = TOSCA_TO_HOT_INPUT_TYPES[input.type] + + if input.name in self.parsed_params: + hot_default = DataEntity.validate_datatype( + input.type, self.parsed_params[input.name]) + elif input.default is not None: + hot_default = DataEntity.validate_datatype(input.type, + input.default) + else: + if self.deploy: + msg = _("Need to specify a value " + "for input {0}.").format(input.name) + log.error(msg) + raise Exception(msg) + if input.type == "scalar-unit.size": + # Assumption here is to use this scalar-unit.size for size of + # cinder volume in heat templates and will be in GB. + # should add logic to support other types if needed. + input_value = hot_default + hot_default = (ScalarUnit_Size(hot_default). + get_num_from_scalar_unit('GiB')) + if hot_default == 0: + msg = _('Unit value should be > 0.') + log.error(msg) + raise Exception(msg) + elif int(hot_default) < hot_default: + hot_default = int(hot_default) + 1 + log.warning(_("Cinder unit value should be in multiples" + " of GBs. So corrected %(input_value)s " + "to %(hot_default)s GB.") + % {'input_value': input_value, + 'hot_default': hot_default}) + if input.type == 'version': + hot_default = TOSCAVersionProperty(hot_default).get_version() + + hot_constraints = [] + if input.constraints: + for constraint in input.constraints: + if hot_default: + constraint.validate(hot_default) + hc, hvalue = self._translate_constraints( + constraint.constraint_key, constraint.constraint_value) + hot_constraints.append({hc: hvalue}) + + hot_inputs.append(HotParameter(name=input.name, + type=hot_input_type, + description=input.description, + default=hot_default, + constraints=hot_constraints)) + return hot_inputs + + def _translate_constraints(self, name, value): + hot_constraint = TOSCA_TO_HOT_CONSTRAINTS_ATTRS[name] + + # Offset used to support less_than and greater_than. + # TODO(anyone): when parser supports float, verify this works + offset = 1 + + if name == EQUAL: + hot_value = [value] + elif name == GREATER_THAN: + hot_value = {"min": value + offset} + elif name == GREATER_OR_EQUAL: + hot_value = {"min": value} + elif name == LESS_THAN: + hot_value = {"max": value - offset} + elif name == LESS_OR_EQUAL: + hot_value = {"max": value} + elif name == IN_RANGE: + # value is list type here + min_value = min(value) + max_value = max(value) + hot_value = {"min": min_value, "max": max_value} + elif name == LENGTH: + hot_value = {"min": value, "max": value} + elif name == MIN_LENGTH: + hot_value = {"min": value} + elif name == MAX_LENGTH: + hot_value = {"max": value} + else: + hot_value = value + return hot_constraint, hot_value diff --git a/tosca2heat/heat-translator/translator/hot/translate_node_templates.py b/tosca2heat/heat-translator/translator/hot/translate_node_templates.py new file mode 100644 index 0000000..46cdd71 --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/translate_node_templates.py @@ -0,0 +1,483 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib +import logging +import os +import six + +from toscaparser.functions import GetAttribute +from toscaparser.functions import GetInput +from toscaparser.functions import GetProperty +from toscaparser.properties import Property +from toscaparser.relationship_template import RelationshipTemplate +from toscaparser.utils.gettextutils import _ +from translator.common.exception import ToscaClassAttributeError +from translator.common.exception import ToscaClassImportError +from translator.common.exception import ToscaModImportError +from translator.conf.config import ConfigProvider as translatorConfig +from translator.hot.syntax.hot_resource import HotResource +from translator.hot.tosca.tosca_block_storage_attachment import ( + ToscaBlockStorageAttachment + ) + +########################### +# Module utility Functions +# for dynamic class loading +########################### + + +def _generate_type_map(): + '''Generate TOSCA translation types map. + + Load user defined classes from location path specified in conf file. + Base classes are located within the tosca directory. + + ''' + + # Base types directory + BASE_PATH = 'translator/hot/tosca' + + # Custom types directory defined in conf file + custom_path = translatorConfig.get_value('DEFAULT', + 'custom_types_location') + + # First need to load the parent module, for example 'contrib.hot', + # for all of the dynamically loaded classes. + classes = [] + _load_classes((BASE_PATH, custom_path), classes) + try: + types_map = {clazz.toscatype: clazz for clazz in classes} + except AttributeError as e: + raise ToscaClassAttributeError(message=e.message) + + return types_map + + +def _load_classes(locations, classes): + '''Dynamically load all the classes from the given locations.''' + + for cls_path in locations: + # Use the absolute path of the class path + abs_path = os.path.dirname(os.path.abspath(__file__)) + abs_path = abs_path.replace('translator/hot', cls_path) + + # Grab all the tosca type module files in the given path + mod_files = [f for f in os.listdir(abs_path) if f.endswith('.py') + and not f.startswith('__init__') + and f.startswith('tosca_')] + + # For each module, pick out the target translation class + for f in mod_files: + # NOTE: For some reason the existing code does not use the map to + # instantiate ToscaBlockStorageAttachment. Don't add it to the map + # here until the dependent code is fixed to use the map. + if f == 'tosca_block_storage_attachment.py': + continue + + mod_name = cls_path + '/' + f.strip('.py') + mod_name = mod_name.replace('/', '.') + try: + mod = importlib.import_module(mod_name) + target_name = getattr(mod, 'TARGET_CLASS_NAME') + clazz = getattr(mod, target_name) + classes.append(clazz) + except ImportError: + raise ToscaModImportError(mod_name=mod_name) + except AttributeError: + if target_name: + raise ToscaClassImportError(name=target_name, + mod_name=mod_name) + else: + # TARGET_CLASS_NAME is not defined in module. + # Re-raise the exception + raise + +################## +# Module constants +################## + +SECTIONS = (TYPE, PROPERTIES, REQUIREMENTS, INTERFACES, LIFECYCLE, INPUT) = \ + ('type', 'properties', 'requirements', + 'interfaces', 'lifecycle', 'input') + +# TODO(anyone): the following requirement names should not be hard-coded +# in the translator. Since they are basically arbitrary names, we have to get +# them from TOSCA type definitions. +# To be fixed with the blueprint: +# https://blueprints.launchpad.net/heat-translator/+spec/tosca-custom-types +REQUIRES = (CONTAINER, DEPENDENCY, DATABASE_ENDPOINT, CONNECTION, HOST) = \ + ('container', 'dependency', 'database_endpoint', + 'connection', 'host') + +INTERFACES_STATE = (CREATE, START, CONFIGURE, START, DELETE) = \ + ('create', 'stop', 'configure', 'start', 'delete') + + +TOSCA_TO_HOT_REQUIRES = {'container': 'server', 'host': 'server', + 'dependency': 'depends_on', "connects": 'depends_on'} + +TOSCA_TO_HOT_PROPERTIES = {'properties': 'input'} +log = logging.getLogger('heat-translator') + +TOSCA_TO_HOT_TYPE = _generate_type_map() + + +class TranslateNodeTemplates(object): + '''Translate TOSCA NodeTemplates to Heat Resources.''' + + def __init__(self, tosca, hot_template): + self.tosca = tosca + self.nodetemplates = self.tosca.nodetemplates + self.hot_template = hot_template + # list of all HOT resources generated + self.hot_resources = [] + # mapping between TOSCA nodetemplate and HOT resource + log.debug(_('Mapping between TOSCA nodetemplate and HOT resource.')) + self.hot_lookup = {} + self.policies = self.tosca.topology_template.policies + + def translate(self): + return self._translate_nodetemplates() + + def _recursive_handle_properties(self, resource): + '''Recursively handle the properties of the depends_on_nodes nodes.''' + # Use of hashtable (dict) here should be faster? + if resource in self.processed_resources: + return + self.processed_resources.append(resource) + for depend_on in resource.depends_on_nodes: + self._recursive_handle_properties(depend_on) + + if resource.type == "OS::Nova::ServerGroup": + resource.handle_properties(self.hot_resources) + else: + resource.handle_properties() + + def _translate_nodetemplates(self): + + log.debug(_('Translating the node templates.')) + suffix = 0 + # Copy the TOSCA graph: nodetemplate + for node in self.nodetemplates: + base_type = HotResource.get_base_type(node.type_definition) + hot_node = TOSCA_TO_HOT_TYPE[base_type.type](node) + self.hot_resources.append(hot_node) + self.hot_lookup[node] = hot_node + + # BlockStorage Attachment is a special case, + # which doesn't match to Heat Resources 1 to 1. + if base_type.type == "tosca.nodes.Compute": + volume_name = None + requirements = node.requirements + if requirements: + # Find the name of associated BlockStorage node + for requires in requirements: + for value in requires.values(): + if isinstance(value, dict): + for node_name in value.values(): + for n in self.nodetemplates: + if n.name == node_name: + volume_name = node_name + break + else: # unreachable code ! + for n in self.nodetemplates: + if n.name == node_name: + volume_name = node_name + break + + suffix = suffix + 1 + attachment_node = self._get_attachment_node(node, + suffix, + volume_name) + if attachment_node: + self.hot_resources.append(attachment_node) + for i in self.tosca.inputs: + if (i.name == 'key_name' and + node.get_property_value('key_name') is None): + schema = {'type': i.type, 'default': i.default} + value = {"get_param": "key_name"} + prop = Property(i.name, value, schema) + node._properties.append(prop) + + for policy in self.policies: + policy_type = policy.type_definition + policy_node = TOSCA_TO_HOT_TYPE[policy_type.type](policy) + self.hot_resources.append(policy_node) + + # Handle life cycle operations: this may expand each node + # into multiple HOT resources and may change their name + lifecycle_resources = [] + for resource in self.hot_resources: + expanded = resource.handle_life_cycle() + if expanded: + lifecycle_resources += expanded + self.hot_resources += lifecycle_resources + + # Handle configuration from ConnectsTo relationship in the TOSCA node: + # this will generate multiple HOT resources, set of 2 for each + # configuration + connectsto_resources = [] + for node in self.nodetemplates: + for requirement in node.requirements: + for endpoint, details in six.iteritems(requirement): + relation = None + if isinstance(details, dict): + target = details.get('node') + relation = details.get('relationship') + else: + target = details + if (target and relation and + not isinstance(relation, six.string_types)): + interfaces = relation.get('interfaces') + connectsto_resources += \ + self._create_connect_configs(node, + target, + interfaces) + self.hot_resources += connectsto_resources + + # Copy the initial dependencies based on the relationship in + # the TOSCA template + for node in self.nodetemplates: + for node_depend in node.related_nodes: + # if the source of dependency is a server and the + # relationship type is 'tosca.relationships.HostedOn', + # add dependency as properties.server + if node_depend.type == 'tosca.nodes.Compute' and \ + node.related[node_depend].type == \ + node.type_definition.HOSTEDON: + self.hot_lookup[node].properties['server'] = \ + {'get_resource': self.hot_lookup[node_depend].name} + # for all others, add dependency as depends_on + else: + self.hot_lookup[node].depends_on.append( + self.hot_lookup[node_depend].top_of_chain()) + + self.hot_lookup[node].depends_on_nodes.append( + self.hot_lookup[node_depend].top_of_chain()) + + # handle hosting relationship + for resource in self.hot_resources: + resource.handle_hosting() + + # handle built-in properties of HOT resources + # if a resource depends on other resources, + # their properties need to be handled first. + # Use recursion to handle the properties of the + # dependent nodes in correct order + self.processed_resources = [] + for resource in self.hot_resources: + self._recursive_handle_properties(resource) + + # handle resources that need to expand to more than one HOT resource + expansion_resources = [] + for resource in self.hot_resources: + expanded = resource.handle_expansion() + if expanded: + expansion_resources += expanded + self.hot_resources += expansion_resources + + # Resolve function calls: GetProperty, GetAttribute, GetInput + # at this point, all the HOT resources should have been created + # in the graph. + for resource in self.hot_resources: + # traverse the reference chain to get the actual value + inputs = resource.properties.get('input_values') + if inputs: + for name, value in six.iteritems(inputs): + inputs[name] = self._translate_input(value, resource) + + return self.hot_resources + + def _translate_input(self, input_value, resource): + get_property_args = None + if isinstance(input_value, GetProperty): + get_property_args = input_value.args + # to remove when the parser is fixed to return GetProperty + if isinstance(input_value, dict) and 'get_property' in input_value: + get_property_args = input_value['get_property'] + if get_property_args is not None: + hot_target = self._find_hot_resource_for_tosca( + get_property_args[0], resource) + if hot_target: + props = hot_target.get_tosca_props() + prop_name = get_property_args[1] + if prop_name in props: + return props[prop_name] + elif isinstance(input_value, GetAttribute): + # for the attribute + # get the proper target type to perform the translation + args = input_value.result() + hot_target = self._find_hot_resource_for_tosca(args[0], resource) + + return hot_target.get_hot_attribute(args[1], args) + # most of artifacts logic should move to the parser + elif isinstance(input_value, dict) and 'get_artifact' in input_value: + get_artifact_args = input_value['get_artifact'] + + hot_target = self._find_hot_resource_for_tosca( + get_artifact_args[0], resource) + artifacts = TranslateNodeTemplates.get_all_artifacts( + hot_target.nodetemplate) + + if get_artifact_args[1] in artifacts: + artifact = artifacts[get_artifact_args[1]] + if artifact.get('type', None) == 'tosca.artifacts.File': + return {'get_file': artifact.get('file')} + elif isinstance(input_value, GetInput): + if isinstance(input_value.args, list) \ + and len(input_value.args) == 1: + return {'get_param': input_value.args[0]} + else: + return {'get_param': input_value.args} + + return input_value + + @staticmethod + def get_all_artifacts(nodetemplate): + artifacts = nodetemplate.type_definition.get_value('artifacts', + parent=True) + if not artifacts: + artifacts = {} + tpl_artifacts = nodetemplate.entity_tpl.get('artifacts') + if tpl_artifacts: + artifacts.update(tpl_artifacts) + + return artifacts + + def _get_attachment_node(self, node, suffix, volume_name): + attach = False + ntpl = self.nodetemplates + for key, value in node.relationships.items(): + if key.is_derived_from('tosca.relationships.AttachesTo'): + if value.is_derived_from('tosca.nodes.BlockStorage'): + attach = True + if attach: + relationship_tpl = None + for req in node.requirements: + for key, val in req.items(): + attach = val + relship = val.get('relationship') + for rkey, rval in val.items(): + if relship and isinstance(relship, dict): + for rkey, rval in relship.items(): + if rkey == 'type': + relationship_tpl = val + attach = rval + elif rkey == 'template': + rel_tpl_list = \ + (self.tosca.topology_template. + _tpl_relationship_templates()) + relationship_tpl = rel_tpl_list[rval] + attach = rval + else: + continue + elif isinstance(relship, str): + attach = relship + relationship_tpl = val + relationship_templates = \ + self.tosca._tpl_relationship_templates() + if 'relationship' in relationship_tpl and \ + attach not in \ + self.tosca._tpl_relationship_types() and \ + attach in relationship_templates: + relationship_tpl['relationship'] = \ + relationship_templates[attach] + break + if relationship_tpl: + rval_new = attach + "_" + str(suffix) + att = RelationshipTemplate( + relationship_tpl, rval_new, + self.tosca._tpl_relationship_types()) + hot_node = ToscaBlockStorageAttachment(att, ntpl, + node.name, + volume_name + ) + return hot_node + + def find_hot_resource(self, name): + for resource in self.hot_resources: + if resource.name == name: + return resource + + def _find_tosca_node(self, tosca_name): + for node in self.nodetemplates: + if node.name == tosca_name: + return node + + def _find_hot_resource_for_tosca(self, tosca_name, + current_hot_resource=None): + if tosca_name == 'SELF': + return current_hot_resource + if tosca_name == 'HOST' and current_hot_resource is not None: + for req in current_hot_resource.nodetemplate.requirements: + if 'host' in req: + return self._find_hot_resource_for_tosca(req['host']) + + for node in self.nodetemplates: + if node.name == tosca_name: + return self.hot_lookup[node] + + return None + + def _create_connect_configs(self, source_node, target_name, + connect_interfaces): + connectsto_resources = [] + if connect_interfaces: + for iname, interface in six.iteritems(connect_interfaces): + connectsto_resources += \ + self._create_connect_config(source_node, target_name, + interface) + return connectsto_resources + + def _create_connect_config(self, source_node, target_name, + connect_interface): + connectsto_resources = [] + target_node = self._find_tosca_node(target_name) + # the configuration can occur on the source or the target + connect_config = connect_interface.get('pre_configure_target') + if connect_config is not None: + config_location = 'target' + else: + connect_config = connect_interface.get('pre_configure_source') + if connect_config is not None: + config_location = 'source' + else: + msg = _("Template error: " + "no configuration found for ConnectsTo " + "in {1}").format(self.nodetemplate.name) + log.error(msg) + raise Exception(msg) + config_name = source_node.name + '_' + target_name + '_connect_config' + implement = connect_config.get('implementation') + if config_location == 'target': + hot_config = HotResource(target_node, + config_name, + 'OS::Heat::SoftwareConfig', + {'config': {'get_file': implement}}) + elif config_location == 'source': + hot_config = HotResource(source_node, + config_name, + 'OS::Heat::SoftwareConfig', + {'config': {'get_file': implement}}) + connectsto_resources.append(hot_config) + hot_target = self._find_hot_resource_for_tosca(target_name) + hot_source = self._find_hot_resource_for_tosca(source_node.name) + connectsto_resources.append(hot_config. + handle_connectsto(source_node, + target_node, + hot_source, + hot_target, + config_location, + connect_interface)) + return connectsto_resources diff --git a/tosca2heat/heat-translator/translator/hot/translate_outputs.py b/tosca2heat/heat-translator/translator/hot/translate_outputs.py new file mode 100644 index 0000000..4197cdd --- /dev/null +++ b/tosca2heat/heat-translator/translator/hot/translate_outputs.py @@ -0,0 +1,48 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from toscaparser.utils.gettextutils import _ +from translator.hot.syntax.hot_output import HotOutput + +log = logging.getLogger('heat-translator') + + +class TranslateOutputs(object): + '''Translate TOSCA Outputs to Heat Outputs.''' + + def __init__(self, outputs, node_translator): + log.debug(_('Translating TOSCA outputs to HOT outputs.')) + self.outputs = outputs + self.nodes = node_translator + + def translate(self): + return self._translate_outputs() + + def _translate_outputs(self): + hot_outputs = [] + for output in self.outputs: + if output.value.name == 'get_attribute': + get_parameters = output.value.args + hot_target = self.nodes.find_hot_resource(get_parameters[0]) + hot_value = hot_target.get_hot_attribute(get_parameters[1], + get_parameters) + hot_outputs.append(HotOutput(output.name, + hot_value, + output.description)) + else: + hot_outputs.append(HotOutput(output.name, + output.value, + output.description)) + return hot_outputs |