TOSCA parser

Parse the TOSCA template as per the specification defined in TOSCA Simple
Profile in YAML v1.0 working draft 1.

Partially
Implements: blueprint heat-translator-tosca

Change-Id: I9d71a28b7b48b7591a55c2b0947ce63beb335bbf
This commit is contained in:
Sahdev Zala
2014-05-21 21:34:33 -05:00
parent 0c1591bf15
commit 0d23118af2
12 changed files with 628 additions and 18 deletions

View File

@@ -166,6 +166,34 @@ tosca.nodes.WebApplication:
requirements:
- host: tosca.nodes.WebServer
tosca.nodes.WebApplication.WordPress:
derived_from: tosca.nodes.WebApplication
properties:
admin_user:
required: no
type: string
admin_password:
required: no
type: string
db_host:
required: no
type: string
requirements:
- database_endpoint: tosca.nodes.Database
interfaces:
tosca.interfaces.node.Lifecycle:
inputs:
db_host:
type: string
db_port:
type: integer
db_name:
type: string
db_user:
type: string
db_password:
type: string
##########################################################################
# Relationship Type.
# A Relationship Type is a reusable entity that defines the type of one

View File

@@ -28,7 +28,9 @@ class EntityType(object):
os.path.dirname(os.path.abspath(__file__)),
"TOSCA_definition.yaml")
TOSCA_DEF = translator.toscalib.utils.yamlparser.load_yaml(TOSCA_DEF_FILE)
loader = translator.toscalib.utils.yamlparser.load_yaml
TOSCA_DEF = loader(TOSCA_DEF_FILE)
RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO) = \
('tosca.relationships.DependsOn',

View File

@@ -67,13 +67,20 @@ class NodeType(StatefulEntityType):
'''
relationship = {}
requires = self.requirements
parent_node = self.parent_type
if requires is None:
requires = self._get_value(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)
requires.extend(req)
parent_node = parent_node.parent_type
if requires:
for req in requires:
for key, value in req.items():
relation = self._get_relation(key, value)
rtype = RelationshipType(relation)
rtype = RelationshipType(relation, key)
relatednode = NodeType(value)
relationship[rtype] = relatednode
return relationship
@@ -156,11 +163,12 @@ class NodeType(StatefulEntityType):
if key == type:
return value
def _get_value(self, key, defs=None, parent=None):
def _get_value(self, ndtype, defs=None, parent=None):
value = None
if defs is None:
defs = self.defs
value = self.entity_value(defs, key)
if ndtype in defs:
value = defs[ndtype]
if parent and not value:
p = self.parent_type
while value is None:
@@ -169,6 +177,6 @@ class NodeType(StatefulEntityType):
break
if p and p.type == 'tosca.nodes.Root':
break
value = self.entity_value(defs, key)
value = p._get_value(ndtype)
p = p.parent_type
return value

View File

@@ -51,9 +51,15 @@ class PropertyDef(object):
@property
def constraints(self):
if self.schema:
try:
if self.CONSTRAINTS in self.schema:
return self.schema[self.CONSTRAINTS]
except Exception:
#Simply passing the exception to ignore Node Type in-line value.
#Exception will not be needed when Node Type and Node Template
#properties are separated.
#TODO(spzala)
pass
@property
def description(self):
@@ -69,16 +75,19 @@ class PropertyDef(object):
self._validate_datatype()
def _validate_datatype(self):
if self.schema:
dtype = self.schema[self.TYPE]
if dtype == self.STRING:
return Constraint.validate_string(self.value)
elif dtype == self.INTEGER:
return Constraint.validate_integer(self.value)
elif dtype == self.NUMBER:
return Constraint.validate_number(self.value)
elif dtype == self.LIST:
return Constraint.validate_list(self.value)
try:
if self.TYPE in self.schema:
dtype = self.schema[self.TYPE]
if dtype == self.STRING:
return Constraint.validate_string(self.value)
elif dtype == self.INTEGER:
return Constraint.validate_integer(self.value)
elif dtype == self.NUMBER:
return Constraint.validate_number(self.value)
elif dtype == self.LIST:
return Constraint.validate_list(self.value)
except Exception:
pass
def _validate_constraints(self):
constraints = self.constraints

View File

@@ -19,10 +19,11 @@ from translator.toscalib.elements.statefulentitytype import StatefulEntityType
class RelationshipType(StatefulEntityType):
'''TOSCA built-in relationship type.'''
def __init__(self, type):
def __init__(self, type, capability_name):
super(RelationshipType, self).__init__()
self.defs = self.TOSCA_DEF[type]
self.type = type
self.capability_name = capability_name
@property
def valid_targets(self):

View File

@@ -0,0 +1,154 @@
# 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.elements.capabilitytype import CapabilityTypeDef
from translator.toscalib.elements.interfaces import InterfacesDef
from translator.toscalib.elements.nodetype import NodeType
from translator.toscalib.elements.properties import PropertyDef
from translator.toscalib.utils.gettextutils import _
SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS,
INTERFACES, CAPABILITIES) = \
('derived_from', 'properties', 'requirements', 'interfaces',
'capabilities')
log = logging.getLogger('tosca')
class NodeTemplate(NodeType):
'''Node template from a Tosca profile.'''
def __init__(self, name, nodetemplates):
super(NodeTemplate, self).__init__(nodetemplates[name]['type'])
self.name = name
self.nodetemplates = nodetemplates
self.nodetemplate = nodetemplates[self.name]
self.type = self.nodetemplate['type']
self.type_properties = self.properties_def
self.type_capabilities = self.capabilities
self.type_lifecycle_ops = self.lifecycle_operations
self.type_relationship = self.relationship
self.related = {}
@property
def tpl_requirements(self):
return self._get_value(REQUIREMENTS, self.nodetemplate)
@property
def tpl_relationship(self):
tpl_relation = {}
requires = self.tpl_requirements
if requires:
for r in requires:
for cap, node in r.items():
for relationship_type in self.type_relationship.keys():
if cap == relationship_type.capability_name:
rtpl = NodeTemplate(node, self.nodetemplates)
tpl_relation[relationship_type] = rtpl
return tpl_relation
@property
def tpl_capabilities(self):
tpl_cap = []
properties = {}
cap_type = None
caps = self._get_value(CAPABILITIES, self.nodetemplate)
if caps:
for name, value in caps.items():
for prop, val in value.items():
properties = val
for c in self.type_capabilities:
if c.name == name:
cap_type = c.type
cap = CapabilityTypeDef(name, cap_type,
self.name, properties)
tpl_cap.append(cap)
return tpl_cap
@property
def tpl_interfaces(self):
tpl_ifaces = []
ifaces = self._get_value(INTERFACES, self.nodetemplate)
if ifaces:
for i in ifaces:
for name, value in ifaces.items():
for ops, val in value.items():
iface = InterfacesDef(None, name, self.name,
ops, val)
tpl_ifaces.append(iface)
return tpl_ifaces
@property
def properties(self):
tpl_props = []
properties = self._get_value(PROPERTIES, self.nodetemplate)
requiredprop = []
for p in self.type_properties:
if p.required:
requiredprop.append(p.name)
if properties:
#make sure it's not missing any property required by a node type
missingprop = []
for r in requiredprop:
if r not in properties.keys():
missingprop.append(r)
if missingprop:
raise ValueError(_("Node template %(tpl)s is missing "
"one or more required properties %(prop)s")
% {'tpl': self.name, 'prop': missingprop})
for name, value in properties.items():
prop = PropertyDef(name, self.type, value, self.name)
tpl_props.append(prop)
else:
if requiredprop:
raise ValueError(_("Node template %(tpl)s is missing"
"one or more required properties %(prop)s")
% {'tpl': self.name, 'prop': requiredprop})
return tpl_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.tpl_relationship.items():
for tpl in self.nodetemplates:
if tpl == node.type:
self.related[NodeTemplate(tpl)] = relation
return self.related.keys()
def ref_property(self, cap, cap_name, property):
requires = self.tpl_requirements
tpl_name = None
if requires:
for r in requires:
for cap, node in r.items():
if cap == cap:
tpl_name = node
break
if tpl_name:
tpl = NodeTemplate(tpl_name, self.nodetemplates)
caps = tpl.tpl_capabilities
for c in caps:
if c.name == cap_name:
if c.property == property:
return c.property_value
def validate(self):
for prop in self.properties:
prop.validate()

View File

@@ -0,0 +1,76 @@
# 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.elements.constraints import Constraint
from translator.toscalib.elements.properties import PropertyDef
log = logging.getLogger('tosca')
class Input(object):
def __init__(self, name, schema):
self.name = name
self.schema = schema
@property
def type(self):
return self.schema['type']
@property
def description(self):
if PropertyDef.DESCRIPTION in self.schema:
return self.schema['description']
@property
def default(self):
if self.PropertyDef.DEFAULT in self.schema:
return self.schema['default']
@property
def constraints(self):
if PropertyDef.CONSTRAINTS in self.schema:
return self.schema['constraints']
def validate(self):
self.validate_type(self.type)
if self.constraints:
self.validate_constraints(self.constraints)
def validate_type(self, input_type):
if input_type not in PropertyDef.PROPERTIY_TYPES:
raise ValueError(_('Invalid type %s') % type)
def validate_constraints(self, constraints):
for constraint in constraints:
for key in constraint.keys():
if key not in Constraint.CONSTRAINTS:
raise ValueError(_('Invalid constraint %s') % constraint)
class Output(object):
def __init__(self, name, attrs):
self.name = name
self.attrs = attrs
@property
def description(self):
return self.attrs['description']
@property
def value(self):
return self.attrs['value']

View File

@@ -0,0 +1,102 @@
tosca_definitions_version: tosca_simple_1.0
description: >
TOSCA simple profile with wordpress, web server and mysql on the same server.
inputs:
cpus:
type: integer
description: Number of CPUs for the server.
constraints:
- valid_values: [ 1, 2, 4, 8 ]
db_name:
type: string
description: The name of the database.
db_user:
type: string
description: The user name of the DB user.
db_pwd:
type: string
description: The WordPress database admin account password.
db_root_pwd:
type: string
description: Root password for MySQL.
db_port:
type: integer
description: Port for the MySQL database.
node_templates:
wordpress:
type: tosca.nodes.WebApplication.WordPress
requirements:
- host: webserver
- database_endpoint: mysql_database
interfaces:
tosca.interfaces.node.Lifecycle:
create: wordpress_install.sh
configure:
implementation: wordpress_configure.sh
input:
wp_db_name: { get_property: [ mysql_database, db_name ] }
wp_db_user: { get_property: [ mysql_database, db_user ] }
wp_db_password: { get_property: [ mysql_database, db_password ] }
wp_db_port: { get_ref_property: [ database_endpoint, database_endpoint, port ] }
mysql_database:
type: tosca.nodes.Database
properties:
db_name: { get_input: db_name }
db_user: { get_input: db_user }
db_password: { get_input: db_pwd }
capabilities:
database_endpoint:
properties:
port: { get_input: db_port }
requirements:
- host: mysql_dbms
interfaces:
tosca.interfaces.node.Lifecycle:
configure: mysql_database_configure.sh
mysql_dbms:
type: tosca.nodes.DBMS
properties:
dbms_root_password: { get_input: db_root_pwd }
dbms_port: { get_input: db_port }
requirements:
- host: server
interfaces:
tosca.interfaces.node.Lifecycle:
create: mysql_dbms_install.sh
start: mysql_dbms_start.sh
configure:
implementation: mysql_dbms_configure.sh
input:
db_root_password: { get_property: [ mysql_dbms, dbms_root_password ] }
webserver:
type: tosca.nodes.WebServer
requirements:
- host: server
interfaces:
tosca.interfaces.node.Lifecycle:
create: webserver_install.sh
start: webserver_start.sh
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
outputs:
website_url:
description: URL for Wordpress wiki.
value: { get_property: [server, ip_address] }

View File

@@ -15,7 +15,6 @@
from translator.toscalib.elements.nodetype import NodeType
from translator.toscalib.tests.base import TestCase
compute_type = NodeType('tosca.nodes.Compute')
component_type = NodeType('tosca.nodes.SoftwareComponent')

View File

@@ -0,0 +1,100 @@
# 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.toscalib.tests.base import TestCase
from translator.toscalib.tosca_template import ToscaTemplate
class ToscaTemplateTest(TestCase):
'''TOSCA template.'''
tosca_tpl = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"data/tosca_single_instance_wordpress.yaml")
tosca = ToscaTemplate(tosca_tpl)
def test_version(self):
self.assertEqual(self.tosca.version, "tosca_simple_1.0")
def test_description(self):
expected_description = "TOSCA simple profile with wordpress, " \
"web server and mysql on the same server."
self.assertEqual(self.tosca.description, expected_description)
def test_inputs(self):
self.assertEqual(
['cpus', 'db_name', 'db_port',
'db_pwd', 'db_root_pwd', 'db_user'],
sorted([input.name for input in self.tosca.inputs]))
input_name = "db_port"
expected_description = "Port for the MySQL database."
for input in self.tosca.inputs:
if input.name == input_name:
self.assertEqual(input.description, expected_description)
def test_node_tpls(self):
'''Test nodetemplate names.'''
self.assertEqual(
['mysql_database', 'mysql_dbms', 'server',
'webserver', 'wordpress'],
sorted([tpl.name for tpl in self.tosca.nodetemplates]))
tpl_name = "mysql_database"
expected_type = "tosca.nodes.Database"
expected_properties = ['db_name', 'db_password', 'db_user']
expected_capabilities = ['database_endpoint']
expected_requirements = [{'host': 'mysql_dbms'}]
expected_relationshp = ['tosca.relationships.HostedOn']
expected_host = ['mysql_dbms']
expected_interface = ['tosca.interfaces.node.Lifecycle']
for tpl in self.tosca.nodetemplates:
if tpl_name == tpl.name:
'''Test node type.'''
self.assertEqual(tpl.type, expected_type)
'''Test properties.'''
self.assertEqual(
expected_properties,
sorted([p.name for p in tpl.properties]))
'''Test capabilities.'''
self.assertEqual(
expected_capabilities,
sorted([p.name for p in tpl.tpl_capabilities]))
'''Test requirements.'''
self.assertEqual(
expected_requirements, tpl.tpl_requirements)
'''Test relationship.'''
self.assertEqual(
expected_relationshp,
[x.type for x in tpl.tpl_relationship.keys()])
self.assertEqual(
expected_host,
[y.name for y in tpl.tpl_relationship.values()])
'''Test interfaces.'''
self.assertEqual(
expected_interface,
[x.type for x in tpl.tpl_interfaces])
def test_outputs(self):
self.assertEqual(
['website_url'],
sorted([output.name for output in self.tosca.outputs]))

View File

@@ -0,0 +1,83 @@
# 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.nodetemplate import NodeTemplate
from translator.toscalib.parameters import Input, Output
from translator.toscalib.tpl_relationship_graph import ToscaGraph
from translator.toscalib.utils.gettextutils import _
import translator.toscalib.utils.yamlparser
SECTIONS = (VERSION, DESCRIPTION, INPUTS,
NODE_TEMPLATES, OUTPUTS) = \
('tosca_definitions_version', 'description', 'inputs',
'node_templates', 'outputs')
log = logging.getLogger("tosca.model")
class ToscaTemplate(object):
'''Load the template data.'''
def __init__(self, path):
self.tpl = translator.toscalib.utils.yamlparser.load_yaml(path)
self.version = self._tpl_version()
self.description = self._tpl_description()
self.inputs = self._inputs()
self.nodetemplates = self._nodetemplates()
self.outputs = self._outputs()
self.graph = ToscaGraph(self.nodetemplates)
def _inputs(self):
inputs = []
for name, attrs in self._tpl_inputs().items():
input = Input(name, attrs)
if not isinstance(input.schema, dict):
raise ValueError(_("The input %(input)s has no attributes.")
% {'input': input})
input.validate()
inputs.append(input)
return inputs
def _nodetemplates(self):
nodetemplates = []
tpls = self._tpl_nodetemplates()
for name, value in tpls.items():
tpl = NodeTemplate(name, tpls)
tpl.validate()
nodetemplates.append(tpl)
return nodetemplates
def _outputs(self):
outputs = []
for name, attrs in self._tpl_outputs().items():
outputs.append(Output(name, attrs))
return outputs
def _tpl_version(self):
return self.tpl[VERSION]
def _tpl_description(self):
return self.tpl[DESCRIPTION].rstrip()
def _tpl_inputs(self):
return self.tpl[INPUTS]
def _tpl_nodetemplates(self):
return self.tpl[NODE_TEMPLATES]
def _tpl_outputs(self):
return self.tpl[OUTPUTS]

View File

@@ -0,0 +1,48 @@
# 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.
class ToscaGraph(object):
'''Graph of Tosca Node Templates.'''
def __init__(self, nodetemplates):
self.nodetemplates = nodetemplates
self.vertices = {}
self._create()
def _create_vertex(self, node):
self.vertices[node.name] = node
def _create_edge(self, node1, node2, relationship):
if node1 not in self.vertices:
self._create_vertex(node1)
self.vertices[node1.name]._add_next(node2,
relationship)
def vertex(self, node):
if node in self.vertices:
return self.vertices[node]
def __iter__(self):
return iter(self.vertices.values())
def _create(self):
for node in self.nodetemplates:
if node.tpl_relationship:
relation = node.tpl_relationship
for relation, nodetpls in relation.items():
for tpl in self.nodetemplates:
if tpl.name == nodetpls.name:
self._create_edge(node, tpl, relation)
self._create_vertex(node)