From ccefa3514e4eeb4dfcd2b898e2531dc43434b1c6 Mon Sep 17 00:00:00 2001 From: Sahdev Zala Date: Fri, 8 Aug 2014 11:12:46 -0500 Subject: [PATCH] Implement TOSCA Block Storage Create TOSCA BlockStorage node type and its mapping in Heat. Introduce relationship template which will also provide a base for implementing relationship templates section in the TOSCA template. Introduce tests for generator/translator code. implements blueprint tosca-blockstorage-type Closes-Bug: #1351129 Closes-Bug: #1354632 Change-Id: I7df198ea45875031557e2607d932f14f272bf2e1 --- translator/hot/syntax/hot_resource.py | 2 +- translator/hot/syntax/hot_template.py | 2 +- translator/hot/tosca/tosca_block_storage.py | 32 ++++ .../tosca/tosca_block_storage_attachment.py | 40 +++++ translator/hot/tosca/tosca_compute.py | 5 +- translator/hot/translate_inputs.py | 4 +- translator/hot/translate_node_templates.py | 54 +++++- translator/tests/__init__.py | 0 translator/tests/base.py | 53 ++++++ .../tosca_blockstorage_with_attachment.yaml | 50 ++++++ ...multiple_blockstorage_with_attachment.yaml | 73 ++++++++ translator/tests/test_blockstorage.py | 80 +++++++++ .../toscalib/elements/TOSCA_definition.yaml | 39 +++++ .../toscalib/elements/capabilitytype.py | 10 +- translator/toscalib/elements/entitytype.py | 28 ++- translator/toscalib/elements/interfaces.py | 2 +- translator/toscalib/elements/nodetype.py | 54 ++---- .../toscalib/elements/relationshiptype.py | 21 ++- .../toscalib/elements/statefulentitytype.py | 12 ++ translator/toscalib/entity_template.py | 157 +++++++++++++++++ translator/toscalib/functions.py | 2 +- translator/toscalib/nodetemplate.py | 162 +++--------------- translator/toscalib/relationship_template.py | 39 +++++ translator/toscalib/tests/test_toscadef.py | 1 - .../toscalib/tests/test_toscatplvalidation.py | 48 ++++-- 25 files changed, 748 insertions(+), 222 deletions(-) create mode 100644 translator/hot/tosca/tosca_block_storage.py create mode 100644 translator/hot/tosca/tosca_block_storage_attachment.py create mode 100644 translator/tests/__init__.py create mode 100644 translator/tests/base.py create mode 100644 translator/tests/data/tosca_blockstorage_with_attachment.yaml create mode 100644 translator/tests/data/tosca_multiple_blockstorage_with_attachment.yaml create mode 100644 translator/tests/test_blockstorage.py create mode 100644 translator/toscalib/entity_template.py create mode 100644 translator/toscalib/relationship_template.py diff --git a/translator/hot/syntax/hot_resource.py b/translator/hot/syntax/hot_resource.py index 09269e90..e96dabab 100644 --- a/translator/hot/syntax/hot_resource.py +++ b/translator/hot/syntax/hot_resource.py @@ -103,7 +103,7 @@ class HotResource(object): # in interfaces_deploy_sequence # TODO(anyone): find some better way to encode this implicit sequence group = {} - for op, hot in deploy_lookup.iteritems(): + for op, hot in deploy_lookup.items(): # position to determine potential preceding nodes op_index = interfaces_deploy_sequence.index(op) for preceding_op in \ diff --git a/translator/hot/syntax/hot_template.py b/translator/hot/syntax/hot_template.py index fc8f00a0..a7ae3071 100644 --- a/translator/hot/syntax/hot_template.py +++ b/translator/hot/syntax/hot_template.py @@ -61,7 +61,7 @@ class HotTemplate(object): all_outputs.update(output.get_dict_output()) dict_output.update({self.OUTPUTS: all_outputs}) - yaml_string = yaml.dump(dict_output, default_flow_style=False) + yaml_string = yaml.dump(dict_output) # get rid of the '' from yaml.dump around numbers yaml_string = yaml_string.replace('\'', '') return version_string + desc_str + yaml_string diff --git a/translator/hot/tosca/tosca_block_storage.py b/translator/hot/tosca/tosca_block_storage.py new file mode 100644 index 00000000..d46eda89 --- /dev/null +++ b/translator/hot/tosca/tosca_block_storage.py @@ -0,0 +1,32 @@ +# +# 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 + + +class ToscaBlockStorage(HotResource): + '''Translate TOSCA node type 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.properties: + if isinstance(prop.value, dict): + for x, y in prop.value.items(): + if x == 'get_input': + tosca_props[prop.name] = {'get_param': y} + self.properties = tosca_props diff --git a/translator/hot/tosca/tosca_block_storage_attachment.py b/translator/hot/tosca/tosca_block_storage_attachment.py new file mode 100644 index 00000000..785705a3 --- /dev/null +++ b/translator/hot/tosca/tosca_block_storage_attachment.py @@ -0,0 +1,40 @@ +# +# 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 + + +class ToscaBlockStorageAttachment(HotResource): + '''Translate TOSCA relationship AttachTo for Compute and BlockStorage.''' + + def __init__(self, template, nodetemplates, instace_uuid, volume_id): + super(ToscaBlockStorageAttachment, + self).__init__(template, type='OS::Cinder::VolumeAttachment') + self.nodetemplates = nodetemplates + self.instace_uuid = instace_uuid + self.volume_id = volume_id + + def handle_properties(self): + tosca_props = {} + for prop in self.nodetemplate.properties: + if isinstance(prop.value, dict): + for x, y in prop.value.items(): + if x == 'get_input': + tosca_props[prop.name] = {'get_param': y} + self.properties = tosca_props + #instance_uuid and volume_id for Cinder volume attachment + self.properties['instance_uuid'] = self.instace_uuid + self.properties['volume_id'] = self.volume_id + + def handle_life_cycle(self): + pass diff --git a/translator/hot/tosca/tosca_compute.py b/translator/hot/tosca/tosca_compute.py index 8973ba7f..8b4a2be4 100755 --- a/translator/hot/tosca/tosca_compute.py +++ b/translator/hot/tosca/tosca_compute.py @@ -124,8 +124,9 @@ class ToscaCompute(HotResource): return this_list matching_flavors = [] for flavor in this_list: - if this_dict[flavor][attr] >= size: - matching_flavors.append(flavor) + if isinstance(size, int): + if this_dict[flavor][attr] >= size: + matching_flavors.append(flavor) return matching_flavors def _match_images(self, this_list, this_dict, attr, prop): diff --git a/translator/hot/translate_inputs.py b/translator/hot/translate_inputs.py index b4b8b2c6..a9c54097 100644 --- a/translator/hot/translate_inputs.py +++ b/translator/hot/translate_inputs.py @@ -65,7 +65,9 @@ class TranslateInputs(): hot_constraints = [] if input.constraints: for constraint in input.constraints: - constraint_name, value = constraint.iteritems().next() + for name, value in constraint.items(): + constraint_name = name + value = value hc, hvalue = self._translate_constraints(constraint_name, value) hot_constraints.append({hc: hvalue}) diff --git a/translator/hot/translate_node_templates.py b/translator/hot/translate_node_templates.py index c52cec24..bb021526 100644 --- a/translator/hot/translate_node_templates.py +++ b/translator/hot/translate_node_templates.py @@ -11,11 +11,16 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.hot.tosca.tosca_block_storage import ToscaBlockStorage +from translator.hot.tosca.tosca_block_storage_attachment import ( + ToscaBlockStorageAttachment + ) from translator.hot.tosca.tosca_compute import ToscaCompute from translator.hot.tosca.tosca_database import ToscaDatabase from translator.hot.tosca.tosca_dbms import ToscaDbms from translator.hot.tosca.tosca_webserver import ToscaWebserver from translator.hot.tosca.tosca_wordpress import ToscaWordpress +from translator.toscalib.relationship_template import RelationshipTemplate SECTIONS = (TYPE, PROPERTIES, REQUIREMENTS, INTERFACES, LIFECYCLE, INPUT) = \ ('type', 'properties', 'requirements', @@ -39,7 +44,8 @@ TOSCA_TO_HOT_TYPE = {'tosca.nodes.Compute': ToscaCompute, 'tosca.nodes.WebServer': ToscaWebserver, 'tosca.nodes.DBMS': ToscaDbms, 'tosca.nodes.Database': ToscaDatabase, - 'tosca.nodes.WebApplication.WordPress': ToscaWordpress} + 'tosca.nodes.WebApplication.WordPress': ToscaWordpress, + 'tosca.nodes.BlockStorage': ToscaBlockStorage} TOSCA_TO_HOT_REQUIRES = {'container': 'server', 'host': 'server', 'dependency': 'depends_on', "connects": 'depends_on'} @@ -61,18 +67,39 @@ class TranslateNodeTemplates(): hot_resources = [] hot_lookup = {} + attachment_suffix = 0 # Copy the TOSCA graph: nodetemplate for node in self.nodetemplates: hot_node = TOSCA_TO_HOT_TYPE[node.type](node) hot_resources.append(hot_node) hot_lookup[node] = hot_node - # Handle life cycle operations: this may expand each node into - # multiple HOT resources and may change their name + # BlockStorage Attachment is a special case, + # which doesn't match to Heat Resources 1 to 1. + if node.type == "tosca.nodes.Compute": + volume_name = None + reuirements = node.requirements + # Find the name of associated BlockStorage node + for requires in reuirements: + for value in requires.values(): + for n in self.nodetemplates: + if n.name == value: + volume_name = value + break + attachment_suffix = attachment_suffix + 1 + attachment_node = self._get_attachment_node(node, + attachment_suffix, + volume_name) + if attachment_node: + hot_resources.append(attachment_node) + + # Handle life cycle operations: this may expand each node + # into multiple HOT resources and may change their name lifecycle_resources = [] for resource in hot_resources: expanded = resource.handle_life_cycle() - lifecycle_resources += expanded + if expanded: + lifecycle_resources += expanded hot_resources += lifecycle_resources # Copy the initial dependencies based on the relationship in @@ -98,3 +125,22 @@ class TranslateNodeTemplates(): resource.handle_properties() return hot_resources + + def _get_attachment_node(self, node, suffix, volume_name): + attach = False + ntpl = self.nodetemplates + for key, value in node.relationship.items(): + if key.type == 'tosca.relationships.AttachTo': + if value.type == 'tosca.nodes.BlockStorage': + attach = True + if attach: + for req in node.requirements: + for rkey, rval in req.items(): + if rkey == 'type': + rval = rval + "_" + str(suffix) + att = RelationshipTemplate(req, rval, None) + hot_node = ToscaBlockStorageAttachment(att, ntpl, + node.name, + volume_name + ) + return hot_node diff --git a/translator/tests/__init__.py b/translator/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/translator/tests/base.py b/translator/tests/base.py new file mode 100644 index 00000000..6e932685 --- /dev/null +++ b/translator/tests/base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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 + +import fixtures +import testtools + +_TRUE_VALUES = ('True', 'true', '1', 'yes') + + +class TestCase(testtools.TestCase): + + """Test case base class for all unit tests.""" + + def setUp(self): + """Run before each test method to initialize test environment.""" + + super(TestCase, self).setUp() + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + # If timeout value is invalid do not set a timeout. + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + self.useFixture(fixtures.NestedTempfile()) + self.useFixture(fixtures.TempHomeDir()) + + if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + self.log_fixture = self.useFixture(fixtures.FakeLogger()) diff --git a/translator/tests/data/tosca_blockstorage_with_attachment.yaml b/translator/tests/data/tosca_blockstorage_with_attachment.yaml new file mode 100644 index 00000000..fc9a440b --- /dev/null +++ b/translator/tests/data/tosca_blockstorage_with_attachment.yaml @@ -0,0 +1,50 @@ +tosca_definitions_version: tosca_simple_1.0 + +description: > + TOSCA simple profile with server and attached block storage. + +inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + storage_size: + type: integer + default: 1 GB + description: Size of the storage to be created. + storage_snapshot_id: + type: string + description: Some identifier that represents an existing snapshot that should be used when creating the block storage. + storage_location: + type: string + description: The relative location (e.g., path on the file system), which provides the root location to address an attached node. + +node_templates: + my_server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + requirements: + - attachment: my_storage + type: AttachTo + properties: + location: { get_input: storage_location } + my_storage: + type: tosca.nodes.BlockStorage + properties: + size: { get_input: storage_size } + snapshot_id: { get_input: storage_snapshot_id } + +outputs: + public_ip: + description: Public IP address of the newly created compute instance. + value: { get_attr: [server, ip_address] } diff --git a/translator/tests/data/tosca_multiple_blockstorage_with_attachment.yaml b/translator/tests/data/tosca_multiple_blockstorage_with_attachment.yaml new file mode 100644 index 00000000..f377da8a --- /dev/null +++ b/translator/tests/data/tosca_multiple_blockstorage_with_attachment.yaml @@ -0,0 +1,73 @@ +tosca_definitions_version: tosca_simple_1.0 + +description: > + TOSCA simple profile with server and attached block storage. + +inputs: + cpus: + type: integer + description: Number of CPUs for the server. + constraints: + - valid_values: [ 1, 2, 4, 8 ] + storage_size: + type: integer + default: 1 GB + description: Size of the storage to be created. + storage_snapshot_id: + type: string + description: Some identifier that represents an existing snapshot that should be used when creating the block storage. + storage_location: + type: string + description: The relative location (e.g., path on the file system), which provides the root location to address an attached node. + +node_templates: + my_server: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + requirements: + - attachment: my_storage + type: AttachTo + properties: + location: { get_input: storage_location } + my_storage: + type: tosca.nodes.BlockStorage + properties: + size: { get_input: storage_size } + snapshot_id: { get_input: storage_snapshot_id } + + my_server2: + type: tosca.nodes.Compute + properties: + # compute properties (flavor) + disk_size: 10 + num_cpus: { get_input: cpus } + mem_size: 4096 + # host image properties + os_arch: x86_64 + os_type: Linux + os_distribution: Fedora + os_version: 18 + requirements: + - attachment: my_storage2 + type: AttachTo + properties: + location: { get_input: storage_location } + my_storage2: + type: tosca.nodes.BlockStorage + properties: + size: { get_input: storage_size } + snapshot_id: { get_input: storage_snapshot_id } + +outputs: + public_ip: + description: Public IP address of the newly created compute instance. + value: { get_attr: [server, ip_address] } diff --git a/translator/tests/test_blockstorage.py b/translator/tests/test_blockstorage.py new file mode 100644 index 00000000..2d3b66ef --- /dev/null +++ b/translator/tests/test_blockstorage.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 translator.hot.tosca_translator import TOSCATranslator +from translator.tests.base import TestCase +from translator.toscalib.tosca_template import ToscaTemplate +import translator.toscalib.utils.yamlparser + + +class ToscaBlockStorageTest(TestCase): + parsed_params = {'storage_snapshot_id': 'test_id', + 'storage_location': '/test', 'cpus': '1', + 'storage_size': '1'} + + def test_translate_single_storage(self): + '''TOSCA template with single BlockStorage and Attachment.''' + tosca_tpl = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/tosca_blockstorage_with_attachment.yaml") + + tosca = ToscaTemplate(tosca_tpl) + translate = TOSCATranslator(tosca, self.parsed_params) + output = translate.translate() + + expected_resouce = {'attachto_1': + {'type': 'OS::Cinder::VolumeAttachment', + 'properties': + {'instance_uuid': 'my_server', + 'location': {'get_param': 'storage_location'}, + 'volume_id': 'my_storage'}}} + + output_dict = translator.toscalib.utils.yamlparser.simple_parse(output) + + resources = output_dict.get('resources') + translated_value = resources.get('attachto_1') + expected_value = expected_resouce.get('attachto_1') + self.assertEqual(translated_value, expected_value) + + def test_translate_multi_storage(self): + '''TOSCA template with multiple BlockStorage and Attachment.''' + tosca_tpl = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data/tosca_multiple_blockstorage_with_attachment.yaml") + tosca = ToscaTemplate(tosca_tpl) + translated_volume_attachment = [] + translate = TOSCATranslator(tosca, self.parsed_params) + output = translate.translate() + + expected_resource_1 = {'type': 'OS::Cinder::VolumeAttachment', + 'properties': + {'instance_uuid': 'my_server', + 'location': {'get_param': 'storage_location'}, + 'volume_id': 'my_storage'}} + + expected_resource_2 = {'type': 'OS::Cinder::VolumeAttachment', + 'properties': + {'instance_uuid': 'my_server2', + 'location': {'get_param': 'storage_location'}, + 'volume_id': 'my_storage2'}} + + output_dict = translator.toscalib.utils.yamlparser.simple_parse(output) + + resources = output_dict.get('resources') + translated_volume_attachment.append(resources.get('attachto_1')) + translated_volume_attachment.append(resources.get('attachto_2')) + self.assertIn(expected_resource_1, translated_volume_attachment) + self.assertIn(expected_resource_2, translated_volume_attachment) diff --git a/translator/toscalib/elements/TOSCA_definition.yaml b/translator/toscalib/elements/TOSCA_definition.yaml index 37b71ed1..bcd37f43 100644 --- a/translator/toscalib/elements/TOSCA_definition.yaml +++ b/translator/toscalib/elements/TOSCA_definition.yaml @@ -92,6 +92,9 @@ tosca.nodes.Compute: capabilities: host: type: tosca.capabilities.Container + requirements: + - attachment: tosca.nodes.BlockStorage + - type: AttachTo tosca.nodes.SoftwareComponent: derived_from: tosca.nodes.Root @@ -169,6 +172,26 @@ tosca.nodes.WebApplication: requirements: - host: tosca.nodes.WebServer +tosca.nodes.BlockStorage: + derived_from: tosca.nodes.Root + properties: + size: + type: integer + constraints: + - greater_or_equal: 1 + volume_id: + type: string + required: false + snapshot_id: + type: string + required: false + attributes: + volumeId: + type: string + capabilities: + attachment: + type: tosca.capabilities.Attachment + ########################################################################## # Relationship Type. # A Relationship Type is a reusable entity that defines the type of one @@ -189,6 +212,19 @@ tosca.relationships.ConnectsTo: derived_from: tosca.relationships.DependsOn valid_targets: [ tosca.capabilities.Endpoint ] +tosca.relationships.AttachTo: + derived_from: tosca.relationships.Root + valid_targets: [ tosca.capabilities.Attachment ] + properties: + location: + required: true + type: string + constraints: + - min_length: 1 + device: + required: false + type: string + ########################################################################## # Capability Type. # A Capability Type is a reusable entity that describes a kind of @@ -220,6 +256,9 @@ tosca.capabilities.Endpoint: tosca.capabilities.DatabaseEndpoint: derived_from: tosca.capabilities.Endpoint +tosca.capabilities.Attachment: + derived_from: tosca.capabilities.Root + ########################################################################## # Interfaces Type. # The Interfaces element describes a list of one or more interface diff --git a/translator/toscalib/elements/capabilitytype.py b/translator/toscalib/elements/capabilitytype.py index 9f29c153..7e24604b 100644 --- a/translator/toscalib/elements/capabilitytype.py +++ b/translator/toscalib/elements/capabilitytype.py @@ -13,20 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. -from translator.toscalib.elements.entitytype import EntityType from translator.toscalib.elements.property_definition import PropertyDef +from translator.toscalib.elements.statefulentitytype import StatefulEntityType -class CapabilityTypeDef(EntityType): +class CapabilityTypeDef(StatefulEntityType): '''TOSCA built-in capabilities type.''' def __init__(self, name, ctype, ntype, properties): self.name = name - if self.CAPABILITY_PREFIX not in ctype: - ctype = self.CAPABILITY_PREFIX + ctype - if self.NODE_PREFIX not in ntype: - ntype = self.NODE_PREFIX + ntype - self.type = ctype + super(CapabilityTypeDef, self).__init__(ctype, self.CAPABILITY_PREFIX) self.nodetype = ntype self.properties = properties self.defs = {} diff --git a/translator/toscalib/elements/entitytype.py b/translator/toscalib/elements/entitytype.py index 7731e214..6ecab13b 100644 --- a/translator/toscalib/elements/entitytype.py +++ b/translator/toscalib/elements/entitytype.py @@ -23,6 +23,11 @@ log = logging.getLogger('tosca') class EntityType(object): '''Base class for TOSCA elements.''' + SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, + INTERFACES, CAPABILITIES) = \ + ('derived_from', 'properties', 'attributes', 'requirements', + 'interfaces', 'capabilities') + '''TOSCA definition file.''' TOSCA_DEF_FILE = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -32,10 +37,11 @@ class EntityType(object): TOSCA_DEF = loader(TOSCA_DEF_FILE) - RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO) = \ + RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO, ATTACHTO) = \ ('tosca.relationships.DependsOn', 'tosca.relationships.HostedOn', - 'tosca.relationships.ConnectsTo') + 'tosca.relationships.ConnectsTo', + 'tosca.relationships.AttachTo') NODE_PREFIX = 'tosca.nodes.' RELATIONSHIP_PREFIX = 'tosca.relationships.' @@ -49,3 +55,21 @@ class EntityType(object): def entity_value(self, defs, key): if key in defs: return defs[key] + + def get_value(self, ndtype, defs=None, parent=None): + value = None + if defs is None: + defs = self.defs + if ndtype in defs: + value = defs[ndtype] + if parent and not value: + p = self.parent_type + while value is None: + #check parent node + if not p: + break + if p and p.type == 'tosca.nodes.Root': + break + value = p.get_value(ndtype) + p = p.parent_type + return value diff --git a/translator/toscalib/elements/interfaces.py b/translator/toscalib/elements/interfaces.py index f81149b9..c88b5e0f 100644 --- a/translator/toscalib/elements/interfaces.py +++ b/translator/toscalib/elements/interfaces.py @@ -49,7 +49,7 @@ class InterfacesDef(StatefulEntityType): elif i == 'input': self.input = self._create_input_functions(j) else: - what = ('Interfaces of node template %s' % + what = ('Interfaces of template %s' % self.node_template.name) raise UnknownFieldError(what=what, field=i) else: diff --git a/translator/toscalib/elements/nodetype.py b/translator/toscalib/elements/nodetype.py index f75fada6..ddc5043c 100644 --- a/translator/toscalib/elements/nodetype.py +++ b/translator/toscalib/elements/nodetype.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from translator.toscalib.common.exception import InvalidNodeTypeError from translator.toscalib.elements.attribute_definition import AttributeDef from translator.toscalib.elements.capabilitytype import CapabilityTypeDef from translator.toscalib.elements.interfaces import InterfacesDef @@ -22,26 +21,11 @@ from translator.toscalib.elements.relationshiptype import RelationshipType from translator.toscalib.elements.statefulentitytype import StatefulEntityType -SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, - INTERFACES, CAPABILITIES) = \ - ('derived_from', 'properties', 'attributes', 'requirements', - 'interfaces', 'capabilities') - - class NodeType(StatefulEntityType): '''TOSCA built-in node type.''' def __init__(self, ntype, custom_def=None): - super(NodeType, self).__init__() - if self.NODE_PREFIX not in ntype: - ntype = self.NODE_PREFIX + ntype - if ntype in list(self.TOSCA_DEF.keys()): - self.defs = self.TOSCA_DEF[ntype] - elif custom_def and ntype in list(custom_def.keys()): - self.defs = custom_def[ntype] - else: - raise InvalidNodeTypeError(what=ntype) - self.type = ntype + super(NodeType, self).__init__(ntype, self.NODE_PREFIX, custom_def) self.related = {} @property @@ -55,7 +39,7 @@ class NodeType(StatefulEntityType): def properties_def(self): '''Return a list of property definition objects.''' properties = [] - props = self.get_value(PROPERTIES) + props = self.get_value(self.PROPERTIES) if props: for prop, schema in props.items(): properties.append(PropertyDef(prop, None, schema)) @@ -64,7 +48,7 @@ class NodeType(StatefulEntityType): @property def attributes_def(self): '''Return a list of attribute definition objects.''' - attrs = self.get_value(ATTRIBUTES) + attrs = self.get_value(self.ATTRIBUTES) if attrs: return [AttributeDef(attr, None, schema) for attr, schema in attrs.items()] @@ -84,6 +68,8 @@ class NodeType(StatefulEntityType): if requires: for req in requires: for key, value in req.items(): + if key == 'type': + continue relation = self._get_relation(key, value) rtype = RelationshipType(relation, key) relatednode = NodeType(value) @@ -112,9 +98,9 @@ class NodeType(StatefulEntityType): typecapabilities = [] self.cap_prop = None self.cap_type = None - caps = self.get_value(CAPABILITIES) + caps = self.get_value(self.CAPABILITIES) if caps is None: - caps = self.get_value(CAPABILITIES, None, True) + caps = self.get_value(self.CAPABILITIES, None, True) if caps: cproperties = None for name, value in caps.items(): @@ -128,24 +114,24 @@ class NodeType(StatefulEntityType): @property def requirements(self): - return self.get_value(REQUIREMENTS) + return self.get_value(self.REQUIREMENTS) def get_all_requirements(self): requires = self.requirements parent_node = self.parent_type if requires is None: - requires = self.get_value(REQUIREMENTS, None, True) + requires = self.get_value(self.REQUIREMENTS, None, True) parent_node = parent_node.parent_type if parent_node: while parent_node.type != 'tosca.nodes.Root': - req = parent_node.get_value(REQUIREMENTS, None, True) + req = parent_node.get_value(self.REQUIREMENTS, None, True) requires.extend(req) parent_node = parent_node.parent_type return requires @property def interfaces(self): - return self.get_value(INTERFACES) + return self.get_value(self.INTERFACES) @property def lifecycle_inputs(self): @@ -180,21 +166,3 @@ class NodeType(StatefulEntityType): for key, value in self.get_capability(name): if key == type: return value - - def get_value(self, ndtype, defs=None, parent=None): - value = None - if defs is None: - defs = self.defs - if ndtype in defs: - value = defs[ndtype] - if parent and not value: - p = self.parent_type - while value is None: - #check parent node - if not p: - break - if p and p.type == 'tosca.nodes.Root': - break - value = p.get_value(ndtype) - p = p.parent_type - return value diff --git a/translator/toscalib/elements/relationshiptype.py b/translator/toscalib/elements/relationshiptype.py index 1b1b84d0..98880cf8 100644 --- a/translator/toscalib/elements/relationshiptype.py +++ b/translator/toscalib/elements/relationshiptype.py @@ -13,18 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.toscalib.elements.property_definition import PropertyDef from translator.toscalib.elements.statefulentitytype import StatefulEntityType class RelationshipType(StatefulEntityType): '''TOSCA built-in relationship type.''' - - def __init__(self, type, capability_name): - super(RelationshipType, self).__init__() - self.defs = self.TOSCA_DEF[type] - if self.RELATIONSHIP_PREFIX not in type: - type = self.RELATIONSHIP_PREFIX + type - self.type = type + def __init__(self, type, capability_name, custom_def=None): + super(RelationshipType, self).__init__(type, self.RELATIONSHIP_PREFIX, + custom_def) self.capability_name = capability_name @property @@ -35,3 +32,13 @@ class RelationshipType(StatefulEntityType): def parent_type(self): '''Return a relationship this relationship is derived from.''' return self.derived_from(self.defs) + + @property + def properties_def(self): + '''Return a list of property definition objects.''' + properties = [] + props = self.get_value(self.PROPERTIES) + if props: + for prop, schema in props.items(): + properties.append(PropertyDef(prop, None, schema)) + return properties diff --git a/translator/toscalib/elements/statefulentitytype.py b/translator/toscalib/elements/statefulentitytype.py index 6b7d2166..a82d9280 100644 --- a/translator/toscalib/elements/statefulentitytype.py +++ b/translator/toscalib/elements/statefulentitytype.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from translator.toscalib.common.exception import InvalidNodeTypeError from translator.toscalib.elements.entitytype import EntityType @@ -27,3 +28,14 @@ class StatefulEntityType(EntityType): 'post_configure_target', 'add_target', 'remove_target'] + + def __init__(self, entitytype, prefix, custom_def=None): + if prefix not in entitytype: + entitytype = prefix + entitytype + if entitytype in list(self.TOSCA_DEF.keys()): + self.defs = self.TOSCA_DEF[entitytype] + elif custom_def and entitytype in list(custom_def.keys()): + self.defs = custom_def[entitytype] + else: + raise InvalidNodeTypeError(what=entitytype) + self.type = entitytype diff --git a/translator/toscalib/entity_template.py b/translator/toscalib/entity_template.py new file mode 100644 index 00000000..e71d7420 --- /dev/null +++ b/translator/toscalib/entity_template.py @@ -0,0 +1,157 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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.toscalib.common.exception import MissingRequiredFieldError +from translator.toscalib.common.exception import UnknownFieldError +from translator.toscalib.elements.capabilitytype import CapabilityTypeDef +from translator.toscalib.elements.interfaces import InterfacesDef +from translator.toscalib.elements.nodetype import NodeType +from translator.toscalib.elements.relationshiptype import RelationshipType +from translator.toscalib.properties import Property + + +class EntityTemplate(object): + '''Base class for TOSCA templates.''' + + SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type') + + def __init__(self, name, template, entity_name, custom_def=None): + self.name = name + self.entity_tpl = template + self.custom_def = custom_def + self._validate_field(self.entity_tpl) + if entity_name == 'node_type': + self.type_definition = NodeType(self.entity_tpl['type'], + custom_def) + if entity_name == 'relationship_type': + self.type_definition = RelationshipType(self.entity_tpl['type'], + custom_def) + + @property + def type(self): + return self.type_definition.type + + @property + def requirements(self): + return self.type_definition.get_value(self.REQUIREMENTS, + self.entity_tpl) + + @property + def properties(self): + props = [] + properties = self.type_definition.get_value(self.PROPERTIES, + self.entity_tpl) + if properties: + for name, value in properties.items(): + for p in self.type_definition.properties_def: + if p.name == name: + prop = Property(name, value, p.schema) + props.append(prop) + return props + + @property + def interfaces(self): + interfaces = [] + type_interfaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) + if type_interfaces: + for interface_type, value in type_interfaces.items(): + for op, op_def in value.items(): + iface = InterfacesDef(self.type_definition, + interfacetype=interface_type, + node_template=self, + name=op, + value=op_def) + interfaces.append(iface) + return interfaces + + @property + def capabilities(self): + capability = [] + properties = {} + cap_type = None + caps = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl) + if caps: + for name, value in caps.items(): + for prop, val in value.items(): + properties = val + for c in self.type_definition.capabilities: + if c.name == name: + cap_type = c.type + cap = CapabilityTypeDef(name, cap_type, + self.name, properties) + capability.append(cap) + return capability + + def _validate_properties(self, template, entitytype): + properties = entitytype.get_value(self.PROPERTIES, template) + allowed_props = [] + required_props = [] + for p in entitytype.properties_def: + allowed_props.append(p.name) + if p.required: + required_props.append(p.name) + if properties: + self._common_validate_field(properties, allowed_props, + 'Properties') + #make sure it's not missing any property required by a tosca type + missingprop = [] + for r in required_props: + if r not in properties.keys(): + missingprop.append(r) + if missingprop: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + else: + if required_props: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + + def _validate_capabilities(self): + type_capabilities = self.type_definition.capabilities + allowed_caps = [] + if type_capabilities: + for tcap in type_capabilities: + allowed_caps.append(tcap.name) + capabilities = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl) + if capabilities: + self._common_validate_field(capabilities, allowed_caps, + 'Capabilities') + + def _validate_field(self, template): + if not isinstance(template, dict): + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + try: + template[self.TYPE] + except KeyError: + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + + def _common_validate_field(self, schema, allowedlist, section): + for name in schema: + if name not in allowedlist: + raise UnknownFieldError( + what='%(section)s of template %(nodename)s' + % {'section': section, 'nodename': self.name}, + field=name) diff --git a/translator/toscalib/functions.py b/translator/toscalib/functions.py index c7e013cf..694ec7ab 100644 --- a/translator/toscalib/functions.py +++ b/translator/toscalib/functions.py @@ -76,7 +76,7 @@ class GetRefProperty(Function): if name: from translator.toscalib.nodetemplate import NodeTemplate tpl = NodeTemplate( - name, self.node_template.node_templates) + name, self.node_template.templates) caps = tpl.capabilities required_cap = self.args[1] required_property = self.args[2] diff --git a/translator/toscalib/nodetemplate.py b/translator/toscalib/nodetemplate.py index f09ebce9..6ffe5c6a 100644 --- a/translator/toscalib/nodetemplate.py +++ b/translator/toscalib/nodetemplate.py @@ -16,42 +16,24 @@ import logging -from translator.toscalib.common.exception import MissingRequiredFieldError from translator.toscalib.common.exception import TypeMismatchError from translator.toscalib.common.exception import UnknownFieldError -from translator.toscalib.elements.capabilitytype import CapabilityTypeDef from translator.toscalib.elements.interfaces import InterfacesDef from translator.toscalib.elements.interfaces import LIFECYCLE, CONFIGURE -from translator.toscalib.elements.nodetype import NodeType -from translator.toscalib.properties import Property - - -SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE) = \ - ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities', 'type') +from translator.toscalib.entity_template import EntityTemplate log = logging.getLogger('tosca') -class NodeTemplate(object): +class NodeTemplate(EntityTemplate): '''Node template from a Tosca profile.''' def __init__(self, name, node_templates, custom_def=None): - self.name = name - self.node_templates = node_templates - self._validate_field() - self.node_template = node_templates[self.name] - self.node_type = NodeType(self.node_template[TYPE], custom_def) + super(NodeTemplate, self).__init__(name, node_templates[name], + 'node_type', + custom_def) + self.templates = node_templates self.related = {} - @property - def type(self): - return self.node_type.type - - @property - def requirements(self): - return self.node_type.get_value(REQUIREMENTS, self.node_template) - @property def relationship(self): relation = {} @@ -59,72 +41,26 @@ class NodeTemplate(object): if requires: for r in requires: for cap, node in r.items(): - for rtype in self.node_type.relationship.keys(): + for rtype in self.type_definition.relationship.keys(): if cap == rtype.capability_name: - rtpl = NodeTemplate(node, self.node_templates) + rtpl = NodeTemplate(node, self.templates) relation[rtype] = rtpl return relation - @property - def capabilities(self): - capability = [] - properties = {} - cap_type = None - caps = self.node_type.get_value(CAPABILITIES, self.node_template) - if caps: - for name, value in caps.items(): - for prop, val in value.items(): - properties = val - for c in self.node_type.capabilities: - if c.name == name: - cap_type = c.type - cap = CapabilityTypeDef(name, cap_type, - self.name, properties) - capability.append(cap) - return capability - - @property - def interfaces(self): - interfaces = [] - type_interfaces = self.node_type.get_value(INTERFACES, - self.node_template) - if type_interfaces: - for interface_type, value in type_interfaces.items(): - for op, op_def in value.items(): - iface = InterfacesDef(self.node_type, - interfacetype=interface_type, - node_template=self, - name=op, - value=op_def) - interfaces.append(iface) - return interfaces - - @property - def properties(self): - props = [] - properties = self.node_type.get_value(PROPERTIES, self.node_template) - if properties: - for name, value in properties.items(): - for p in self.node_type.properties_def: - if p.name == name: - prop = Property(name, value, p.schema) - props.append(prop) - return props - def _add_next(self, nodetpl, relationship): self.related[nodetpl] = relationship @property def related_nodes(self): if not self.related: - for relation, node in self.node_type.relationship.items(): + for relation, node in self.type_definition.relationship.items(): for tpl in self.node_templates: if tpl == node.type: self.related[NodeTemplate(tpl)] = relation return self.related.keys() def ref_property(self, cap, cap_name, property): - requires = self.node_type.requirements + requires = self.type_definition.requirements name = None if requires: for r in requires: @@ -142,42 +78,32 @@ class NodeTemplate(object): def validate(self): self._validate_capabilities() - self._validate_requirments() - self._validate_properties() + self._validate_requirements() + self._validate_properties(self.entity_tpl, self.type_definition) self._validate_interfaces() for prop in self.properties: prop.validate() - def _validate_capabilities(self): - type_capabilities = self.node_type.capabilities - allowed_caps = [] - if type_capabilities: - for tcap in type_capabilities: - allowed_caps.append(tcap.name) - capabilities = self.node_type.get_value(CAPABILITIES, - self.node_template) - if capabilities: - self._common_validate_field(capabilities, allowed_caps, - 'Capabilities') - - def _validate_requirments(self): - type_requires = self.node_type.get_all_requirements() - allowed_reqs = [] + def _validate_requirements(self): + type_requires = self.type_definition.get_all_requirements() + allowed_reqs = ['type', 'properties', 'interfaces'] if type_requires: for treq in type_requires: for key in treq: allowed_reqs.append(key) - requires = self.node_type.get_value(REQUIREMENTS, self.node_template) + requires = self.type_definition.get_value(self.REQUIREMENTS, + self.entity_tpl) if requires: if not isinstance(requires, list): raise TypeMismatchError( - what='Requirements of node template %s' % self.name, + what='Requirements of template %s' % self.name, type='list') for req in requires: self._common_validate_field(req, allowed_reqs, 'Requirements') def _validate_interfaces(self): - ifaces = self.node_type.get_value(INTERFACES, self.node_template) + ifaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) if ifaces: for i in ifaces: for name, value in ifaces.items(): @@ -193,51 +119,5 @@ class NodeTemplate(object): 'Interfaces') else: raise UnknownFieldError( - what='Interfaces of node template %s' % self.name, + what='Interfaces of template %s' % self.name, field=name) - - def _validate_properties(self): - properties = self.node_type.get_value(PROPERTIES, self.node_template) - allowed_props = [] - required_props = [] - for p in self.node_type.properties_def: - allowed_props.append(p.name) - if p.required: - required_props.append(p.name) - if properties: - self._common_validate_field(properties, allowed_props, - 'Properties') - #make sure it's not missing any property required by a node type - missingprop = [] - for r in required_props: - if r not in properties.keys(): - missingprop.append(r) - if missingprop: - raise MissingRequiredFieldError( - what='Properties of node template %s' % self.name, - required=missingprop) - else: - if required_props: - raise MissingRequiredFieldError( - what='Properties of node template %s' % self.name, - required=missingprop) - - def _validate_field(self): - if not isinstance(self.node_templates[self.name], dict): - raise MissingRequiredFieldError( - what='Node template %s' % self.name, required=TYPE) - try: - self.node_templates[self.name][TYPE] - except KeyError: - raise MissingRequiredFieldError( - what='Node template %s' % self.name, required=TYPE) - self._common_validate_field(self.node_templates[self.name], SECTIONS, - 'Second level') - - def _common_validate_field(self, schema, allowedlist, section): - for name in schema: - if name not in allowedlist: - raise UnknownFieldError( - what='%(section)s of node template %(nodename)s' - % {'section': section, 'nodename': self.name}, - field=name) diff --git a/translator/toscalib/relationship_template.py b/translator/toscalib/relationship_template.py new file mode 100644 index 00000000..d34ac4b3 --- /dev/null +++ b/translator/toscalib/relationship_template.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 translator.toscalib.entity_template import EntityTemplate + +SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type') + +log = logging.getLogger('tosca') + + +class RelationshipTemplate(EntityTemplate): + '''Relationship template.''' + def __init__(self, relationship_template, name, custom_def=None): + super(RelationshipTemplate, self).__init__(name, + relationship_template, + 'relationship_type', + custom_def=None) + self.name = name.lower() + + def validate(self): + self._validate_properties(self.entity_tpl, self.type_definition) diff --git a/translator/toscalib/tests/test_toscadef.py b/translator/toscalib/tests/test_toscadef.py index b0c1aafc..7215367f 100644 --- a/translator/toscalib/tests/test_toscadef.py +++ b/translator/toscalib/tests/test_toscadef.py @@ -49,7 +49,6 @@ class ToscaDefTest(TestCase): sorted([p.name for p in compute_type.attributes_def])) def test_requirements(self): - self.assertEqual(compute_type.requirements, None) self.assertEqual( [{'host': 'tosca.nodes.Compute'}], [r for r in component_type.requirements]) diff --git a/translator/toscalib/tests/test_toscatplvalidation.py b/translator/toscalib/tests/test_toscatplvalidation.py index 9650175a..416c7969 100644 --- a/translator/toscalib/tests/test_toscatplvalidation.py +++ b/translator/toscalib/tests/test_toscatplvalidation.py @@ -14,12 +14,15 @@ # under the License. import os +import six + from translator.toscalib.common.exception import InvalidNodeTypeError from translator.toscalib.common.exception import MissingRequiredFieldError from translator.toscalib.common.exception import TypeMismatchError from translator.toscalib.common.exception import UnknownFieldError from translator.toscalib.nodetemplate import NodeTemplate from translator.toscalib.parameters import Input, Output +from translator.toscalib.relationship_template import RelationshipTemplate from translator.toscalib.tests.base import TestCase from translator.toscalib.tosca_template import ToscaTemplate import translator.toscalib.utils.yamlparser @@ -156,6 +159,7 @@ class ToscaTemplateValidationTest(TestCase): nodetemplate.capabilities nodetemplate.properties nodetemplate.interfaces + except Exception as err: self.assertTrue(isinstance(err, expectederror)) self.assertEqual(expectedmessage, err.__str__()) @@ -175,7 +179,7 @@ class ToscaTemplateValidationTest(TestCase): os_distribution: Fedora os_version: 18 ''' - expectedmessage = ('Node template server is missing ' + expectedmessage = ('Template server is missing ' 'required field: "type".') self._single_node_template_content_test(tpl_snippet, MissingRequiredFieldError, @@ -200,7 +204,7 @@ class ToscaTemplateValidationTest(TestCase): db_root_password: { get_property: [ mysql_dbms, \ dbms_root_password ] } ''' - expectedmessage = ('Second level of node template mysql_dbms ' + expectedmessage = ('Second level of template mysql_dbms ' 'contain(s) unknown field: "requirement", ' 'refer to the TOSCA specs to verify valid values.') self._single_node_template_content_test(tpl_snippet, @@ -244,7 +248,7 @@ class ToscaTemplateValidationTest(TestCase): create: webserver_install.sh start: webserver_start.sh ''' - expectedmessage = ('Requirements of node template webserver ' + expectedmessage = ('Requirements of template webserver ' 'must be of type: "list".') self._single_node_template_content_test(tpl_snippet, TypeMismatchError, @@ -269,7 +273,7 @@ class ToscaTemplateValidationTest(TestCase): tosca.interfaces.node.Lifecycle: configure: mysql_database_configure.sh ''' - expectedmessage = ('Requirements of node template mysql_database ' + expectedmessage = ('Requirements of template mysql_database ' 'contain(s) unknown field: "database_endpoint", ' 'refer to the TOSCA specs to verify valid values.') self._single_node_template_content_test(tpl_snippet, @@ -295,7 +299,7 @@ class ToscaTemplateValidationTest(TestCase): tosca.interfaces.node.Lifecycle: configure: mysql_database_configure.sh ''' - expectedmessage = ('Capabilities of node template mysql_database ' + expectedmessage = ('Capabilities of template mysql_database ' 'contain(s) unknown field: "http_endpoint", ' 'refer to the TOSCA specs to verify valid values.') self._single_node_template_content_test(tpl_snippet, @@ -317,7 +321,7 @@ class ToscaTemplateValidationTest(TestCase): os_distribution: Fedora os_version: 18 ''' - expectedmessage = ('Properties of node template server is missing ' + expectedmessage = ('Properties of template server is missing ' 'required field: "[\'os_type\']".') self._single_node_template_content_test(tpl_snippet, MissingRequiredFieldError, @@ -339,7 +343,7 @@ class ToscaTemplateValidationTest(TestCase): os_version: 18 os_image: F18_x86_64 ''' - expectedmessage = ('Properties of node template server contain(s) ' + expectedmessage = ('Properties of template server contain(s) ' 'unknown field: "os_image", refer to the TOSCA ' 'specs to verify valid values.') self._single_node_template_content_test(tpl_snippet, @@ -367,7 +371,7 @@ class ToscaTemplateValidationTest(TestCase): wp_db_port: { get_ref_property: [ database_endpoint, \ database_endpoint, port ] } ''' - expectedmessage = ('Interfaces of node template wordpress ' + expectedmessage = ('Interfaces of template wordpress ' 'contain(s) unknown field: ' '"tosca.interfaces.node.Lifecycles", ' 'refer to the TOSCA specs to verify valid values.') @@ -395,7 +399,7 @@ class ToscaTemplateValidationTest(TestCase): wp_db_port: { get_ref_property: [ database_endpoint, \ database_endpoint, port ] } ''' - expectedmessage = ('Interfaces of node template wordpress contain(s) ' + expectedmessage = ('Interfaces of template wordpress contain(s) ' 'unknown field: "config", refer to the TOSCA specs' ' to verify valid values.') self._single_node_template_content_test(tpl_snippet, @@ -422,9 +426,33 @@ class ToscaTemplateValidationTest(TestCase): wp_db_port: { get_ref_property: [ database_endpoint, \ database_endpoint, port ] } ''' - expectedmessage = ('Interfaces of node template wordpress contain(s) ' + expectedmessage = ('Interfaces of template wordpress contain(s) ' 'unknown field: "inputs", refer to the TOSCA specs' ' to verify valid values.') self._single_node_template_content_test(tpl_snippet, UnknownFieldError, expectedmessage) + + def test_relationship_template_properties(self): + tpl_snippet = ''' + relationship_templates: + storage_attachto: + type: AttachTo + properties: + device: test_device + ''' + expectedmessage = ('Properties of template ' + 'storage_attachto is missing required field: ' + '"[\'location\']".') + self._single_rel_template_content_test(tpl_snippet, + MissingRequiredFieldError, + expectedmessage) + + def _single_rel_template_content_test(self, tpl_snippet, expectederror, + expectedmessage): + rel_template = (translator.toscalib.utils.yamlparser. + simple_parse(tpl_snippet))['relationship_templates'] + name = list(rel_template.keys())[0] + rel_template = RelationshipTemplate(rel_template[name], name) + err = self.assertRaises(expectederror, rel_template.validate) + self.assertEqual(expectedmessage, six.text_type(err))