Merge "Add driver filter and evaluator for scheduler"

This commit is contained in:
Jenkins 2015-01-07 07:58:22 +00:00 committed by Gerrit Code Review
commit ebc819cc52
10 changed files with 1177 additions and 0 deletions

View File

@ -633,6 +633,10 @@ class ExtendVolumeError(CinderException):
message = _("Error extending volume: %(reason)s")
class EvaluatorParseException(Exception):
message = _("Error during evaluator parsing: %(reason)s")
# Driver specific exceptions
# Coraid
class CoraidException(VolumeDriverException):

View File

View File

@ -0,0 +1,297 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# 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 operator
import re
import pyparsing
import six
from cinder import exception
from cinder.i18n import _
def _operatorOperands(tokenList):
it = iter(tokenList)
while 1:
try:
op1 = next(it)
op2 = next(it)
yield(op1, op2)
except StopIteration:
break
class EvalConstant(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
result = self.value
if (isinstance(result, six.string_types) and
re.match("^[a-zA-Z_]+\.[a-zA-Z_]+$", result)):
(which_dict, entry) = result.split('.')
try:
result = _vars[which_dict][entry]
except KeyError as e:
msg = _("KeyError: %s") % e
raise exception.EvaluatorParseException(msg)
except TypeError as e:
msg = _("TypeError: %s") % e
raise exception.EvaluatorParseException(msg)
try:
result = int(result)
except ValueError:
try:
result = float(result)
except ValueError as e:
msg = _("ValueError: %s") % e
raise exception.EvaluatorParseException(msg)
return result
class EvalSignOp(object):
operations = {
'+': 1,
'-': -1,
}
def __init__(self, toks):
self.sign, self.value = toks[0]
def eval(self):
return self.operations[self.sign] * self.value.eval()
class EvalAddOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
sum = self.value[0].eval()
for op, val in _operatorOperands(self.value[1:]):
if op == '+':
sum += val.eval()
elif op == '-':
sum -= val.eval()
return sum
class EvalMultOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
prod = self.value[0].eval()
for op, val in _operatorOperands(self.value[1:]):
try:
if op == '*':
prod *= val.eval()
elif op == '/':
prod /= float(val.eval())
except ZeroDivisionError as e:
msg = _("ZeroDivisionError: %s") % e
raise exception.EvaluatorParseException(msg)
return prod
class EvalPowerOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
prod = self.value[0].eval()
for op, val in _operatorOperands(self.value[1:]):
prod = pow(prod, val.eval())
return prod
class EvalNegateOp(object):
def __init__(self, toks):
self.negation, self.value = toks[0]
def eval(self):
return not self.value.eval()
class EvalComparisonOp(object):
operations = {
"<": operator.lt,
"<=": operator.le,
">": operator.gt,
">=": operator.ge,
"!=": operator.ne,
"==": operator.eq,
"<>": operator.ne,
}
def __init__(self, toks):
self.value = toks[0]
def eval(self):
val1 = self.value[0].eval()
for op, val in _operatorOperands(self.value[1:]):
fn = self.operations[op]
val2 = val.eval()
if not fn(val1, val2):
break
val1 = val2
else:
return True
return False
class EvalTernaryOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
condition = self.value[0].eval()
if condition:
return self.value[2].eval()
else:
return self.value[4].eval()
class EvalFunction(object):
functions = {
"abs": abs,
"max": max,
"min": min,
}
def __init__(self, toks):
self.func, self.value = toks[0]
def eval(self):
args = self.value.eval()
if type(args) is list:
return self.functions[self.func](*args)
else:
return self.functions[self.func](args)
class EvalCommaSeperator(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
val1 = self.value[0].eval()
val2 = self.value[2].eval()
if type(val2) is list:
val_list = []
val_list.append(val1)
for val in val2:
val_list.append(val)
return val_list
return [val1, val2]
class EvalBoolAndOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
left = self.value[0].eval()
right = self.value[2].eval()
return left and right
class EvalBoolOrOp(object):
def __init__(self, toks):
self.value = toks[0]
def eval(self):
left = self.value[0].eval()
right = self.value[2].eval()
return left or right
_parser = None
_vars = {}
def _def_parser():
# Enabling packrat parsing greatly speeds up the parsing.
pyparsing.ParserElement.enablePackrat()
alphas = pyparsing.alphas
Combine = pyparsing.Combine
Forward = pyparsing.Forward
nums = pyparsing.nums
oneOf = pyparsing.oneOf
opAssoc = pyparsing.opAssoc
operatorPrecedence = pyparsing.operatorPrecedence
Word = pyparsing.Word
integer = Word(nums)
real = Combine(Word(nums) + '.' + Word(nums))
variable = Word(alphas + '_' + '.')
number = real | integer
expr = Forward()
fn = Word(alphas + '_' + '.')
operand = number | variable | fn
signop = oneOf('+ -')
addop = oneOf('+ -')
multop = oneOf('* /')
comparisonop = oneOf(' '.join(EvalComparisonOp.operations.keys()))
ternaryop = ('?', ':')
boolandop = oneOf('AND and &&')
boolorop = oneOf('OR or ||')
negateop = oneOf('NOT not !')
operand.setParseAction(EvalConstant)
expr = operatorPrecedence(operand, [
(fn, 1, opAssoc.RIGHT, EvalFunction),
("^", 2, opAssoc.RIGHT, EvalPowerOp),
(signop, 1, opAssoc.RIGHT, EvalSignOp),
(multop, 2, opAssoc.LEFT, EvalMultOp),
(addop, 2, opAssoc.LEFT, EvalAddOp),
(negateop, 1, opAssoc.RIGHT, EvalNegateOp),
(comparisonop, 2, opAssoc.LEFT, EvalComparisonOp),
(ternaryop, 3, opAssoc.LEFT, EvalTernaryOp),
(boolandop, 2, opAssoc.LEFT, EvalBoolAndOp),
(boolorop, 2, opAssoc.LEFT, EvalBoolOrOp),
(',', 2, opAssoc.RIGHT, EvalCommaSeperator), ])
return expr
def evaluate(expression, **kwargs):
"""Evaluates an expression.
Provides the facility to evaluate mathematical expressions, and to
substitute variables from dictionaries into those expressions.
Supports both integer and floating point values, and automatic
promotion where necessary.
"""
global _parser
if _parser is None:
_parser = _def_parser()
global _vars
_vars = kwargs
try:
result = _parser.parseString(expression, parseAll=True)[0]
except pyparsing.ParseException as e:
msg = _("ParseException: %s") % e
raise exception.EvaluatorParseException(msg)
return result.eval()

View File

@ -0,0 +1,145 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# 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 six
from cinder.i18n import _LW
from cinder.openstack.common import log as logging
from cinder.openstack.common.scheduler import filters
from cinder.scheduler.evaluator import evaluator
LOG = logging.getLogger(__name__)
class DriverFilter(filters.BaseHostFilter):
"""DriverFilter filters hosts based on a 'filter function' and metrics.
DriverFilter filters based on volume host's provided 'filter function'
and metrics.
"""
def host_passes(self, host_state, filter_properties):
"""Determines whether a host has a passing filter_function or not."""
stats = self._generate_stats(host_state, filter_properties)
LOG.debug("Checking host '%s'", stats['host_stats']['host'])
result = self._check_filter_function(stats)
LOG.debug("Result: %s", result)
LOG.debug("Done checking host '%s'", stats['host_stats']['host'])
return result
def _check_filter_function(self, stats):
"""Checks if a volume passes a host's filter function.
Returns a tuple in the format (filter_passing, filter_invalid).
Both values are booleans.
"""
host_stats = stats['host_stats']
extra_specs = stats['extra_specs']
# Check that the volume types match
if (extra_specs is None or 'volume_backend_name' not in extra_specs):
LOG.warning(_LW("No 'volume_backend_name' key in extra_specs. "
"Skipping volume backend name check."))
elif (extra_specs['volume_backend_name'] !=
host_stats['volume_backend_name']):
LOG.warning(_LW("Volume backend names do not match: '%(target)s' "
"vs '%(current)s' :: Skipping"),
{'target': extra_specs['volume_backend_name'],
'current': host_stats['volume_backend_name']})
return False
if stats['filter_function'] is None:
LOG.warning(_LW("Filter function not set :: passing host"))
return True
try:
filter_result = self._run_evaluator(stats['filter_function'],
stats)
except Exception as ex:
# Warn the admin for now that there is an error in the
# filter function.
LOG.warning(_LW("Error in filtering function "
"'%(function)s' : '%(error)s' :: failing host"),
{'function': stats['filter_function'],
'error': ex, })
return False
return filter_result
def _run_evaluator(self, func, stats):
"""Evaluates a given function using the provided available stats."""
host_stats = stats['host_stats']
host_caps = stats['host_caps']
extra_specs = stats['extra_specs']
qos_specs = stats['qos_specs']
volume_stats = stats['volume_stats']
result = evaluator.evaluate(
func,
extra=extra_specs,
stats=host_stats,
capabilities=host_caps,
volume=volume_stats,
qos=qos_specs)
return result
def _generate_stats(self, host_state, filter_properties):
"""Generates statistics from host and volume data."""
host_stats = {
'host': host_state.host,
'volume_backend_name': host_state.volume_backend_name,
'vendor_name': host_state.vendor_name,
'driver_version': host_state.driver_version,
'storage_protocol': host_state.storage_protocol,
'QoS_support': host_state.QoS_support,
'total_capacity_gb': host_state.total_capacity_gb,
'allocated_capacity_gb': host_state.allocated_capacity_gb,
'free_capacity_gb': host_state.free_capacity_gb,
'reserved_percentage': host_state.reserved_percentage,
'updated': host_state.updated,
}
host_caps = host_state.capabilities
filter_function = None
if ('filter_function' in host_caps and
host_caps['filter_function'] is not None):
filter_function = six.text_type(host_caps['filter_function'])
qos_specs = filter_properties.get('qos_specs', {})
volume_type = filter_properties.get('volume_type', {})
extra_specs = volume_type.get('extra_specs', {})
request_spec = filter_properties.get('request_spec', {})
volume_stats = request_spec.get('volume_properties', {})
stats = {
'host_stats': host_stats,
'host_caps': host_caps,
'extra_specs': extra_specs,
'qos_specs': qos_specs,
'volume_stats': volume_stats,
'volume_type': volume_type,
'filter_function': filter_function,
}
return stats

View File

@ -0,0 +1,143 @@
# Copyright (C) 2014 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 six
from cinder.i18n import _LW
from cinder.openstack.common import log as logging
from cinder.openstack.common.scheduler import weights
from cinder.scheduler.evaluator import evaluator
LOG = logging.getLogger(__name__)
class GoodnessWeigher(weights.BaseHostWeigher):
"""Goodness Weigher. Assign weights based on a host's goodness function.
Goodness rating is the following:
0 -- host is a poor choice
...
50 -- host is a good choice
...
100 -- host is a perfect choice
"""
def _weigh_object(self, host_state, weight_properties):
"""Determine host's goodness rating based on a goodness_function."""
stats = self._generate_stats(host_state, weight_properties)
LOG.debug("Checking host '%s'", stats['host_stats']['host'])
result = self._check_goodness_function(stats)
LOG.debug("Goodness: %s", result)
LOG.debug("Done checking host '%s'", stats['host_stats']['host'])
return result
def _check_goodness_function(self, stats):
"""Gets a host's goodness rating based on its goodness function."""
goodness_rating = 0
if stats['goodness_function'] is None:
LOG.warning(_LW("Goodness function not set :: defaulting to "
"minimal goodness rating of 0"))
else:
try:
goodness_result = self._run_evaluator(
stats['goodness_function'],
stats)
except Exception as ex:
LOG.warning(_LW("Error in goodness_function function "
"'%(function)s' : '%(error)s' :: Defaulting "
"to a goodness of 0"),
{'function': stats['goodness_function'],
'error': ex, })
return goodness_rating
if type(goodness_result) is bool:
if goodness_result:
goodness_rating = 100
elif goodness_result < 0 or goodness_result > 100:
LOG.warning(_LW("Invalid goodness result. Result must be "
"between 0 and 100. Result generated: '%s' "
":: Defaulting to a goodness of 0"),
goodness_result)
else:
goodness_rating = goodness_result
return goodness_rating
def _run_evaluator(self, func, stats):
"""Evaluates a given function using the provided available stats."""
host_stats = stats['host_stats']
host_caps = stats['host_caps']
extra_specs = stats['extra_specs']
qos_specs = stats['qos_specs']
volume_stats = stats['volume_stats']
result = evaluator.evaluate(
func,
extra=extra_specs,
stats=host_stats,
capabilities=host_caps,
volume=volume_stats,
qos=qos_specs)
return result
def _generate_stats(self, host_state, weight_properties):
"""Generates statistics from host and volume data."""
host_stats = {
'host': host_state.host,
'volume_backend_name': host_state.volume_backend_name,
'vendor_name': host_state.vendor_name,
'driver_version': host_state.driver_version,
'storage_protocol': host_state.storage_protocol,
'QoS_support': host_state.QoS_support,
'total_capacity_gb': host_state.total_capacity_gb,
'allocated_capacity_gb': host_state.allocated_capacity_gb,
'free_capacity_gb': host_state.free_capacity_gb,
'reserved_percentage': host_state.reserved_percentage,
'updated': host_state.updated,
}
host_caps = host_state.capabilities
goodness_function = None
if ('goodness_function' in host_caps and
host_caps['goodness_function'] is not None):
goodness_function = six.text_type(host_caps['goodness_function'])
qos_specs = weight_properties.get('qos_specs', {})
volume_type = weight_properties.get('volume_type', {})
extra_specs = volume_type.get('extra_specs', {})
request_spec = weight_properties.get('request_spec', {})
volume_stats = request_spec.get('volume_properties', {})
stats = {
'host_stats': host_stats,
'host_caps': host_caps,
'extra_specs': extra_specs,
'qos_specs': qos_specs,
'volume_stats': volume_stats,
'volume_type': volume_type,
'goodness_function': goodness_function,
}
return stats

View File

@ -0,0 +1,185 @@
# Copyright (C) 2014 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.
"""
Tests For Goodness Weigher.
"""
from cinder.scheduler.weights.goodness import GoodnessWeigher
from cinder import test
from cinder.tests.scheduler import fakes
class GoodnessWeigherTestCase(test.TestCase):
def setUp(self):
super(GoodnessWeigherTestCase, self).setUp()
def test_goodness_weigher_passing_host(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '100'
}
})
host_state_2 = fakes.FakeHostState('host2', {
'host': 'host2.example.com',
'capabilities': {
'goodness_function': '0'
}
})
host_state_3 = fakes.FakeHostState('host3', {
'host': 'host3.example.com',
'capabilities': {
'goodness_function': '100 / 2'
}
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(100, weight)
weight = weigher._weigh_object(host_state_2, weight_properties)
self.assertEqual(0, weight)
weight = weigher._weigh_object(host_state_3, weight_properties)
self.assertEqual(50, weight)
def test_goodness_weigher_capabilities_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'foo': 50,
'goodness_function': '10 + capabilities.foo'
}
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(60, weight)
def test_goodness_weigher_extra_specs_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '10 + extra.foo'
}
})
weight_properties = {
'volume_type': {
'extra_specs': {
'foo': 50
}
}
}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(60, weight)
def test_goodness_weigher_volume_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '10 + volume.foo'
}
})
weight_properties = {
'request_spec': {
'volume_properties': {
'foo': 50
}
}
}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(60, weight)
def test_goodness_weigher_qos_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '10 + qos.foo'
}
})
weight_properties = {
'qos_specs': {
'foo': 50
}
}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(60, weight)
def test_goodness_weigher_stats_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': 'stats.free_capacity_gb > 20'
},
'free_capacity_gb': 50
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(100, weight)
def test_goodness_weigher_invalid_substitution(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '10 + stats.my_val'
},
'foo': 50
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(0, weight)
def test_goodness_weigher_host_rating_out_of_bounds(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '-10'
}
})
host_state_2 = fakes.FakeHostState('host2', {
'host': 'host2.example.com',
'capabilities': {
'goodness_function': '200'
}
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(0, weight)
weight = weigher._weigh_object(host_state_2, weight_properties)
self.assertEqual(0, weight)
def test_goodness_weigher_invalid_goodness_function(self):
weigher = GoodnessWeigher()
host_state = fakes.FakeHostState('host1', {
'host': 'host.example.com',
'capabilities': {
'goodness_function': '50 / 0'
}
})
weight_properties = {}
weight = weigher._weigh_object(host_state, weight_properties)
self.assertEqual(0, weight)

View File

@ -317,3 +317,267 @@ class HostFiltersTestCase(test.TestCase):
'same_host': "NOT-a-valid-UUID", }}
self.assertFalse(filt_cls.host_passes(host, filter_properties))
def test_driver_filter_passing_function(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': '1 == 1',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_failing_function(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': '1 == 2',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertFalse(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_no_filter_function(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': None,
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_not_implemented(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_no_volume_extra_specs(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': '1 == 1',
}
})
filter_properties = {'volume_type': {}}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_volume_backend_name_different(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': '1 == 1',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake2',
}
}
}
self.assertFalse(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_extra_spec_replacement(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': 'extra.var == 1',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
'var': 1,
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_stats_replacement(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'total_capacity_gb': 100,
'capabilities': {
'filter_function': 'stats.total_capacity_gb < 200',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_volume_replacement(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': 'volume.size < 5',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
},
'request_spec': {
'volume_properties': {
'size': 1
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_qos_spec_replacement(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': 'qos.var == 1',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
},
'qos_specs': {
'var': 1
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_exception_caught(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': '1 / 0 == 0',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertFalse(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_function_empty_qos(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'filter_function': 'qos.maxiops == 1',
}
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
},
'qos_specs': None
}
self.assertFalse(filt_cls.host_passes(host1, filter_properties))
def test_driver_filter_capabilities(self):
filt_cls = self.class_map['DriverFilter']()
host1 = fakes.FakeHostState(
'host1', {
'volume_backend_name': 'fake',
'capabilities': {
'foo': 10,
'filter_function': 'capabilities.foo == 10',
},
})
filter_properties = {
'volume_type': {
'extra_specs': {
'volume_backend_name': 'fake',
}
}
}
self.assertTrue(filt_cls.host_passes(host1, filter_properties))

View File

@ -0,0 +1,136 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# 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 cinder import exception
from cinder.scheduler.evaluator.evaluator import evaluate
from cinder import test
class EvaluatorTestCase(test.TestCase):
def test_simple_integer(self):
self.assertEqual(2, evaluate("1+1"))
self.assertEqual(9, evaluate("2+3+4"))
self.assertEqual(23, evaluate("11+12"))
self.assertEqual(30, evaluate("5*6"))
self.assertEqual(2, evaluate("22/11"))
self.assertEqual(38, evaluate("109-71"))
self.assertEqual(493, evaluate("872 - 453 + 44 / 22 * 4 + 66"))
def test_simple_float(self):
self.assertEqual(2.0, evaluate("1.0 + 1.0"))
self.assertEqual(2.5, evaluate("1.5 + 1.0"))
self.assertEqual(3.0, evaluate("1.5 * 2.0"))
def test_int_float_mix(self):
self.assertEqual(2.5, evaluate("1.5 + 1"))
self.assertEqual(4.25, evaluate("8.5 / 2"))
self.assertEqual(5.25, evaluate("10/4+0.75 + 2"))
def test_negative_numbers(self):
self.assertEqual(-2, evaluate("-2"))
self.assertEqual(-1, evaluate("-2+1"))
self.assertEqual(3, evaluate("5+-2"))
def test_exponent(self):
self.assertEqual(8, evaluate("2^3"))
self.assertEqual(-8, evaluate("-2 ^ 3"))
self.assertEqual(15.625, evaluate("2.5 ^ 3"))
self.assertEqual(8, evaluate("4 ^ 1.5"))
def test_function(self):
self.assertEqual(5, evaluate("abs(-5)"))
self.assertEqual(2, evaluate("abs(2)"))
self.assertEqual(1, evaluate("min(1, 100)"))
self.assertEqual(100, evaluate("max(1, 100)"))
def test_parentheses(self):
self.assertEqual(1, evaluate("(1)"))
self.assertEqual(-1, evaluate("(-1)"))
self.assertEqual(2, evaluate("(1+1)"))
self.assertEqual(15, evaluate("(1+2) * 5"))
self.assertEqual(3, evaluate("(1+2)*(3-1)/((1+(2-1)))"))
self.assertEqual(-8.0, evaluate("((1.0 / 0.5) * (2)) *(-2)"))
def test_comparisons(self):
self.assertEqual(True, evaluate("1 < 2"))
self.assertEqual(True, evaluate("2 > 1"))
self.assertEqual(True, evaluate("2 != 1"))
self.assertEqual(False, evaluate("1 > 2"))
self.assertEqual(False, evaluate("2 < 1"))
self.assertEqual(False, evaluate("2 == 1"))
self.assertEqual(True, evaluate("(1 == 1) == !(1 == 2)"))
def test_logic_ops(self):
self.assertEqual(True, evaluate("(1 == 1) AND (2 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) and (2 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) && (2 == 2)"))
self.assertEqual(False, evaluate("(1 == 1) && (5 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) OR (5 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) or (5 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) || (5 == 2)"))
self.assertEqual(False, evaluate("(5 == 1) || (5 == 2)"))
self.assertEqual(False, evaluate("(1 == 1) AND NOT (2 == 2)"))
self.assertEqual(False, evaluate("(1 == 1) AND not (2 == 2)"))
self.assertEqual(False, evaluate("(1 == 1) AND !(2 == 2)"))
self.assertEqual(True, evaluate("(1 == 1) AND NOT (5 == 2)"))
self.assertEqual(True,
evaluate("(1 == 1) OR NOT (2 == 2) AND (5 == 5)"))
def test_ternary_conditional(self):
self.assertEqual(5, evaluate("(1 < 2) ? 5 : 10"))
self.assertEqual(10, evaluate("(1 > 2) ? 5 : 10"))
def test_variables_dict(self):
stats = {'iops': 1000, 'usage': 0.65, 'count': 503, 'free_space': 407}
request = {'iops': 500, 'size': 4}
self.assertEqual(1500, evaluate("stats.iops + request.iops",
stats=stats, request=request))
def test_missing_var(self):
stats = {'iops': 1000, 'usage': 0.65, 'count': 503, 'free_space': 407}
request = {'iops': 500, 'size': 4}
self.assertRaises(exception.EvaluatorParseException,
evaluate,
"foo.bob + 5",
stats=stats, request=request)
self.assertRaises(exception.EvaluatorParseException,
evaluate,
"stats.bob + 5",
stats=stats, request=request)
self.assertRaises(exception.EvaluatorParseException,
evaluate,
"fake.var + 1",
stats=stats, request=request, fake=None)
def test_bad_expression(self):
self.assertRaises(exception.EvaluatorParseException,
evaluate,
"1/*1")
def test_nonnumber_comparison(self):
nonnumber = {'test': 'foo'}
request = {'test': 'bar'}
self.assertRaises(
exception.EvaluatorParseException,
evaluate,
"nonnumber.test != request.test",
nonnumber=nonnumber, request=request)
def test_div_zero(self):
self.assertRaises(exception.EvaluatorParseException,
evaluate,
"7 / 0")

View File

@ -26,6 +26,7 @@ paramiko>=1.13.0
Paste
PasteDeploy>=1.5.0
pycrypto>=2.6
pyparsing>=2.0.1
python-barbicanclient>=2.1.0,!=3.0.0
python-glanceclient>=0.15.0
python-novaclient>=2.18.0

View File

@ -31,6 +31,7 @@ cinder.scheduler.filters =
CapabilitiesFilter = cinder.openstack.common.scheduler.filters.capabilities_filter:CapabilitiesFilter
CapacityFilter = cinder.scheduler.filters.capacity_filter:CapacityFilter
DifferentBackendFilter = cinder.scheduler.filters.affinity_filter:DifferentBackendFilter
DriverFilter = cinder.scheduler.filters.driver_filter:DriverFilter
JsonFilter = cinder.openstack.common.scheduler.filters.json_filter:JsonFilter
RetryFilter = cinder.openstack.common.scheduler.filters.ignore_attempted_hosts_filter:IgnoreAttemptedHostsFilter
SameBackendFilter = cinder.scheduler.filters.affinity_filter:SameBackendFilter
@ -38,6 +39,7 @@ cinder.scheduler.weights =
AllocatedCapacityWeigher = cinder.scheduler.weights.capacity:AllocatedCapacityWeigher
CapacityWeigher = cinder.scheduler.weights.capacity:CapacityWeigher
ChanceWeigher = cinder.scheduler.weights.chance:ChanceWeigher
GoodnessWeigher = cinder.scheduler.weights.goodness:GoodnessWeigher
VolumeNumberWeigher = cinder.scheduler.weights.volume_number:VolumeNumberWeigher
console_scripts =
cinder-all = cinder.cmd.all:main