message = _("Error extending volume: %(reason)s")
+class EvaluatorParseException(Exception):
+ message = _("Error during evaluator parsing: %(reason)s")
+
+
# Driver specific exceptions
# Coraid
class CoraidException(VolumeDriverException):
--- /dev/null
+# 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()
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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)
\ No newline at end of file
'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))
\ No newline at end of file
--- /dev/null
+# 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")
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
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
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