Merge "Implement LoadBalancer node type per TOSCA v1.0 spec"
This commit is contained in:
@@ -49,6 +49,18 @@ class TOSCAException(Exception):
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
@staticmethod
|
||||
def generate_inv_schema_property_error(self, attr, value, valid_values):
|
||||
msg = (_('Schema definition of "%(propname)s" has '
|
||||
'"%(attr)s" attribute with invalid value '
|
||||
'"%(value1)s". The value must be one of '
|
||||
'"%(value2)s".') % {"propname": self.name,
|
||||
"attr": attr,
|
||||
"value1": value,
|
||||
"value2": valid_values})
|
||||
ExceptionCollector.appendException(
|
||||
InvalidSchemaError(message=msg))
|
||||
|
||||
@staticmethod
|
||||
def set_fatal_format_exception(flag):
|
||||
if isinstance(flag, bool):
|
||||
|
||||
@@ -366,6 +366,25 @@ tosca.nodes.ObjectStorage:
|
||||
storage_endpoint:
|
||||
type: tosca.capabilities.Endpoint
|
||||
|
||||
tosca.nodes.LoadBalancer:
|
||||
derived_from: tosca.nodes.Root
|
||||
properties:
|
||||
algorithm:
|
||||
type: string
|
||||
required: false
|
||||
status: experimental
|
||||
capabilities:
|
||||
client:
|
||||
type: tosca.capabilities.Endpoint.Public
|
||||
occurrences: [0, UNBOUNDED]
|
||||
description: the Floating (IP) client’s on the public network can connect to
|
||||
requirements:
|
||||
- application:
|
||||
capability: tosca.capabilities.Endpoint
|
||||
relationship: tosca.relationships.RoutesTo
|
||||
occurrences: [0, UNBOUNDED]
|
||||
description: Connection to one or more load balanced applications
|
||||
|
||||
##########################################################################
|
||||
# Relationship Type.
|
||||
# A Relationship Type is a reusable entity that defines the type of one
|
||||
|
||||
@@ -12,19 +12,35 @@
|
||||
|
||||
from toscaparser.common.exception import ExceptionCollector
|
||||
from toscaparser.common.exception import InvalidSchemaError
|
||||
from toscaparser.common.exception import TOSCAException
|
||||
from toscaparser.utils.gettextutils import _
|
||||
|
||||
|
||||
class PropertyDef(object):
|
||||
'''TOSCA built-in Property type.'''
|
||||
|
||||
VALID_PROPERTY_KEYNAMES = (PROPERTY_KEYNAME_DEFAULT,
|
||||
PROPERTY_KEYNAME_REQUIRED,
|
||||
PROPERTY_KEYNAME_STATUS) = \
|
||||
('default', 'required', 'status')
|
||||
|
||||
PROPERTY_REQUIRED_DEFAULT = False
|
||||
|
||||
VALID_REQUIRED_VALUES = ['true', 'false']
|
||||
VALID_STATUS_VALUES = (PROPERTY_STATUS_SUPPORTED,
|
||||
PROPERTY_STATUS_EXPERIMENTAL) = \
|
||||
('supported', 'experimental')
|
||||
|
||||
PROPERTY_STATUS_DEFAULT = PROPERTY_STATUS_SUPPORTED
|
||||
|
||||
def __init__(self, name, value=None, schema=None):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.schema = schema
|
||||
self._status = self.PROPERTY_STATUS_DEFAULT
|
||||
self._required = self.PROPERTY_REQUIRED_DEFAULT
|
||||
|
||||
# Validate required 'type' property exists
|
||||
try:
|
||||
self.schema['type']
|
||||
except KeyError:
|
||||
@@ -33,32 +49,52 @@ class PropertyDef(object):
|
||||
ExceptionCollector.appendException(
|
||||
InvalidSchemaError(message=msg))
|
||||
|
||||
if 'required' in self.schema:
|
||||
required = self.schema['required']
|
||||
if not isinstance(required, bool):
|
||||
if required.lower() not in self.VALID_REQUIRED_VALUES:
|
||||
valid_values = ', '.join(self.VALID_REQUIRED_VALUES)
|
||||
msg = (_('Schema definition of "%(propname)s" has '
|
||||
'"required" attribute with invalid value '
|
||||
'"%(value1)s". The value must be one of '
|
||||
'"%(value2)s".') % {"propname": self.name,
|
||||
"value1": required,
|
||||
"value2": valid_values})
|
||||
ExceptionCollector.appendException(
|
||||
InvalidSchemaError(message=msg))
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
if self.schema:
|
||||
for prop_key, prop_value in self.schema.items():
|
||||
if prop_key == 'required' and prop_value:
|
||||
return True
|
||||
return False
|
||||
self._load_required_attr_from_schema()
|
||||
self._load_status_attr_from_schema()
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self.schema:
|
||||
for prop_key, prop_value in self.schema.items():
|
||||
if prop_key == 'default':
|
||||
if prop_key == self.PROPERTY_KEYNAME_DEFAULT:
|
||||
return prop_value
|
||||
return None
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
return self._required
|
||||
|
||||
def _load_required_attr_from_schema(self):
|
||||
# IF 'required' keyname exists verify it's a boolean,
|
||||
# if so override default
|
||||
if self.PROPERTY_KEYNAME_REQUIRED in self.schema:
|
||||
value = self.schema[self.PROPERTY_KEYNAME_REQUIRED]
|
||||
if isinstance(value, bool):
|
||||
self._required = value
|
||||
else:
|
||||
valid_values = ', '.join(self.VALID_REQUIRED_VALUES)
|
||||
attr = self.PROPERTY_KEYNAME_REQUIRED
|
||||
TOSCAException.generate_inv_schema_property_error(self,
|
||||
attr,
|
||||
value,
|
||||
valid_values)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
def _load_status_attr_from_schema(self):
|
||||
# IF 'status' keyname exists verify it's a valid value,
|
||||
# if so override default
|
||||
if self.PROPERTY_KEYNAME_STATUS in self.schema:
|
||||
value = self.schema[self.PROPERTY_KEYNAME_STATUS]
|
||||
if value in self.VALID_STATUS_VALUES:
|
||||
self._status = value
|
||||
else:
|
||||
valid_values = ', '.join(self.VALID_STATUS_VALUES)
|
||||
attr = self.PROPERTY_KEYNAME_STATUS
|
||||
TOSCAException.generate_inv_schema_property_error(self,
|
||||
attr,
|
||||
value,
|
||||
valid_values)
|
||||
|
||||
@@ -198,27 +198,31 @@ class EntityTemplate(object):
|
||||
required_props = []
|
||||
for p in entitytype.get_properties_def_objects():
|
||||
allowed_props.append(p.name)
|
||||
if p.required:
|
||||
# If property is 'required' and has no 'default' value then record
|
||||
if p.required and p.default is None:
|
||||
required_props.append(p.name)
|
||||
# validate all required properties have values
|
||||
if properties:
|
||||
req_props_no_value_or_default = []
|
||||
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:
|
||||
req_props_no_value_or_default.append(r)
|
||||
# Required properties found without value or a default value
|
||||
if req_props_no_value_or_default:
|
||||
ExceptionCollector.appendException(
|
||||
MissingRequiredFieldError(
|
||||
what='"properties" of template "%s"' % self.name,
|
||||
required=missingprop))
|
||||
required=req_props_no_value_or_default))
|
||||
else:
|
||||
# Required properties in schema, but not in template
|
||||
if required_props:
|
||||
ExceptionCollector.appendException(
|
||||
MissingRequiredFieldError(
|
||||
what='"properties" of template "%s"' % self.name,
|
||||
required=missingprop))
|
||||
required=required_props))
|
||||
|
||||
def _validate_field(self, template):
|
||||
if not isinstance(template, dict):
|
||||
|
||||
75
toscaparser/tests/data/tosca_load_balancer.yaml
Normal file
75
toscaparser/tests/data/tosca_load_balancer.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
# Note: this could eventually be translated to a Neutron Load Balancer
|
||||
# However, in Heat/HOT the preferred way of doing this is creating an Autoscale Group
|
||||
#
|
||||
#heat_template_version: 2015-04-30 ...
|
||||
#resources:
|
||||
#load_bal_resource:
|
||||
# type: OS::Neutron::Pool
|
||||
# properties:
|
||||
# admin_state_up: Boolean
|
||||
# description: String
|
||||
# lb_method: String
|
||||
# monitors: [Value, Value, ...]
|
||||
# name: String
|
||||
# protocol: String
|
||||
# provider: String
|
||||
# subnet: String
|
||||
# vip: {
|
||||
# "description": String,
|
||||
# "name": String,
|
||||
# "connection_limit": Integer,
|
||||
# "protocol_port": Integer,
|
||||
# "subnet": String,
|
||||
# "address": String,
|
||||
# "admin_state_up": Boolean,
|
||||
# "session_persistence":
|
||||
# {
|
||||
# "cookie_name": String,
|
||||
# "type": String}
|
||||
# }
|
||||
#
|
||||
# example from: https://gist.github.com/therve/9231701
|
||||
#
|
||||
#resources:
|
||||
# web_server_group:
|
||||
# type: AWS::AutoScaling::AutoScalingGroup
|
||||
# properties:
|
||||
# AvailabilityZones: [nova]
|
||||
# LaunchConfigurationName: {get_resource: launch_config}
|
||||
# MinSize: 1
|
||||
# MaxSize: 3
|
||||
# LoadBalancerNames:
|
||||
# - {get_resource: mylb}
|
||||
# mypool:
|
||||
# type: OS::Neutron::Pool
|
||||
# properties:
|
||||
# protocol: HTTP
|
||||
# monitors: [{get_resource: mymonitor}]
|
||||
# subnet_id: {get_param: subnet_id}
|
||||
# lb_method: ROUND_ROBIN
|
||||
# vip:
|
||||
# protocol_port: 80
|
||||
# mylb:
|
||||
# type: OS::Neutron::LoadBalancer
|
||||
# properties:
|
||||
# protocol_port: 80
|
||||
# pool_id: {get_resource: mypool}
|
||||
|
||||
tosca_definitions_version: tosca_simple_yaml_1_0
|
||||
|
||||
description: Template for deploying a load balancer with predefined endpoint properties.
|
||||
|
||||
topology_template:
|
||||
node_templates:
|
||||
simple_load_balancer:
|
||||
type: tosca.nodes.LoadBalancer
|
||||
capabilities:
|
||||
# properties:
|
||||
# algorithm: DEFAULT (define new keyword, ROUND_ROBIN?)
|
||||
# Client, public facing endpoint
|
||||
client:
|
||||
properties:
|
||||
network_name: PUBLIC
|
||||
floating: true
|
||||
dns_name: http://mycompany.com/
|
||||
|
||||
@@ -274,7 +274,7 @@ class DataTypeTest(TestCase):
|
||||
error = self.assertRaises(ValueError, data.validate)
|
||||
self.assertEqual(_('"1" is not a string.'), error.__str__())
|
||||
|
||||
# contact_pone is an invalid field name in nested datatype
|
||||
# 'contact_pone' is an invalid attribute name in nested datatype below
|
||||
def test_validation_in_nested_datatype(self):
|
||||
value_snippet = '''
|
||||
name: Mike
|
||||
|
||||
@@ -248,6 +248,23 @@ class PropertyTest(TestCase):
|
||||
'value must be one of "%s".') % valid_values)
|
||||
self.assertEqual(expected_message, str(error))
|
||||
|
||||
def test_invalid_property_status(self):
|
||||
tpl_snippet = '''
|
||||
properties:
|
||||
prop:
|
||||
type: string
|
||||
status: unknown
|
||||
'''
|
||||
schema = yamlparser.simple_parse(tpl_snippet)
|
||||
error = self.assertRaises(exception.InvalidSchemaError, PropertyDef,
|
||||
'prop', None, schema['properties']['prop'])
|
||||
|
||||
valid_values = ', '.join(PropertyDef.VALID_STATUS_VALUES)
|
||||
expected_message = (_('Schema definition of "prop" has "status" '
|
||||
'attribute with invalid value "unknown". The '
|
||||
'value must be one of "%s".') % valid_values)
|
||||
self.assertEqual(expected_message, str(error))
|
||||
|
||||
def test_capability_proprety_inheritance(self):
|
||||
tosca_custom_def_example1 = '''
|
||||
tosca.capabilities.ScalableNew:
|
||||
|
||||
@@ -1149,34 +1149,6 @@ custom_types/wordpress.yaml
|
||||
'are "%s".') % valid_versions))
|
||||
|
||||
def test_node_template_capabilities_properties(self):
|
||||
tpl_snippet = '''
|
||||
node_templates:
|
||||
server:
|
||||
type: tosca.nodes.Compute
|
||||
capabilities:
|
||||
host:
|
||||
properties:
|
||||
disk_size: 10 GB
|
||||
num_cpus: { get_input: cpus }
|
||||
mem_size: 4096 MB
|
||||
os:
|
||||
properties:
|
||||
architecture: x86_64
|
||||
type: Linux
|
||||
distribution: Fedora
|
||||
version: 18.0
|
||||
scalable:
|
||||
properties:
|
||||
min_instances: 1
|
||||
default_instances: 5
|
||||
'''
|
||||
expectedmessage = _('"properties" of template "server" is missing '
|
||||
'required field "[\'max_instances\']".')
|
||||
err = self.assertRaises(
|
||||
exception.MissingRequiredFieldError,
|
||||
lambda: self._single_node_template_content_test(tpl_snippet))
|
||||
self.assertEqual(expectedmessage, err.__str__())
|
||||
|
||||
# validating capability property values
|
||||
tpl_snippet = '''
|
||||
node_templates:
|
||||
|
||||
Reference in New Issue
Block a user