Add scalar unit type

Implement scalar-unit.size type:

1. any number of spaces (including zero or none) is allowed between the
scalar value and the unit value.
2. support constraints like greater_or_equal and in_range. (eg:
storage_size: in_range [4 GB, 20 GB ] )
3. default unit is 'B'(byte) according to the TOSCA profile while
allowing user to set the unit of the output number as long as the input
unit is a valid one
4. supported scalar-unit.size (case-insensitive): B, KB, KiB, MB, MiB,
GB, GiB, TB, TiB

implement BP: add-scalar-unit-type

Change-Id: Idf3705312afd33d7aeadc3f8856cf728c41ec190
This commit is contained in:
Haiyang DING 2015-01-27 01:36:16 +00:00
parent 509360110b
commit e39f712e51
7 changed files with 274 additions and 224 deletions

View File

@ -113,6 +113,8 @@ class DataEntity(object):
if entry_schema:
DataEntity.validate_entry(value, entry_schema, custom_def)
return value
elif type == Schema.SCALAR_UNIT_SIZE:
return Constraint.validate_scalar_unit_size(value)
elif type == Schema.MAP:
Constraint.validate_map(value)
if entry_schema:

View File

@ -38,21 +38,19 @@ tosca.nodes.Compute:
Number of (actual or virtual) CPUs associated with the Compute node.
disk_size:
required: no
type: integer
type: scalar-unit.size
constraints:
- greater_or_equal: 0
- greater_or_equal: 0 MB
description: >
Size of the local disk, in Gigabytes (GB), available to applications
running on the Compute node.
Size of the local disk, available to applications running on the
Compute node.
mem_size:
required: no
type: integer
type: scalar-unit.size
constraints:
- greater_or_equal: 0
- greater_or_equal: 0 MB
description: >
Size of memory, in Megabytes (MB), available to applications running
on the Compute node.
default: 1024
Size of memory, available to applications running on the Compute node.
os_arch:
required: no
default: x86_64

View File

@ -12,6 +12,7 @@
import collections
import datetime
import math
import numbers
import re
import six
@ -34,12 +35,18 @@ class Schema(collections.Mapping):
PROPERTY_TYPES = (
INTEGER, STRING, BOOLEAN, FLOAT,
NUMBER, TIMESTAMP, LIST, MAP
NUMBER, TIMESTAMP, LIST, MAP, SCALAR_UNIT_SIZE
) = (
'integer', 'string', 'boolean', 'float',
'number', 'timestamp', 'list', 'map'
'number', 'timestamp', 'list', 'map', 'scalar-unit.size'
)
SCALAR_UNIT_SIZE_DEFAULT = 'B'
SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000,
'MIB': 1048576, 'GB': 1000000000,
'GIB': 1073741824, 'TB': 1000000000000,
'TIB': 1099511627776}
def __init__(self, name, schema_dict):
self.name = name
if not isinstance(schema_dict, collections.Mapping):
@ -135,6 +142,16 @@ class Constraint(object):
self.property_name = property_name
self.property_type = property_type
self.constraint_value = constraint[self.constraint_key]
self.constraint_value_msg = self.constraint_value
if self.property_type == Schema.SCALAR_UNIT_SIZE:
if isinstance(self.constraint_value, list):
self.constraint_value = [Constraint.
get_num_from_scalar_unit_size(v)
for v in self.constraint_value]
else:
self.constraint_value = (Constraint.
get_num_from_scalar_unit_size
(self.constraint_value))
# check if constraint is valid for property type
if property_type not in self.valid_prop_types:
msg = _('Constraint type "%(ctype)s" is not valid '
@ -147,6 +164,9 @@ class Constraint(object):
return _('Property %s could not be validated.') % self.property_name
def validate(self, value):
self.value_msg = value
if self.property_type == Schema.SCALAR_UNIT_SIZE:
value = self.get_num_from_scalar_unit_size(value)
if not self._is_valid(value):
err_msg = self._err_msg(value)
raise ValidationError(message=err_msg)
@ -190,6 +210,36 @@ class Constraint(object):
return normalised == 'true'
raise ValueError(_('"%s" is not a boolean') % value)
@staticmethod
def validate_scalar_unit_size(value):
regex = re.compile('(\d*)\s*(\w*)')
result = regex.match(str(value)).groups()
if result[0] and ((not result[1]) or (result[1].upper() in
Schema.SCALAR_UNIT_SIZE_DICT.
keys())):
return value
raise ValueError(_('"%s" is not a valid scalar-unit') % value)
@staticmethod
def get_num_from_scalar_unit_size(value, unit=None):
if unit:
if unit.upper() not in Schema.SCALAR_UNIT_SIZE_DICT.keys():
raise ValueError(_('input unit "%s" is not a valid unit')
% unit)
else:
unit = Schema.SCALAR_UNIT_SIZE_DEFAULT
Constraint.validate_scalar_unit_size(value)
regex = re.compile('(\d*)\s*(\w*)')
result = regex.match(str(value)).groups()
if not result[1]:
converted = (Constraint.str_to_num(result[0]))
if result[1].upper() in Schema.SCALAR_UNIT_SIZE_DICT.keys():
converted = int(Constraint.str_to_num(result[0])
* Schema.SCALAR_UNIT_SIZE_DICT[result[1].upper()]
* math.pow(Schema.SCALAR_UNIT_SIZE_DICT
[unit.upper()], -1))
return converted
@staticmethod
def str_to_num(value):
'''Convert a string representation of a number into a numeric type.'''
@ -221,8 +271,8 @@ class Equal(Constraint):
def _err_msg(self, value):
return (_('%(pname)s: %(pvalue)s is not equal to "%(cvalue)s".') %
dict(pname=self.property_name,
pvalue=value,
cvalue=self.constraint_value))
pvalue=self.value_msg,
cvalue=self.constraint_value_msg))
class GreaterThan(Constraint):
@ -238,7 +288,7 @@ class GreaterThan(Constraint):
datetime.time, datetime.datetime)
valid_prop_types = (Schema.INTEGER, Schema.FLOAT,
Schema.TIMESTAMP)
Schema.TIMESTAMP, Schema.SCALAR_UNIT_SIZE)
def __init__(self, property_name, property_type, constraint):
super(GreaterThan, self).__init__(property_name, property_type,
@ -256,8 +306,8 @@ class GreaterThan(Constraint):
def _err_msg(self, value):
return (_('%(pname)s: %(pvalue)s must be greater than "%(cvalue)s".') %
dict(pname=self.property_name,
pvalue=value,
cvalue=self.constraint_value))
pvalue=self.value_msg,
cvalue=self.constraint_value_msg))
class GreaterOrEqual(Constraint):
@ -273,7 +323,7 @@ class GreaterOrEqual(Constraint):
datetime.time, datetime.datetime)
valid_prop_types = (Schema.INTEGER, Schema.FLOAT,
Schema.TIMESTAMP)
Schema.TIMESTAMP, Schema.SCALAR_UNIT_SIZE)
def __init__(self, property_name, property_type, constraint):
super(GreaterOrEqual, self).__init__(property_name, property_type,
@ -291,8 +341,8 @@ class GreaterOrEqual(Constraint):
return (_('%(pname)s: %(pvalue)s must be greater or equal '
'to "%(cvalue)s".') %
dict(pname=self.property_name,
pvalue=value,
cvalue=self.constraint_value))
pvalue=self.value_msg,
cvalue=self.constraint_value_msg))
class LessThan(Constraint):
@ -308,7 +358,7 @@ class LessThan(Constraint):
datetime.time, datetime.datetime)
valid_prop_types = (Schema.INTEGER, Schema.FLOAT,
Schema.TIMESTAMP)
Schema.TIMESTAMP, Schema.SCALAR_UNIT_SIZE)
def __init__(self, property_name, property_type, constraint):
super(LessThan, self).__init__(property_name, property_type,
@ -326,8 +376,8 @@ class LessThan(Constraint):
def _err_msg(self, value):
return (_('%(pname)s: %(pvalue)s must be less than "%(cvalue)s".') %
dict(pname=self.property_name,
pvalue=value,
cvalue=self.constraint_value))
pvalue=self.value_msg,
cvalue=self.constraint_value_msg))
class LessOrEqual(Constraint):
@ -343,7 +393,7 @@ class LessOrEqual(Constraint):
datetime.time, datetime.datetime)
valid_prop_types = (Schema.INTEGER, Schema.FLOAT,
Schema.TIMESTAMP)
Schema.TIMESTAMP, Schema.SCALAR_UNIT_SIZE)
def __init__(self, property_name, property_type, constraint):
super(LessOrEqual, self).__init__(property_name, property_type,
@ -362,8 +412,8 @@ class LessOrEqual(Constraint):
return (_('%(pname)s: %(pvalue)s must be less or '
'equal to "%(cvalue)s".') %
dict(pname=self.property_name,
pvalue=value,
cvalue=self.constraint_value))
pvalue=self.value_msg,
cvalue=self.constraint_value_msg))
class InRange(Constraint):
@ -379,7 +429,7 @@ class InRange(Constraint):
datetime.time, datetime.datetime)
valid_prop_types = (Schema.INTEGER, Schema.FLOAT,
Schema.TIMESTAMP)
Schema.TIMESTAMP, Schema.SCALAR_UNIT_SIZE)
def __init__(self, property_name, property_type, constraint):
super(InRange, self).__init__(property_name, property_type, constraint)
@ -407,9 +457,9 @@ class InRange(Constraint):
return (_('%(pname)s: %(pvalue)s is out of range '
'(min:%(vmin)s, max:%(vmax)s).') %
dict(pname=self.property_name,
pvalue=value,
vmin=self.min,
vmax=self.max))
pvalue=self.value_msg,
vmin=self.constraint_value_msg[0],
vmax=self.constraint_value_msg[1]))
class ValidValues(Constraint):

View File

@ -10,9 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
from translator.toscalib.common.exception import InvalidPropertyValueError
from translator.toscalib.dataentity import DataEntity
from translator.toscalib.elements.constraints import Schema
from translator.toscalib.functions import is_function
@ -35,7 +32,7 @@ class Property(object):
def __init__(self, property_name, value, schema_dict, custom_def=None):
self.name = property_name
self.value = self._convert_value(value)
self.value = value
self.custom_def = custom_def
self.schema = Schema(property_name, schema_dict)
@ -76,20 +73,3 @@ class Property(object):
if self.constraints:
for constraint in self.constraints:
constraint.validate(self.value)
def _convert_value(self, value):
if self.name == 'mem_size':
mem_reader = re.compile('(\d*)\s*(\w*)')
matcher = str(value)
result = mem_reader.match(matcher).groups()
r = []
if (result[0] != '') and (result[1] == ''):
r = int(result[0])
elif (result[0] != '') and (result[1] == 'MB'):
r = int(result[0])
elif (result[0] != '') and (result[1] == 'GB'):
r = int(result[0]) * 1024
else:
raise InvalidPropertyValueError(what=self.name)
return r
return value

View File

@ -1,167 +0,0 @@
# 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 InvalidPropertyValueError
from translator.toscalib.nodetemplate import NodeTemplate
from translator.toscalib.tests.base import TestCase
import translator.toscalib.utils.yamlparser
class ToscaTemplateMemorySizeOutputTest(TestCase):
scenarios = [
(
# tpl_snippet with mem_size given as number
'mem_size_is_number',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1024
# host image properties
os_type: Linux
''',
expected=1024)
),
(
# tpl_snippet with mem_size given as number+space+MB
'mem_size_is_number_Space_MB',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1024 MB
# host image properties
os_type: Linux
''',
expected=1024)
),
(
# tpl_snippet with mem_size given as number+space+GB
'mem_size_is_number_Space_GB',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1 GB
# host image properties
os_type: Linux
''',
expected=1024)
),
(
# tpl_snippet with mem_size given as number+GB
'mem_size_is_number_NoSpace_GB',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1GB
# host image properties
os_type: Linux
''',
expected=1024)
),
(
# tpl_snippet with mem_size given as number+Spaces+GB
'mem_size_is_number_Spaces_GB',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1 GB
# host image properties
os_type: Linux
''',
expected=1024)
),
(
# tpl_snippet with no mem_size given
'mem_size_is_absent',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
# host image properties
os_type: Linux
''',
expected=1024)
),
]
def test_scenario_mem_size(self):
tpl = self.tpl_snippet
nodetemplates = (translator.toscalib.utils.yamlparser.
simple_parse(tpl))['node_templates']
name = list(nodetemplates.keys())[0]
nodetemplate = NodeTemplate(name, nodetemplates)
for p in nodetemplate.properties:
if p.name == 'mem_size':
resolved = p.value
self.assertEqual(resolved, self.expected)
class ToscaTemplateMemorySizeErrorTest(TestCase):
scenarios = [
(
# tpl_snippet with mem_size given as empty (error)
'mem_size_is_empty',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size:
# host image properties
os_type: Linux
''',
err=InvalidPropertyValueError)
),
(
# tpl_snippet with mem_size given as number+InvalidUnit (error)
'mem_size_unit_is_invalid',
dict(tpl_snippet='''
node_templates:
server:
type: tosca.nodes.Compute
properties:
# compute properties (flavor)
mem_size: 1 QB
# host image properties
os_type: Linux
''',
err=InvalidPropertyValueError)
),
]
def test_scenario_mem_size_error(self):
tpl = self.tpl_snippet
nodetemplates = (translator.toscalib.utils.yamlparser.
simple_parse(tpl))['node_templates']
name = list(nodetemplates.keys())[0]
nodetemplate = NodeTemplate(name, nodetemplates)
self.assertRaises(self.err,
nodetemplate._create_properties)

View File

@ -0,0 +1,194 @@
# 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 ValidationError
from translator.toscalib.elements.constraints import Constraint
from translator.toscalib.nodetemplate import NodeTemplate
from translator.toscalib.tests.base import TestCase
from translator.toscalib.utils import yamlparser
class ScalarUnitPositiveTest(TestCase):
scenarios = [
(
# tpl_snippet with mem_size given as number
'mem_size_is_number',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1024
''',
expected=1024)
),
(
# tpl_snippet with mem_size given as number+space+MB
'mem_size_is_number_Space_MB',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1024 MB
''',
expected='1024 MB')
),
(
# tpl_snippet with mem_size given as number+spaces+GB
'mem_size_is_number_Space_GB',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1 GB
''',
expected='1 GB')
),
(
# tpl_snippet with mem_size given as number+tiB
'mem_size_is_number_NoSpace_GB',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1tiB
''',
expected='1tiB')
),
(
# tpl_snippet with mem_size given as number+Spaces+GIB
'mem_size_is_number_Spaces_GB',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1 GIB
''',
expected='1 GIB')
),
(
# tpl_snippet with mem_size given as number+Space+tib
'mem_size_is_number_Spaces_GB',
dict(tpl_snippet='''
server:
type: tosca.nodes.Compute
properties:
mem_size: 1 tib
''',
expected='1 tib')
),
]
def test_scenario_scalar_unit_positive(self):
tpl = self.tpl_snippet
nodetemplates = yamlparser.simple_parse(tpl)
nodetemplate = NodeTemplate('server', nodetemplates)
for p in nodetemplate.properties:
if p.name == 'mem_size':
self.assertIsNone(p.validate())
resolved = p.value
self.assertEqual(resolved, self.expected)
class GetNumFromScalarUnitSizePositive(TestCase):
scenarios = [
( # Note that (1 TB) / (1 GB) = 1000
'Input is TB, user input is GB',
dict(InputMemSize='1 TB',
UserInputUnit='gB',
expected=1000)
),
( # Note that (1 Tib)/ (1 GB) = 1099
'Input is TiB, user input is GB',
dict(InputMemSize='1 TiB',
UserInputUnit='gB',
expected=1099)
),
]
def test_scenario_get_num_from_scalar_unit_size(self):
resolved = Constraint.get_num_from_scalar_unit_size(self.InputMemSize,
self.UserInputUnit)
self.assertEqual(resolved, self.expected)
class GetNumFromScalarUnitSizeNegative(TestCase):
InputMemSize = '1 GB'
UserInputUnit = 'qB'
def test_get_num_from_scalar_unit_size_negative(self):
try:
Constraint.get_num_from_scalar_unit_size(self.InputMemSize,
self.UserInputUnit)
except Exception as error:
self.assertTrue(isinstance(error, ValueError))
self.assertEqual('input unit "qB" is not a valid unit',
error.__str__())
class ScalarUnitNegativeTest(TestCase):
custom_def_snippet = '''
tosca.my.nodes.Compute:
derived_from: tosca.nodes.Root
properties:
disk_size:
required: no
type: scalar-unit.size
constraints:
- greater_or_equal: 1 GB
mem_size:
required: no
type: scalar-unit.size
constraints:
- in_range: [1 MiB, 1 GiB]
'''
custom_def = yamlparser.simple_parse(custom_def_snippet)
# disk_size doesn't provide a value, mem_size uses an invalid unit.
def test_invalid_scalar_unit(self):
tpl_snippet = '''
server:
type: tosca.my.nodes.Compute
properties:
disk_size: MB
mem_size: 1 QB
'''
nodetemplates = yamlparser.simple_parse(tpl_snippet)
nodetemplate = NodeTemplate('server', nodetemplates, self.custom_def)
for p in nodetemplate.properties:
self.assertRaises(ValueError, p.validate)
# disk_size is less than 1 GB, mem_size is not in the required range.
# Note: in the spec, the minimum value of mem_size is 1 MiB (> 1 MB)
def test_constraint_for_scalar_unit(self):
tpl_snippet = '''
server:
type: tosca.my.nodes.Compute
properties:
disk_size: 500 MB
mem_size: 1 MB
'''
nodetemplates = yamlparser.simple_parse(tpl_snippet)
nodetemplate = NodeTemplate('server', nodetemplates, self.custom_def)
for p in nodetemplate.properties:
if p.name == 'disk_size':
error = self.assertRaises(ValidationError, p.validate)
self.assertEqual('disk_size: 500 MB must be greater or '
'equal to "1 GB".', error.__str__())
if p.name == 'mem_size':
error = self.assertRaises(ValidationError, p.validate)
self.assertEqual('mem_size: 1 MB is out of range '
'(min:1 MiB, '
'max:1 GiB).', error.__str__())

View File

@ -64,10 +64,3 @@ class ToscaDefTest(TestCase):
self.assertEqual(compute_type.interfaces, None)
root_node = NodeType('tosca.nodes.Root')
self.assertIn('tosca.interfaces.node.Lifecycle', root_node.interfaces)
def test_default_mem_size(self):
test_value = 0
for p_def in compute_type.properties_def:
if p_def.name == 'mem_size':
test_value = p_def.default
self.assertEqual(test_value, 1024)