Add driver filter and evaluator for scheduler
This patch adds a new filter for the cinder scheduler that can interpret two new properties provided by backends, 'filter_function' and 'goodness_function'. A driver can rely on cinder.conf entries to define these properties for a backend or the driver can generate them some other way. An evaluator is used by the filter to parse the properties. The 'goodness_function' property is used to weigh qualified backends in case multiple ones pass the filter. More details can be found in the spec: https://review.openstack.org/#/c/129330/ Implements: blueprint filtering-weighing-with-driver-supplied-functions DocImpact: New optional backend properties in cinder.conf. New filter and weigher available for scheduler. Change-Id: I38408ab49b6ed869c1faae746ee64a3bae86be58
This commit is contained in:
parent
48eb05aa84
commit
59bf887ea9
@ -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):
|
||||
|
0
cinder/scheduler/evaluator/__init__.py
Normal file
0
cinder/scheduler/evaluator/__init__.py
Normal file
297
cinder/scheduler/evaluator/evaluator.py
Normal file
297
cinder/scheduler/evaluator/evaluator.py
Normal 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()
|
145
cinder/scheduler/filters/driver_filter.py
Normal file
145
cinder/scheduler/filters/driver_filter.py
Normal 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
|
143
cinder/scheduler/weights/goodness.py
Normal file
143
cinder/scheduler/weights/goodness.py
Normal 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
|
185
cinder/tests/scheduler/test_goodness_weigher.py
Normal file
185
cinder/tests/scheduler/test_goodness_weigher.py
Normal 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)
|
@ -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))
|
136
cinder/tests/test_evaluator.py
Normal file
136
cinder/tests/test_evaluator.py
Normal 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")
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user