Add DriverFilter and GoodnessWeigher to manila
This patch ports cinder's DriverFilter and GoodnessWeigher to manila. These can use two new properties provided by backends, 'filter_function' and 'goodness_function', which can be used to filter and weigh qualified backends, respectively. Reference for cinder spec: I59b607a88953a346aa35e67e785a0417a7ce8cc9 Reference for cinder commit: I38408ab49b6ed869c1faae746ee64a3bae86be58 DocImpact Change-Id: I873f4152e16efdeb30ceae26335a7974dc9b4b69 Implements: blueprint driver-filter-goodness-weigher
This commit is contained in:
parent
5b11b87dc7
commit
f10776d832
@ -782,3 +782,7 @@ class TegileAPIException(ShareBackendException):
|
|||||||
|
|
||||||
class StorageCommunicationException(ShareBackendException):
|
class StorageCommunicationException(ShareBackendException):
|
||||||
message = _("Could not communicate with storage array.")
|
message = _("Could not communicate with storage array.")
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluatorParseException(ManilaException):
|
||||||
|
message = _("Error during evaluator parsing: %(reason)s")
|
||||||
|
0
manila/scheduler/evaluator/__init__.py
Normal file
0
manila/scheduler/evaluator/__init__.py
Normal file
297
manila/scheduler/evaluator/evaluator.py
Normal file
297
manila/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 manila import exception
|
||||||
|
from manila.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") % six.text_type(e)
|
||||||
|
raise exception.EvaluatorParseException(reason=msg)
|
||||||
|
except TypeError as e:
|
||||||
|
msg = _("TypeError: %s") % six.text_type(e)
|
||||||
|
raise exception.EvaluatorParseException(reason=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = int(result)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
result = float(result)
|
||||||
|
except ValueError as e:
|
||||||
|
msg = _("ValueError: %s") % six.text_type(e)
|
||||||
|
raise exception.EvaluatorParseException(reason=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") % six.text_type(e)
|
||||||
|
raise exception.EvaluatorParseException(reason=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") % six.text_type(e)
|
||||||
|
raise exception.EvaluatorParseException(reason=msg)
|
||||||
|
|
||||||
|
return result.eval()
|
123
manila/scheduler/filters/driver.py
Normal file
123
manila/scheduler/filters/driver.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# 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 oslo_log import log as logging
|
||||||
|
|
||||||
|
from manila.i18n import _LI
|
||||||
|
from manila.i18n import _LW
|
||||||
|
from manila.scheduler.evaluator import evaluator
|
||||||
|
from manila.scheduler.filters import base_host
|
||||||
|
from manila.scheduler import utils
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DriverFilter(base_host.BaseHostFilter):
|
||||||
|
"""DriverFilter filters hosts based on a 'filter function' and metrics.
|
||||||
|
|
||||||
|
DriverFilter filters based on share 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("Driver Filter: 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 share 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 share types match
|
||||||
|
if extra_specs is None or 'share_backend_name' not in extra_specs:
|
||||||
|
LOG.warning(_LW("No 'share_backend_name' key in extra_specs. "
|
||||||
|
"Skipping share backend name check."))
|
||||||
|
elif (extra_specs['share_backend_name'] !=
|
||||||
|
host_stats['share_backend_name']):
|
||||||
|
LOG.warning(_LW("Share backend names do not match: '%(target)s' "
|
||||||
|
"vs '%(current)s' :: Skipping."),
|
||||||
|
{'target': extra_specs['share_backend_name'],
|
||||||
|
'current': host_stats['share_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
|
||||||
|
|
||||||
|
msg = _LI("Filter function result for host %(host)s: %(result)s.")
|
||||||
|
args = {'host': stats['host_stats']['host'],
|
||||||
|
'result': six.text_type(filter_result)}
|
||||||
|
LOG.info(msg, args)
|
||||||
|
|
||||||
|
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']
|
||||||
|
share_stats = stats['share_stats']
|
||||||
|
|
||||||
|
result = evaluator.evaluate(
|
||||||
|
func,
|
||||||
|
extra=extra_specs,
|
||||||
|
stats=host_stats,
|
||||||
|
capabilities=host_caps,
|
||||||
|
share=share_stats)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _generate_stats(self, host_state, filter_properties):
|
||||||
|
"""Generates statistics from host and share data."""
|
||||||
|
|
||||||
|
filter_function = None
|
||||||
|
|
||||||
|
if ('filter_function' in host_state.capabilities and
|
||||||
|
host_state.capabilities['filter_function'] is not None):
|
||||||
|
filter_function = six.text_type(
|
||||||
|
host_state.capabilities['filter_function'])
|
||||||
|
|
||||||
|
stats = utils.generate_stats(host_state, filter_properties)
|
||||||
|
|
||||||
|
stats['filter_function'] = filter_function
|
||||||
|
|
||||||
|
return stats
|
@ -46,13 +46,15 @@ host_manager_opts = [
|
|||||||
'CapacityFilter',
|
'CapacityFilter',
|
||||||
'CapabilitiesFilter',
|
'CapabilitiesFilter',
|
||||||
'ConsistencyGroupFilter',
|
'ConsistencyGroupFilter',
|
||||||
|
'DriverFilter',
|
||||||
'ShareReplicationFilter',
|
'ShareReplicationFilter',
|
||||||
],
|
],
|
||||||
help='Which filter class names to use for filtering hosts '
|
help='Which filter class names to use for filtering hosts '
|
||||||
'when not specified in the request.'),
|
'when not specified in the request.'),
|
||||||
cfg.ListOpt('scheduler_default_weighers',
|
cfg.ListOpt('scheduler_default_weighers',
|
||||||
default=[
|
default=[
|
||||||
'CapacityWeigher'
|
'CapacityWeigher',
|
||||||
|
'GoodnessWeigher',
|
||||||
],
|
],
|
||||||
help='Which weigher class names to use for weighing hosts.')
|
help='Which weigher class names to use for weighing hosts.')
|
||||||
]
|
]
|
||||||
|
63
manila/scheduler/utils.py
Normal file
63
manila/scheduler/utils.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
def generate_stats(host_state, properties):
|
||||||
|
"""Generates statistics from host and share data."""
|
||||||
|
|
||||||
|
host_stats = {
|
||||||
|
'host': host_state.host,
|
||||||
|
'share_backend_name': host_state.share_backend_name,
|
||||||
|
'vendor_name': host_state.vendor_name,
|
||||||
|
'driver_version': host_state.driver_version,
|
||||||
|
'storage_protocol': host_state.storage_protocol,
|
||||||
|
'qos': host_state.qos,
|
||||||
|
'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,
|
||||||
|
'driver_handles_share_servers':
|
||||||
|
host_state.driver_handles_share_servers,
|
||||||
|
'thin_provisioning': host_state.thin_provisioning,
|
||||||
|
'updated': host_state.updated,
|
||||||
|
'consistency_group_support': host_state.consistency_group_support,
|
||||||
|
'dedupe': host_state.dedupe,
|
||||||
|
'compression': host_state.compression,
|
||||||
|
'snapshot_support': host_state.snapshot_support,
|
||||||
|
'replication_domain': host_state.replication_domain,
|
||||||
|
'replication_type': host_state.replication_type,
|
||||||
|
'provisioned_capacity_gb': host_state.provisioned_capacity_gb,
|
||||||
|
'pools': host_state.pools,
|
||||||
|
'max_over_subscription_ratio':
|
||||||
|
host_state.max_over_subscription_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
host_caps = host_state.capabilities
|
||||||
|
|
||||||
|
share_type = properties.get('share_type', {})
|
||||||
|
extra_specs = share_type.get('extra_specs', {})
|
||||||
|
|
||||||
|
request_spec = properties.get('request_spec', {})
|
||||||
|
share_stats = request_spec.get('resource_properties', {})
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'host_stats': host_stats,
|
||||||
|
'host_caps': host_caps,
|
||||||
|
'extra_specs': extra_specs,
|
||||||
|
'share_stats': share_stats,
|
||||||
|
'share_type': share_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
125
manila/scheduler/weighers/goodness.py
Normal file
125
manila/scheduler/weighers/goodness.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# 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 oslo_log import log as logging
|
||||||
|
import six
|
||||||
|
|
||||||
|
from manila.i18n import _LI
|
||||||
|
from manila.i18n import _LW
|
||||||
|
from manila.scheduler.evaluator import evaluator
|
||||||
|
from manila.scheduler import utils
|
||||||
|
from manila.scheduler.weighers import base_host
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GoodnessWeigher(base_host.BaseHostWeigher):
|
||||||
|
"""Goodness Weigher. Assign weights based on a host's goodness function.
|
||||||
|
|
||||||
|
Goodness rating is the following:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
msg = _LI("Goodness function result for host %(host)s: %(result)s.")
|
||||||
|
args = {'host': stats['host_stats']['host'],
|
||||||
|
'result': six.text_type(goodness_rating)}
|
||||||
|
LOG.info(msg, args)
|
||||||
|
|
||||||
|
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']
|
||||||
|
share_stats = stats['share_stats']
|
||||||
|
|
||||||
|
result = evaluator.evaluate(
|
||||||
|
func,
|
||||||
|
extra=extra_specs,
|
||||||
|
stats=host_stats,
|
||||||
|
capabilities=host_caps,
|
||||||
|
share=share_stats)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _generate_stats(self, host_state, weight_properties):
|
||||||
|
"""Generates statistics from host and share data."""
|
||||||
|
|
||||||
|
goodness_function = None
|
||||||
|
|
||||||
|
if ('goodness_function' in host_state.capabilities and
|
||||||
|
host_state.capabilities['goodness_function'] is not None):
|
||||||
|
goodness_function = six.text_type(
|
||||||
|
host_state.capabilities['goodness_function'])
|
||||||
|
|
||||||
|
stats = utils.generate_stats(host_state, weight_properties)
|
||||||
|
|
||||||
|
stats['goodness_function'] = goodness_function
|
||||||
|
|
||||||
|
return stats
|
@ -112,6 +112,14 @@ share_opts = [
|
|||||||
"replication between each other. If this option is not "
|
"replication between each other. If this option is not "
|
||||||
"specified in the group, it means that replication is not "
|
"specified in the group, it means that replication is not "
|
||||||
"enabled on the backend."),
|
"enabled on the backend."),
|
||||||
|
cfg.StrOpt('filter_function',
|
||||||
|
default=None,
|
||||||
|
help='String representation for an equation that will be '
|
||||||
|
'used to filter hosts.'),
|
||||||
|
cfg.StrOpt('goodness_function',
|
||||||
|
default=None,
|
||||||
|
help='String representation for an equation that will be '
|
||||||
|
'used to determine the goodness of a host.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
ssh_opts = [
|
ssh_opts = [
|
||||||
@ -808,6 +816,8 @@ class ShareDriver(object):
|
|||||||
pools=self.pools or None,
|
pools=self.pools or None,
|
||||||
snapshot_support=self.snapshots_are_supported,
|
snapshot_support=self.snapshots_are_supported,
|
||||||
replication_domain=self.replication_domain,
|
replication_domain=self.replication_domain,
|
||||||
|
filter_function=self.get_filter_function(),
|
||||||
|
goodness_function=self.get_goodness_function(),
|
||||||
)
|
)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
common.update(data)
|
common.update(data)
|
||||||
@ -1841,3 +1851,57 @@ class ShareDriver(object):
|
|||||||
backend and their status was 'deleting'.
|
backend and their status was 'deleting'.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_filter_function(self):
|
||||||
|
"""Get filter_function string.
|
||||||
|
|
||||||
|
Returns either the string from the driver instance or global section
|
||||||
|
in manila.conf. If nothing is specified in manila.conf, then try to
|
||||||
|
find the default filter_function. When None is returned the scheduler
|
||||||
|
will always pass the driver instance.
|
||||||
|
|
||||||
|
:return a filter_function string or None
|
||||||
|
"""
|
||||||
|
ret_function = self.configuration.filter_function
|
||||||
|
if not ret_function:
|
||||||
|
ret_function = CONF.filter_function
|
||||||
|
if not ret_function:
|
||||||
|
ret_function = self.get_default_filter_function()
|
||||||
|
return ret_function
|
||||||
|
|
||||||
|
def get_goodness_function(self):
|
||||||
|
"""Get good_function string.
|
||||||
|
|
||||||
|
Returns either the string from the driver instance or global section
|
||||||
|
in manila.conf. If nothing is specified in manila.conf, then try to
|
||||||
|
find the default goodness_function. When None is returned the scheduler
|
||||||
|
will give the lowest score to the driver instance.
|
||||||
|
|
||||||
|
:return a goodness_function string or None
|
||||||
|
"""
|
||||||
|
ret_function = self.configuration.goodness_function
|
||||||
|
if not ret_function:
|
||||||
|
ret_function = CONF.goodness_function
|
||||||
|
if not ret_function:
|
||||||
|
ret_function = self.get_default_goodness_function()
|
||||||
|
return ret_function
|
||||||
|
|
||||||
|
def get_default_filter_function(self):
|
||||||
|
"""Get the default filter_function string.
|
||||||
|
|
||||||
|
Each driver could overwrite the method to return a well-known
|
||||||
|
default string if it is available.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_default_goodness_function(self):
|
||||||
|
"""Get the default goodness_function string.
|
||||||
|
|
||||||
|
Each driver could overwrite the method to return a well-known
|
||||||
|
default string if it is available.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
0
manila/tests/scheduler/evaluator/__init__.py
Normal file
0
manila/tests/scheduler/evaluator/__init__.py
Normal file
140
manila/tests/scheduler/evaluator/test_evaluator.py
Normal file
140
manila/tests/scheduler/evaluator/test_evaluator.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# 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 manila import exception
|
||||||
|
from manila.scheduler.evaluator import evaluator
|
||||||
|
from manila import test
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluatorTestCase(test.TestCase):
|
||||||
|
def test_simple_integer(self):
|
||||||
|
self.assertEqual(2, evaluator.evaluate("1+1"))
|
||||||
|
self.assertEqual(9, evaluator.evaluate("2+3+4"))
|
||||||
|
self.assertEqual(23, evaluator.evaluate("11+12"))
|
||||||
|
self.assertEqual(30, evaluator.evaluate("5*6"))
|
||||||
|
self.assertEqual(2, evaluator.evaluate("22/11"))
|
||||||
|
self.assertEqual(38, evaluator.evaluate("109-71"))
|
||||||
|
self.assertEqual(
|
||||||
|
493, evaluator.evaluate("872 - 453 + 44 / 22 * 4 + 66"))
|
||||||
|
|
||||||
|
def test_simple_float(self):
|
||||||
|
self.assertEqual(2.0, evaluator.evaluate("1.0 + 1.0"))
|
||||||
|
self.assertEqual(2.5, evaluator.evaluate("1.5 + 1.0"))
|
||||||
|
self.assertEqual(3.0, evaluator.evaluate("1.5 * 2.0"))
|
||||||
|
|
||||||
|
def test_int_float_mix(self):
|
||||||
|
self.assertEqual(2.5, evaluator.evaluate("1.5 + 1"))
|
||||||
|
self.assertEqual(4.25, evaluator.evaluate("8.5 / 2"))
|
||||||
|
self.assertEqual(5.25, evaluator.evaluate("10/4+0.75 + 2"))
|
||||||
|
|
||||||
|
def test_negative_numbers(self):
|
||||||
|
self.assertEqual(-2, evaluator.evaluate("-2"))
|
||||||
|
self.assertEqual(-1, evaluator.evaluate("-2+1"))
|
||||||
|
self.assertEqual(3, evaluator.evaluate("5+-2"))
|
||||||
|
|
||||||
|
def test_exponent(self):
|
||||||
|
self.assertEqual(8, evaluator.evaluate("2^3"))
|
||||||
|
self.assertEqual(-8, evaluator.evaluate("-2 ^ 3"))
|
||||||
|
self.assertEqual(15.625, evaluator.evaluate("2.5 ^ 3"))
|
||||||
|
self.assertEqual(8, evaluator.evaluate("4 ^ 1.5"))
|
||||||
|
|
||||||
|
def test_function(self):
|
||||||
|
self.assertEqual(5, evaluator.evaluate("abs(-5)"))
|
||||||
|
self.assertEqual(2, evaluator.evaluate("abs(2)"))
|
||||||
|
self.assertEqual(1, evaluator.evaluate("min(1, 100)"))
|
||||||
|
self.assertEqual(100, evaluator.evaluate("max(1, 100)"))
|
||||||
|
self.assertEqual(100, evaluator.evaluate("max(1, 2, 100)"))
|
||||||
|
|
||||||
|
def test_parentheses(self):
|
||||||
|
self.assertEqual(1, evaluator.evaluate("(1)"))
|
||||||
|
self.assertEqual(-1, evaluator.evaluate("(-1)"))
|
||||||
|
self.assertEqual(2, evaluator.evaluate("(1+1)"))
|
||||||
|
self.assertEqual(15, evaluator.evaluate("(1+2) * 5"))
|
||||||
|
self.assertEqual(3, evaluator.evaluate("(1+2)*(3-1)/((1+(2-1)))"))
|
||||||
|
self.assertEqual(
|
||||||
|
-8.0, evaluator. evaluate("((1.0 / 0.5) * (2)) *(-2)"))
|
||||||
|
|
||||||
|
def test_comparisons(self):
|
||||||
|
self.assertTrue(evaluator.evaluate("1 < 2"))
|
||||||
|
self.assertTrue(evaluator.evaluate("2 > 1"))
|
||||||
|
self.assertTrue(evaluator.evaluate("2 != 1"))
|
||||||
|
self.assertFalse(evaluator.evaluate("1 > 2"))
|
||||||
|
self.assertFalse(evaluator.evaluate("2 < 1"))
|
||||||
|
self.assertFalse(evaluator.evaluate("2 == 1"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) == !(1 == 2)"))
|
||||||
|
|
||||||
|
def test_logic_ops(self):
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) AND (2 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) and (2 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) && (2 == 2)"))
|
||||||
|
self.assertFalse(evaluator.evaluate("(1 == 1) && (5 == 2)"))
|
||||||
|
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) OR (5 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) or (5 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) || (5 == 2)"))
|
||||||
|
self.assertFalse(evaluator.evaluate("(5 == 1) || (5 == 2)"))
|
||||||
|
|
||||||
|
self.assertFalse(evaluator.evaluate("(1 == 1) AND NOT (2 == 2)"))
|
||||||
|
self.assertFalse(evaluator.evaluate("(1 == 1) AND not (2 == 2)"))
|
||||||
|
self.assertFalse(evaluator.evaluate("(1 == 1) AND !(2 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) AND NOT (5 == 2)"))
|
||||||
|
self.assertTrue(evaluator.evaluate("(1 == 1) OR NOT (2 == 2) "
|
||||||
|
"AND (5 == 5)"))
|
||||||
|
|
||||||
|
def test_ternary_conditional(self):
|
||||||
|
self.assertEqual(5, evaluator.evaluate("(1 < 2) ? 5 : 10"))
|
||||||
|
self.assertEqual(10, evaluator.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, evaluator.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,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"foo.bob + 5",
|
||||||
|
stats=stats, request=request)
|
||||||
|
self.assertRaises(exception.EvaluatorParseException,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"stats.bob + 5",
|
||||||
|
stats=stats, request=request)
|
||||||
|
self.assertRaises(exception.EvaluatorParseException,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"fake.var + 1",
|
||||||
|
stats=stats, request=request, fake=None)
|
||||||
|
|
||||||
|
def test_bad_expression(self):
|
||||||
|
self.assertRaises(exception.EvaluatorParseException,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"1/*1")
|
||||||
|
|
||||||
|
def test_nonnumber_comparison(self):
|
||||||
|
nonnumber = {'test': 'foo'}
|
||||||
|
request = {'test': 'bar'}
|
||||||
|
self.assertRaises(
|
||||||
|
exception.EvaluatorParseException,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"nonnumber.test != request.test",
|
||||||
|
nonnumber=nonnumber, request=request)
|
||||||
|
|
||||||
|
def test_div_zero(self):
|
||||||
|
self.assertRaises(exception.EvaluatorParseException,
|
||||||
|
evaluator.evaluate,
|
||||||
|
"7 / 0")
|
191
manila/tests/scheduler/filters/test_driver.py
Normal file
191
manila/tests/scheduler/filters/test_driver.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# 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 oslo_context import context
|
||||||
|
|
||||||
|
from manila.scheduler.filters import driver
|
||||||
|
from manila import test
|
||||||
|
from manila.tests.scheduler import fakes
|
||||||
|
|
||||||
|
|
||||||
|
class HostFiltersTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(HostFiltersTestCase, self).setUp()
|
||||||
|
self.context = context.RequestContext('fake', 'fake')
|
||||||
|
self.filter = driver.DriverFilter()
|
||||||
|
|
||||||
|
def test_passing_function(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': '1 == 1',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_failing_function(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': '1 == 2',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertFalse(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_no_filter_function(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_not_implemented(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_no_share_extra_specs(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': '1 == 1',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_extra_specs_wrong_backend(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': '1 == 1',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {
|
||||||
|
'share_type': {
|
||||||
|
'extra_specs': {
|
||||||
|
'share_backend_name': 'foo',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertFalse(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_function_extra_spec_replacement(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': 'extra.var == 1',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {
|
||||||
|
'share_type': {
|
||||||
|
'extra_specs': {
|
||||||
|
'var': 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_function_stats_replacement(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'total_capacity_gb': 100,
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': 'stats.total_capacity_gb < 200',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {'share_type': {}}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_function_share_replacement(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': 'share.size < 5',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {
|
||||||
|
'request_spec': {
|
||||||
|
'resource_properties': {
|
||||||
|
'size': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_function_exception_caught(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'filter_function': '1 / 0 == 0',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {}
|
||||||
|
|
||||||
|
self.assertFalse(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_capabilities(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'foo': 10,
|
||||||
|
'filter_function': 'capabilities.foo == 10',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {}
|
||||||
|
|
||||||
|
self.assertTrue(self.filter.host_passes(host1, filter_properties))
|
||||||
|
|
||||||
|
def test_wrong_capabilities(self):
|
||||||
|
host1 = fakes.FakeHostState(
|
||||||
|
'host1', {
|
||||||
|
'capabilities': {
|
||||||
|
'bar': 10,
|
||||||
|
'filter_function': 'capabilities.foo == 10',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
filter_properties = {}
|
||||||
|
|
||||||
|
self.assertFalse(self.filter.host_passes(host1, filter_properties))
|
180
manila/tests/scheduler/weighers/test_goodness.py
Normal file
180
manila/tests/scheduler/weighers/test_goodness.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Tests For Goodness Weigher.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from manila.scheduler.weighers import goodness
|
||||||
|
from manila import test
|
||||||
|
from manila.tests.scheduler import fakes
|
||||||
|
|
||||||
|
|
||||||
|
class GoodnessWeigherTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def test_goodness_weigher_with_no_goodness_function(self):
|
||||||
|
weigher = goodness.GoodnessWeigher()
|
||||||
|
host_state = fakes.FakeHostState('host1', {
|
||||||
|
'host': 'host.example.com',
|
||||||
|
'capabilities': {
|
||||||
|
'foo': '50'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
weight_properties = {}
|
||||||
|
weight = weigher._weigh_object(host_state, weight_properties)
|
||||||
|
self.assertEqual(0, weight)
|
||||||
|
|
||||||
|
def test_goodness_weigher_passing_host(self):
|
||||||
|
weigher = goodness.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 = goodness.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 = goodness.GoodnessWeigher()
|
||||||
|
host_state = fakes.FakeHostState('host1', {
|
||||||
|
'host': 'host.example.com',
|
||||||
|
'capabilities': {
|
||||||
|
'goodness_function': '10 + extra.foo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
weight_properties = {
|
||||||
|
'share_type': {
|
||||||
|
'extra_specs': {
|
||||||
|
'foo': 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weight = weigher._weigh_object(host_state, weight_properties)
|
||||||
|
self.assertEqual(60, weight)
|
||||||
|
|
||||||
|
def test_goodness_weigher_share_substitution(self):
|
||||||
|
weigher = goodness.GoodnessWeigher()
|
||||||
|
host_state = fakes.FakeHostState('host1', {
|
||||||
|
'host': 'host.example.com',
|
||||||
|
'capabilities': {
|
||||||
|
'goodness_function': '10 + share.foo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
weight_properties = {
|
||||||
|
'request_spec': {
|
||||||
|
'resource_properties': {
|
||||||
|
'foo': 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weight = weigher._weigh_object(host_state, weight_properties)
|
||||||
|
self.assertEqual(60, weight)
|
||||||
|
|
||||||
|
def test_goodness_weigher_stats_substitution(self):
|
||||||
|
weigher = goodness.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 = goodness.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 = goodness.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 = goodness.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)
|
@ -125,6 +125,8 @@ class EMCShareFrameworkTestCase(test.TestCase):
|
|||||||
data['pools'] = None
|
data['pools'] = None
|
||||||
data['snapshot_support'] = True
|
data['snapshot_support'] = True
|
||||||
data['replication_domain'] = None
|
data['replication_domain'] = None
|
||||||
|
data['filter_function'] = None
|
||||||
|
data['goodness_function'] = None
|
||||||
self.assertEqual(data, self.driver._stats)
|
self.assertEqual(data, self.driver._stats)
|
||||||
|
|
||||||
def _fake_safe_get(self, value):
|
def _fake_safe_get(self, value):
|
||||||
|
@ -257,6 +257,8 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
|
|||||||
'pools': None,
|
'pools': None,
|
||||||
'snapshot_support': True,
|
'snapshot_support': True,
|
||||||
'replication_domain': None,
|
'replication_domain': None,
|
||||||
|
'filter_function': None,
|
||||||
|
'goodness_function': None,
|
||||||
}
|
}
|
||||||
self.assertEqual(test_data, self._driver._stats)
|
self.assertEqual(test_data, self._driver._stats)
|
||||||
|
|
||||||
|
@ -58,6 +58,8 @@ class HPE3ParDriverTestCase(test.TestCase):
|
|||||||
self.conf.network_config_group = 'test_network_config_group'
|
self.conf.network_config_group = 'test_network_config_group'
|
||||||
self.conf.admin_network_config_group = (
|
self.conf.admin_network_config_group = (
|
||||||
'test_admin_network_config_group')
|
'test_admin_network_config_group')
|
||||||
|
self.conf.filter_function = None
|
||||||
|
self.conf.goodness_function = None
|
||||||
|
|
||||||
def safe_get(attr):
|
def safe_get(attr):
|
||||||
try:
|
try:
|
||||||
@ -560,6 +562,8 @@ class HPE3ParDriverTestCase(test.TestCase):
|
|||||||
'vendor_name': 'HPE',
|
'vendor_name': 'HPE',
|
||||||
'pools': None,
|
'pools': None,
|
||||||
'replication_domain': None,
|
'replication_domain': None,
|
||||||
|
'filter_function': None,
|
||||||
|
'goodness_function': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
result = self.driver.get_share_stats(refresh=True)
|
result = self.driver.get_share_stats(refresh=True)
|
||||||
@ -618,6 +622,8 @@ class HPE3ParDriverTestCase(test.TestCase):
|
|||||||
'hp3par_flash_cache': False,
|
'hp3par_flash_cache': False,
|
||||||
'snapshot_support': True,
|
'snapshot_support': True,
|
||||||
'replication_domain': None,
|
'replication_domain': None,
|
||||||
|
'filter_function': None,
|
||||||
|
'goodness_function': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
result = self.driver.get_share_stats(refresh=True)
|
result = self.driver.get_share_stats(refresh=True)
|
||||||
@ -652,6 +658,8 @@ class HPE3ParDriverTestCase(test.TestCase):
|
|||||||
'vendor_name': 'HPE',
|
'vendor_name': 'HPE',
|
||||||
'snapshot_support': True,
|
'snapshot_support': True,
|
||||||
'replication_domain': None,
|
'replication_domain': None,
|
||||||
|
'filter_function': None,
|
||||||
|
'goodness_function': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
result = self.driver.get_share_stats(refresh=True)
|
result = self.driver.get_share_stats(refresh=True)
|
||||||
|
@ -746,6 +746,8 @@ class HuaweiShareDriverTestCase(test.TestCase):
|
|||||||
self.configuration.max_over_subscription_ratio = 1
|
self.configuration.max_over_subscription_ratio = 1
|
||||||
self.configuration.driver_handles_share_servers = False
|
self.configuration.driver_handles_share_servers = False
|
||||||
self.configuration.replication_domain = None
|
self.configuration.replication_domain = None
|
||||||
|
self.configuration.filter_function = None
|
||||||
|
self.configuration.goodness_function = None
|
||||||
|
|
||||||
self.tmp_dir = tempfile.mkdtemp()
|
self.tmp_dir = tempfile.mkdtemp()
|
||||||
self.fake_conf_file = self.tmp_dir + '/manila_huawei_conf.xml'
|
self.fake_conf_file = self.tmp_dir + '/manila_huawei_conf.xml'
|
||||||
@ -2189,6 +2191,8 @@ class HuaweiShareDriverTestCase(test.TestCase):
|
|||||||
expected['qos'] = True
|
expected['qos'] = True
|
||||||
expected["snapshot_support"] = True
|
expected["snapshot_support"] = True
|
||||||
expected['replication_domain'] = None
|
expected['replication_domain'] = None
|
||||||
|
expected['filter_function'] = None
|
||||||
|
expected['goodness_function'] = None
|
||||||
expected["pools"] = []
|
expected["pools"] = []
|
||||||
pool = dict(
|
pool = dict(
|
||||||
pool_name='OpenStack_Pool',
|
pool_name='OpenStack_Pool',
|
||||||
|
@ -59,6 +59,8 @@ class FakeConfig(object):
|
|||||||
"reserved_share_percentage", 0)
|
"reserved_share_percentage", 0)
|
||||||
self.max_over_subscription_ratio = kwargs.get(
|
self.max_over_subscription_ratio = kwargs.get(
|
||||||
"max_over_subscription_ratio", 15.0)
|
"max_over_subscription_ratio", 15.0)
|
||||||
|
self.filter_function = kwargs.get("filter_function", None)
|
||||||
|
self.goodness_function = kwargs.get("goodness_function", None)
|
||||||
|
|
||||||
def safe_get(self, key):
|
def safe_get(self, key):
|
||||||
return getattr(self, key)
|
return getattr(self, key)
|
||||||
@ -303,6 +305,8 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
|
|||||||
'storage_protocol': 'NFS',
|
'storage_protocol': 'NFS',
|
||||||
'total_capacity_gb': 'unknown',
|
'total_capacity_gb': 'unknown',
|
||||||
'vendor_name': 'Open Source',
|
'vendor_name': 'Open Source',
|
||||||
|
'filter_function': None,
|
||||||
|
'goodness_function': None,
|
||||||
}
|
}
|
||||||
if replication_domain:
|
if replication_domain:
|
||||||
expected['replication_type'] = 'readable'
|
expected['replication_type'] = 'readable'
|
||||||
|
10
releasenotes/notes/driver-filter-91e2c60c9d1a48dd.yaml
Normal file
10
releasenotes/notes/driver-filter-91e2c60c9d1a48dd.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add DriverFilter and GoodnessWeigher to manila's scheduler.
|
||||||
|
These can use two new properties provided by backends, 'filter_function'
|
||||||
|
and 'goodness_function', which can be used to filter and weigh qualified
|
||||||
|
backends, respectively.
|
||||||
|
upgrade:
|
||||||
|
- To add DriverFilter and GoodnessWeigher to an active deployment, their
|
||||||
|
references must be added to the filters and weighers sections on
|
||||||
|
entry_points.txt.
|
@ -27,6 +27,7 @@ oslo.concurrency>=3.8.0 # Apache-2.0
|
|||||||
paramiko>=2.0 # LGPLv2.1+
|
paramiko>=2.0 # LGPLv2.1+
|
||||||
Paste # MIT
|
Paste # MIT
|
||||||
PasteDeploy>=1.5.0 # MIT
|
PasteDeploy>=1.5.0 # MIT
|
||||||
|
pyparsing>=2.0.1 # MIT
|
||||||
python-neutronclient>=4.2.0 # Apache-2.0
|
python-neutronclient>=4.2.0 # Apache-2.0
|
||||||
keystoneauth1>=2.7.0 # Apache-2.0
|
keystoneauth1>=2.7.0 # Apache-2.0
|
||||||
keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
|
keystonemiddleware!=4.1.0,!=4.5.0,>=4.0.0 # Apache-2.0
|
||||||
|
@ -38,12 +38,14 @@ manila.scheduler.filters =
|
|||||||
CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter
|
CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter
|
||||||
CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter
|
CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter
|
||||||
ConsistencyGroupFilter = manila.scheduler.filters.consistency_group:ConsistencyGroupFilter
|
ConsistencyGroupFilter = manila.scheduler.filters.consistency_group:ConsistencyGroupFilter
|
||||||
|
DriverFilter = manila.scheduler.filters.driver:DriverFilter
|
||||||
IgnoreAttemptedHostsFilter = manila.scheduler.filters.ignore_attempted_hosts:IgnoreAttemptedHostsFilter
|
IgnoreAttemptedHostsFilter = manila.scheduler.filters.ignore_attempted_hosts:IgnoreAttemptedHostsFilter
|
||||||
JsonFilter = manila.scheduler.filters.json:JsonFilter
|
JsonFilter = manila.scheduler.filters.json:JsonFilter
|
||||||
RetryFilter = manila.scheduler.filters.retry:RetryFilter
|
RetryFilter = manila.scheduler.filters.retry:RetryFilter
|
||||||
ShareReplicationFilter = manila.scheduler.filters.share_replication:ShareReplicationFilter
|
ShareReplicationFilter = manila.scheduler.filters.share_replication:ShareReplicationFilter
|
||||||
manila.scheduler.weighers =
|
manila.scheduler.weighers =
|
||||||
CapacityWeigher = manila.scheduler.weighers.capacity:CapacityWeigher
|
CapacityWeigher = manila.scheduler.weighers.capacity:CapacityWeigher
|
||||||
|
GoodnessWeigher = manila.scheduler.weighers.goodness:GoodnessWeigher
|
||||||
PoolWeigher = manila.scheduler.weighers.pool:PoolWeigher
|
PoolWeigher = manila.scheduler.weighers.pool:PoolWeigher
|
||||||
# These are for backwards compat with Havana notification_driver configuration values
|
# These are for backwards compat with Havana notification_driver configuration values
|
||||||
oslo_messaging.notify.drivers =
|
oslo_messaging.notify.drivers =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user