Merge "Revert "Implement filter scheduler""
This commit is contained in:
commit
9e3a254f6d
cinder
exception.py
openstack-common.confsetup.pyopenstack/common/scheduler
scheduler
tests/scheduler
fakes.pytest_capacity_weigher.pytest_filter_scheduler.pytest_host_filters.pytest_host_manager.pytest_scheduler_options.py
volume
tools
@ -314,14 +314,6 @@ class HostNotFound(NotFound):
|
|||||||
message = _("Host %(host)s could not be found.")
|
message = _("Host %(host)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class SchedulerHostFilterNotFound(NotFound):
|
|
||||||
message = _("Scheduler Host Filter %(filter_name)s could not be found.")
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerHostWeigherNotFound(NotFound):
|
|
||||||
message = _("Scheduler Host Weigher %(weigher_name)s could not be found.")
|
|
||||||
|
|
||||||
|
|
||||||
class HostBinaryNotFound(NotFound):
|
class HostBinaryNotFound(NotFound):
|
||||||
message = _("Could not find binary %(binary)s on host %(host)s.")
|
message = _("Could not find binary %(binary)s on host %(host)s.")
|
||||||
|
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
# Copyright (c) 2011-2012 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Filter support
|
|
||||||
"""
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from stevedore import extension
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFilter(object):
|
|
||||||
"""Base class for all filter classes."""
|
|
||||||
def _filter_one(self, obj, filter_properties):
|
|
||||||
"""Return True if it passes the filter, False otherwise.
|
|
||||||
Override this in a subclass.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def filter_all(self, filter_obj_list, filter_properties):
|
|
||||||
"""Yield objects that pass the filter.
|
|
||||||
|
|
||||||
Can be overriden in a subclass, if you need to base filtering
|
|
||||||
decisions on all objects. Otherwise, one can just override
|
|
||||||
_filter_one() to filter a single object.
|
|
||||||
"""
|
|
||||||
for obj in filter_obj_list:
|
|
||||||
if self._filter_one(obj, filter_properties):
|
|
||||||
yield obj
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterHandler(object):
|
|
||||||
""" Base class to handle loading filter classes.
|
|
||||||
|
|
||||||
This class should be subclassed where one needs to use filters.
|
|
||||||
"""
|
|
||||||
def __init__(self, filter_class_type, filter_namespace):
|
|
||||||
self.namespace = filter_namespace
|
|
||||||
self.filter_class_type = filter_class_type
|
|
||||||
self.filter_manager = extension.ExtensionManager(filter_namespace)
|
|
||||||
|
|
||||||
def _is_correct_class(self, obj):
|
|
||||||
"""Return whether an object is a class of the correct type and
|
|
||||||
is not prefixed with an underscore.
|
|
||||||
"""
|
|
||||||
return (inspect.isclass(obj) and
|
|
||||||
not obj.__name__.startswith('_') and
|
|
||||||
issubclass(obj, self.filter_class_type))
|
|
||||||
|
|
||||||
def get_all_classes(self):
|
|
||||||
return [x.plugin for x in self.filter_manager
|
|
||||||
if self._is_correct_class(x.plugin)]
|
|
||||||
|
|
||||||
def get_filtered_objects(self, filter_classes, objs,
|
|
||||||
filter_properties):
|
|
||||||
for filter_cls in filter_classes:
|
|
||||||
objs = filter_cls().filter_all(objs, filter_properties)
|
|
||||||
return list(objs)
|
|
@ -1,41 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Scheduler host filters
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cinder.openstack.common import log as logging
|
|
||||||
from cinder.openstack.common.scheduler import filter
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseHostFilter(filter.BaseFilter):
|
|
||||||
"""Base class for host filters."""
|
|
||||||
def _filter_one(self, obj, filter_properties):
|
|
||||||
"""Return True if the object passes the filter, otherwise False."""
|
|
||||||
return self.host_passes(obj, filter_properties)
|
|
||||||
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
"""Return True if the HostState passes the filter, otherwise False.
|
|
||||||
Override this in a subclass.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class HostFilterHandler(filter.BaseFilterHandler):
|
|
||||||
def __init__(self, namespace):
|
|
||||||
super(HostFilterHandler, self).__init__(BaseHostFilter, namespace)
|
|
@ -1,30 +0,0 @@
|
|||||||
# Copyright (c) 2011-2012 OpenStack, LLC.
|
|
||||||
# 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.openstack.common.scheduler import filters
|
|
||||||
|
|
||||||
|
|
||||||
class AvailabilityZoneFilter(filters.BaseHostFilter):
|
|
||||||
"""Filters Hosts by availability zone."""
|
|
||||||
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
spec = filter_properties.get('request_spec', {})
|
|
||||||
props = spec.get('resource_properties', [])
|
|
||||||
availability_zone = props.get('availability_zone')
|
|
||||||
|
|
||||||
if availability_zone:
|
|
||||||
return availability_zone == host_state.service['availability_zone']
|
|
||||||
return True
|
|
@ -1,63 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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.openstack.common import log as logging
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
from cinder.openstack.common.scheduler.filters import extra_specs_ops
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CapabilitiesFilter(filters.BaseHostFilter):
|
|
||||||
"""HostFilter to work with resource (instance & volume) type records."""
|
|
||||||
|
|
||||||
def _satisfies_extra_specs(self, capabilities, resource_type):
|
|
||||||
"""Check that the capabilities provided by the services
|
|
||||||
satisfy the extra specs associated with the instance type"""
|
|
||||||
extra_specs = resource_type.get('extra_specs', [])
|
|
||||||
if not extra_specs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for key, req in extra_specs.iteritems():
|
|
||||||
# Either not scope format, or in capabilities scope
|
|
||||||
scope = key.split(':')
|
|
||||||
if len(scope) > 1 and scope[0] != "capabilities":
|
|
||||||
continue
|
|
||||||
elif scope[0] == "capabilities":
|
|
||||||
del scope[0]
|
|
||||||
|
|
||||||
cap = capabilities
|
|
||||||
for index in range(0, len(scope)):
|
|
||||||
try:
|
|
||||||
cap = cap.get(scope[index], None)
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
if cap is None:
|
|
||||||
return False
|
|
||||||
if not extra_specs_ops.match(cap, req):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
"""Return a list of hosts that can create instance_type."""
|
|
||||||
# Note(zhiteng) Currently only Cinder and Nova are using
|
|
||||||
# this filter, so the resource type is either instance or
|
|
||||||
# volume.
|
|
||||||
resource_type = filter_properties.get('resource_type')
|
|
||||||
if not self._satisfies_extra_specs(host_state.capabilities,
|
|
||||||
resource_type):
|
|
||||||
return False
|
|
||||||
return True
|
|
@ -1,68 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 1. The following operations are supported:
|
|
||||||
# =, s==, s!=, s>=, s>, s<=, s<, <in>, <or>, ==, !=, >=, <=
|
|
||||||
# 2. Note that <or> is handled in a different way below.
|
|
||||||
# 3. If the first word in the extra_specs is not one of the operators,
|
|
||||||
# it is ignored.
|
|
||||||
_op_methods = {'=': lambda x, y: float(x) >= float(y),
|
|
||||||
'<in>': lambda x, y: y in x,
|
|
||||||
'==': lambda x, y: float(x) == float(y),
|
|
||||||
'!=': lambda x, y: float(x) != float(y),
|
|
||||||
'>=': lambda x, y: float(x) >= float(y),
|
|
||||||
'<=': lambda x, y: float(x) <= float(y),
|
|
||||||
's==': operator.eq,
|
|
||||||
's!=': operator.ne,
|
|
||||||
's<': operator.lt,
|
|
||||||
's<=': operator.le,
|
|
||||||
's>': operator.gt,
|
|
||||||
's>=': operator.ge}
|
|
||||||
|
|
||||||
|
|
||||||
def match(value, req):
|
|
||||||
words = req.split()
|
|
||||||
|
|
||||||
op = method = None
|
|
||||||
if words:
|
|
||||||
op = words.pop(0)
|
|
||||||
method = _op_methods.get(op)
|
|
||||||
|
|
||||||
if op != '<or>' and not method:
|
|
||||||
return value == req
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if op == '<or>': # Ex: <or> v1 <or> v2 <or> v3
|
|
||||||
while True:
|
|
||||||
if words.pop(0) == value:
|
|
||||||
return True
|
|
||||||
if not words:
|
|
||||||
break
|
|
||||||
op = words.pop(0) # remove a keyword <or>
|
|
||||||
if not words:
|
|
||||||
break
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
if words and method(value, words[0]):
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
@ -1,150 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from cinder.openstack.common import jsonutils
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
|
|
||||||
|
|
||||||
class JsonFilter(filters.BaseHostFilter):
|
|
||||||
"""Host Filter to allow simple JSON-based grammar for
|
|
||||||
selecting hosts.
|
|
||||||
"""
|
|
||||||
def _op_compare(self, args, op):
|
|
||||||
"""Returns True if the specified operator can successfully
|
|
||||||
compare the first item in the args with all the rest. Will
|
|
||||||
return False if only one item is in the list.
|
|
||||||
"""
|
|
||||||
if len(args) < 2:
|
|
||||||
return False
|
|
||||||
if op is operator.contains:
|
|
||||||
bad = not args[0] in args[1:]
|
|
||||||
else:
|
|
||||||
bad = [arg for arg in args[1:]
|
|
||||||
if not op(args[0], arg)]
|
|
||||||
return not bool(bad)
|
|
||||||
|
|
||||||
def _equals(self, args):
|
|
||||||
"""First term is == all the other terms."""
|
|
||||||
return self._op_compare(args, operator.eq)
|
|
||||||
|
|
||||||
def _less_than(self, args):
|
|
||||||
"""First term is < all the other terms."""
|
|
||||||
return self._op_compare(args, operator.lt)
|
|
||||||
|
|
||||||
def _greater_than(self, args):
|
|
||||||
"""First term is > all the other terms."""
|
|
||||||
return self._op_compare(args, operator.gt)
|
|
||||||
|
|
||||||
def _in(self, args):
|
|
||||||
"""First term is in set of remaining terms"""
|
|
||||||
return self._op_compare(args, operator.contains)
|
|
||||||
|
|
||||||
def _less_than_equal(self, args):
|
|
||||||
"""First term is <= all the other terms."""
|
|
||||||
return self._op_compare(args, operator.le)
|
|
||||||
|
|
||||||
def _greater_than_equal(self, args):
|
|
||||||
"""First term is >= all the other terms."""
|
|
||||||
return self._op_compare(args, operator.ge)
|
|
||||||
|
|
||||||
def _not(self, args):
|
|
||||||
"""Flip each of the arguments."""
|
|
||||||
return [not arg for arg in args]
|
|
||||||
|
|
||||||
def _or(self, args):
|
|
||||||
"""True if any arg is True."""
|
|
||||||
return any(args)
|
|
||||||
|
|
||||||
def _and(self, args):
|
|
||||||
"""True if all args are True."""
|
|
||||||
return all(args)
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'=': _equals,
|
|
||||||
'<': _less_than,
|
|
||||||
'>': _greater_than,
|
|
||||||
'in': _in,
|
|
||||||
'<=': _less_than_equal,
|
|
||||||
'>=': _greater_than_equal,
|
|
||||||
'not': _not,
|
|
||||||
'or': _or,
|
|
||||||
'and': _and,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _parse_string(self, string, host_state):
|
|
||||||
"""Strings prefixed with $ are capability lookups in the
|
|
||||||
form '$variable' where 'variable' is an attribute in the
|
|
||||||
HostState class. If $variable is a dictionary, you may
|
|
||||||
use: $variable.dictkey
|
|
||||||
"""
|
|
||||||
if not string:
|
|
||||||
return None
|
|
||||||
if not string.startswith("$"):
|
|
||||||
return string
|
|
||||||
|
|
||||||
path = string[1:].split(".")
|
|
||||||
obj = getattr(host_state, path[0], None)
|
|
||||||
if obj is None:
|
|
||||||
return None
|
|
||||||
for item in path[1:]:
|
|
||||||
obj = obj.get(item, None)
|
|
||||||
if obj is None:
|
|
||||||
return None
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def _process_filter(self, query, host_state):
|
|
||||||
"""Recursively parse the query structure."""
|
|
||||||
if not query:
|
|
||||||
return True
|
|
||||||
cmd = query[0]
|
|
||||||
method = self.commands[cmd]
|
|
||||||
cooked_args = []
|
|
||||||
for arg in query[1:]:
|
|
||||||
if isinstance(arg, list):
|
|
||||||
arg = self._process_filter(arg, host_state)
|
|
||||||
elif isinstance(arg, basestring):
|
|
||||||
arg = self._parse_string(arg, host_state)
|
|
||||||
if arg is not None:
|
|
||||||
cooked_args.append(arg)
|
|
||||||
result = method(self, cooked_args)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
"""Return a list of hosts that can fulfill the requirements
|
|
||||||
specified in the query.
|
|
||||||
"""
|
|
||||||
# TODO(zhiteng) Add description for filter_properties structure
|
|
||||||
# and scheduler_hints.
|
|
||||||
try:
|
|
||||||
query = filter_properties['scheduler_hints']['query']
|
|
||||||
except KeyError:
|
|
||||||
query = None
|
|
||||||
if not query:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# NOTE(comstud): Not checking capabilities or service for
|
|
||||||
# enabled/disabled so that a provided json filter can decide
|
|
||||||
|
|
||||||
result = self._process_filter(jsonutils.loads(query), host_state)
|
|
||||||
if isinstance(result, list):
|
|
||||||
# If any succeeded, include the host
|
|
||||||
result = any(result)
|
|
||||||
if result:
|
|
||||||
# Filter it out.
|
|
||||||
return True
|
|
||||||
return False
|
|
@ -1,91 +0,0 @@
|
|||||||
# Copyright (c) 2011-2012 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Pluggable Weighing support
|
|
||||||
"""
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from stevedore import extension
|
|
||||||
|
|
||||||
|
|
||||||
class WeighedObject(object):
|
|
||||||
"""Object with weight information."""
|
|
||||||
def __init__(self, obj, weight):
|
|
||||||
self.obj = obj
|
|
||||||
self.weight = weight
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<WeighedObject '%s': %s>" % (self.obj, self.weight)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseWeigher(object):
|
|
||||||
"""Base class for pluggable weighers."""
|
|
||||||
def _weight_multiplier(self):
|
|
||||||
"""How weighted this weigher should be. Normally this would
|
|
||||||
be overriden in a subclass based on a config value.
|
|
||||||
"""
|
|
||||||
return 1.0
|
|
||||||
|
|
||||||
def _weigh_object(self, obj, weight_properties):
|
|
||||||
"""Override in a subclass to specify a weight for a specific
|
|
||||||
object.
|
|
||||||
"""
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def weigh_objects(self, weighed_obj_list, weight_properties):
|
|
||||||
"""Weigh multiple objects. Override in a subclass if you need
|
|
||||||
need access to all objects in order to manipulate weights.
|
|
||||||
"""
|
|
||||||
constant = self._weight_multiplier()
|
|
||||||
for obj in weighed_obj_list:
|
|
||||||
obj.weight += (constant *
|
|
||||||
self._weigh_object(obj.obj, weight_properties))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseWeightHandler(object):
|
|
||||||
object_class = WeighedObject
|
|
||||||
|
|
||||||
def __init__(self, weighed_object_type, weight_namespace):
|
|
||||||
self.namespace = weight_namespace
|
|
||||||
self.weighed_object_type = weighed_object_type
|
|
||||||
self.weight_manager = extension.ExtensionManager(weight_namespace)
|
|
||||||
|
|
||||||
def _is_correct_class(self, obj):
|
|
||||||
"""Return whether an object is a class of the correct type and
|
|
||||||
is not prefixed with an underscore.
|
|
||||||
"""
|
|
||||||
return (inspect.isclass(obj) and
|
|
||||||
not obj.__name__.startswith('_') and
|
|
||||||
issubclass(obj, self.weighed_object_type))
|
|
||||||
|
|
||||||
def get_all_classes(self):
|
|
||||||
return [x.plugin for x in self.weight_manager
|
|
||||||
if self._is_correct_class(x.plugin)]
|
|
||||||
|
|
||||||
def get_weighed_objects(self, weigher_classes, obj_list,
|
|
||||||
weighing_properties):
|
|
||||||
"""Return a sorted (highest score first) list of WeighedObjects."""
|
|
||||||
|
|
||||||
if not obj_list:
|
|
||||||
return []
|
|
||||||
|
|
||||||
weighed_objs = [self.object_class(obj, 0.0) for obj in obj_list]
|
|
||||||
for weigher_cls in weigher_classes:
|
|
||||||
weigher = weigher_cls()
|
|
||||||
weigher.weigh_objects(weighed_objs, weighing_properties)
|
|
||||||
|
|
||||||
return sorted(weighed_objs, key=lambda x: x.weight, reverse=True)
|
|
@ -1,45 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Scheduler host weights
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from cinder.openstack.common.scheduler import weight
|
|
||||||
|
|
||||||
|
|
||||||
class WeighedHost(weight.WeighedObject):
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
'weight': self.weight,
|
|
||||||
'host': self.obj.host,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return ("WeighedHost [host: %s, weight: %s]" %
|
|
||||||
(self.obj.host, self.weight))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseHostWeigher(weight.BaseWeigher):
|
|
||||||
"""Base class for host weights."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HostWeightHandler(weight.BaseWeightHandler):
|
|
||||||
object_class = WeighedHost
|
|
||||||
|
|
||||||
def __init__(self, namespace):
|
|
||||||
super(HostWeightHandler, self).__init__(BaseHostWeigher, namespace)
|
|
@ -1,130 +0,0 @@
|
|||||||
# Copyright (c) 2011 Intel Corporation
|
|
||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
The FilterScheduler is for creating volumes.
|
|
||||||
You can customize this scheduler by specifying your own volume Filters and
|
|
||||||
Weighing Functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import operator
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import flags
|
|
||||||
from cinder.openstack.common import importutils
|
|
||||||
from cinder.openstack.common import log as logging
|
|
||||||
from cinder.scheduler import driver
|
|
||||||
from cinder.scheduler import scheduler_options
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterScheduler(driver.Scheduler):
|
|
||||||
"""Scheduler that can be used for filtering and weighing."""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FilterScheduler, self).__init__(*args, **kwargs)
|
|
||||||
self.cost_function_cache = None
|
|
||||||
self.options = scheduler_options.SchedulerOptions()
|
|
||||||
|
|
||||||
def schedule(self, context, topic, method, *args, **kwargs):
|
|
||||||
"""The schedule() contract requires we return the one
|
|
||||||
best-suited host for this request.
|
|
||||||
"""
|
|
||||||
self._schedule(context, topic, *args, **kwargs)
|
|
||||||
|
|
||||||
def _get_configuration_options(self):
|
|
||||||
"""Fetch options dictionary. Broken out for testing."""
|
|
||||||
return self.options.get_configuration()
|
|
||||||
|
|
||||||
def populate_filter_properties(self, request_spec, filter_properties):
|
|
||||||
"""Stuff things into filter_properties. Can be overridden in a
|
|
||||||
subclass to add more data.
|
|
||||||
"""
|
|
||||||
vol = request_spec['volume_properties']
|
|
||||||
filter_properties['size'] = vol['size']
|
|
||||||
filter_properties['availability_zone'] = vol.get('availability_zone')
|
|
||||||
filter_properties['user_id'] = vol.get('user_id')
|
|
||||||
filter_properties['metadata'] = vol.get('metadata')
|
|
||||||
|
|
||||||
def schedule_create_volume(self, context, request_spec, filter_properties):
|
|
||||||
weighed_host = self._schedule(context, request_spec,
|
|
||||||
filter_properties)
|
|
||||||
|
|
||||||
if not weighed_host:
|
|
||||||
raise exception.NoValidHost(reason="")
|
|
||||||
|
|
||||||
host = weighed_host.obj.host
|
|
||||||
volume_id = request_spec['volume_id']
|
|
||||||
snapshot_id = request_spec['snapshot_id']
|
|
||||||
image_id = request_spec['image_id']
|
|
||||||
|
|
||||||
updated_volume = driver.volume_update_db(context, volume_id, host)
|
|
||||||
self.volume_rpcapi.create_volume(context, updated_volume, host,
|
|
||||||
snapshot_id, image_id)
|
|
||||||
|
|
||||||
def _schedule(self, context, request_spec, filter_properties=None):
|
|
||||||
"""Returns a list of hosts that meet the required specs,
|
|
||||||
ordered by their fitness.
|
|
||||||
"""
|
|
||||||
elevated = context.elevated()
|
|
||||||
|
|
||||||
volume_properties = request_spec['volume_properties']
|
|
||||||
# Since Nova is using mixed filters from Oslo and it's own, which
|
|
||||||
# takes 'resource_XX' and 'instance_XX' as input respectively, copying
|
|
||||||
# 'instance_XX' to 'resource_XX' will make both filters happy.
|
|
||||||
resource_properties = volume_properties.copy()
|
|
||||||
volume_type = request_spec.get("volume_type", None)
|
|
||||||
resource_type = request_spec.get("volume_type", None)
|
|
||||||
request_spec.update({'resource_properties': resource_properties})
|
|
||||||
|
|
||||||
config_options = self._get_configuration_options()
|
|
||||||
|
|
||||||
if filter_properties is None:
|
|
||||||
filter_properties = {}
|
|
||||||
filter_properties.update({'context': context,
|
|
||||||
'request_spec': request_spec,
|
|
||||||
'config_options': config_options,
|
|
||||||
'volume_type': volume_type,
|
|
||||||
'resource_type': resource_type})
|
|
||||||
|
|
||||||
self.populate_filter_properties(request_spec,
|
|
||||||
filter_properties)
|
|
||||||
|
|
||||||
# Find our local list of acceptable hosts by filtering and
|
|
||||||
# weighing our options. we virtually consume resources on
|
|
||||||
# it so subsequent selections can adjust accordingly.
|
|
||||||
|
|
||||||
# Note: remember, we are using an iterator here. So only
|
|
||||||
# traverse this list once.
|
|
||||||
hosts = self.host_manager.get_all_host_states(elevated)
|
|
||||||
|
|
||||||
# Filter local hosts based on requirements ...
|
|
||||||
hosts = self.host_manager.get_filtered_hosts(hosts,
|
|
||||||
filter_properties)
|
|
||||||
if not hosts:
|
|
||||||
return None
|
|
||||||
|
|
||||||
LOG.debug(_("Filtered %(hosts)s") % locals())
|
|
||||||
# weighted_host = WeightedHost() ... the best
|
|
||||||
# host for the job.
|
|
||||||
weighed_hosts = self.host_manager.get_weighed_hosts(hosts,
|
|
||||||
filter_properties)
|
|
||||||
best_host = weighed_hosts[0]
|
|
||||||
LOG.debug(_("Choosing %(best_host)s") % locals())
|
|
||||||
best_host.obj.consume_from_volume(volume_properties)
|
|
||||||
return best_host
|
|
@ -1,14 +0,0 @@
|
|||||||
# Copyright (c) 2013 OpenStack, LLC.
|
|
||||||
# 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.
|
|
@ -1,44 +0,0 @@
|
|||||||
# Copyright (c) 2012 Intel
|
|
||||||
# Copyright (c) 2012 OpenStack, LLC.
|
|
||||||
#
|
|
||||||
# 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 math
|
|
||||||
|
|
||||||
from cinder.openstack.common import log as logging
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CapacityFilter(filters.BaseHostFilter):
|
|
||||||
"""CapacityFilter filters based on volume host's capacity utilization."""
|
|
||||||
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
"""Return True if host has sufficient capacity."""
|
|
||||||
volume_size = filter_properties.get('size')
|
|
||||||
|
|
||||||
if not host_state.free_capacity_gb:
|
|
||||||
# Fail Safe
|
|
||||||
LOG.warning(_("Free capacity not set;"
|
|
||||||
"volume node info collection broken."))
|
|
||||||
return False
|
|
||||||
|
|
||||||
reserved = float(host_state.reserved_percentage) / 100
|
|
||||||
free = math.floor(host_state.free_capacity_gb * (1 - reserved))
|
|
||||||
|
|
||||||
return free >= volume_size
|
|
@ -17,261 +17,20 @@
|
|||||||
Manage hosts in the current zone.
|
Manage hosts in the current zone.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import UserDict
|
# FIXME(ja): this code was written only for compute. re-implement for volumes
|
||||||
|
|
||||||
from cinder import db
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import flags
|
|
||||||
from cinder.openstack.common import cfg
|
|
||||||
from cinder.openstack.common import log as logging
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
from cinder.openstack.common.scheduler import weights
|
|
||||||
from cinder.openstack.common import timeutils
|
|
||||||
from cinder import utils
|
|
||||||
|
|
||||||
|
|
||||||
host_manager_opts = [
|
|
||||||
cfg.ListOpt('scheduler_default_filters',
|
|
||||||
default=[
|
|
||||||
'AvailabilityZoneFilter',
|
|
||||||
'CapacityFilter',
|
|
||||||
'CapabilitiesFilter'
|
|
||||||
],
|
|
||||||
help='Which filter class names to use for filtering hosts '
|
|
||||||
'when not specified in the request.'),
|
|
||||||
cfg.ListOpt('scheduler_default_weighers',
|
|
||||||
default=[
|
|
||||||
'CapacityWeigher'
|
|
||||||
],
|
|
||||||
help='Which weigher class names to use for weighing hosts.')
|
|
||||||
]
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
FLAGS.register_opts(host_manager_opts)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyDict(UserDict.IterableUserDict):
|
|
||||||
"""A read-only dict."""
|
|
||||||
def __init__(self, source=None):
|
|
||||||
self.data = {}
|
|
||||||
self.update(source)
|
|
||||||
|
|
||||||
def __setitem__(self, key, item):
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
def pop(self, key, *args):
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
def popitem(self):
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
def update(self, source=None):
|
|
||||||
if source is None:
|
|
||||||
return
|
|
||||||
elif isinstance(source, UserDict.UserDict):
|
|
||||||
self.data = source.data
|
|
||||||
elif isinstance(source, type({})):
|
|
||||||
self.data = source
|
|
||||||
else:
|
|
||||||
raise TypeError
|
|
||||||
|
|
||||||
|
|
||||||
class HostState(object):
|
class HostState(object):
|
||||||
"""Mutable and immutable information tracked for a host."""
|
pass
|
||||||
|
|
||||||
def __init__(self, host, capabilities=None, service=None):
|
|
||||||
self.host = host
|
|
||||||
self.update_capabilities(capabilities, service)
|
|
||||||
|
|
||||||
self.volume_backend_name = None
|
|
||||||
self.vendor_name = None
|
|
||||||
self.driver_version = 0
|
|
||||||
self.storage_protocol = None
|
|
||||||
self.QoS_support = False
|
|
||||||
# Mutable available resources.
|
|
||||||
# These will change as resources are virtually "consumed".
|
|
||||||
self.total_capacity_gb = 0
|
|
||||||
self.free_capacity_gb = 0
|
|
||||||
self.reserved_percentage = 0
|
|
||||||
|
|
||||||
self.updated = None
|
|
||||||
|
|
||||||
def update_capabilities(self, capabilities=None, service=None):
|
|
||||||
# Read-only capability dicts
|
|
||||||
|
|
||||||
if capabilities is None:
|
|
||||||
capabilities = {}
|
|
||||||
self.capabilities = ReadOnlyDict(capabilities)
|
|
||||||
if service is None:
|
|
||||||
service = {}
|
|
||||||
self.service = ReadOnlyDict(service)
|
|
||||||
|
|
||||||
def update_from_volume_capability(self, capability):
|
|
||||||
"""Update information about a host from its volume_node info."""
|
|
||||||
if self.updated and self.updated > capability['timestamp']:
|
|
||||||
return
|
|
||||||
|
|
||||||
if capability:
|
|
||||||
self.volume_backend = capability.get('volume_backend_name', None)
|
|
||||||
self.vendor_name = capability.get('vendor_name', None)
|
|
||||||
self.driver_version = capability.get('driver_version', None)
|
|
||||||
self.storage_protocol = capability.get('storage_protocol', None)
|
|
||||||
self.QoS_support = capability.get('QoS_support', False)
|
|
||||||
|
|
||||||
self.total_capacity_gb = capability['total_capacity_gb']
|
|
||||||
self.free_capacity_gb = capability['free_capacity_gb']
|
|
||||||
self.reserved_percentage = capability['reserved_percentage']
|
|
||||||
|
|
||||||
self.updated = capability['timestamp']
|
|
||||||
|
|
||||||
def consume_from_volume(self, volume):
|
|
||||||
"""Incrementally update host state from an volume"""
|
|
||||||
volume_gb = volume['size']
|
|
||||||
self.free_capacity_gb -= volume_gb
|
|
||||||
self.updated = timeutils.utcnow()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return ("host '%s': free_capacity_gb: %s" %
|
|
||||||
(self.host, self.free_capacity_gb))
|
|
||||||
|
|
||||||
|
|
||||||
class HostManager(object):
|
class HostManager(object):
|
||||||
"""Base HostManager class."""
|
|
||||||
|
|
||||||
host_state_cls = HostState
|
def get_host_list(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def update_service_capabilities(self, *args):
|
||||||
self.service_states = {} # { <host>: {<service>: {cap k : v}}}
|
pass
|
||||||
self.host_state_map = {}
|
|
||||||
self.filter_handler = filters.HostFilterHandler('cinder.scheduler.'
|
|
||||||
'filters')
|
|
||||||
self.filter_classes = self.filter_handler.get_all_classes()
|
|
||||||
self.weight_handler = weights.HostWeightHandler('cinder.scheduler.'
|
|
||||||
'weights')
|
|
||||||
self.weight_classes = self.weight_handler.get_all_classes()
|
|
||||||
|
|
||||||
def _choose_host_filters(self, filter_cls_names):
|
def get_service_capabilities(self, *args):
|
||||||
"""Since the caller may specify which filters to use we need
|
pass
|
||||||
to have an authoritative list of what is permissible. This
|
|
||||||
function checks the filter names against a predefined set
|
|
||||||
of acceptable filters.
|
|
||||||
"""
|
|
||||||
if filter_cls_names is None:
|
|
||||||
filter_cls_names = FLAGS.scheduler_default_filters
|
|
||||||
if not isinstance(filter_cls_names, (list, tuple)):
|
|
||||||
filter_cls_names = [filter_cls_names]
|
|
||||||
good_filters = []
|
|
||||||
bad_filters = []
|
|
||||||
for filter_name in filter_cls_names:
|
|
||||||
found_class = False
|
|
||||||
for cls in self.filter_classes:
|
|
||||||
if cls.__name__ == filter_name:
|
|
||||||
found_class = True
|
|
||||||
good_filters.append(cls)
|
|
||||||
break
|
|
||||||
if not found_class:
|
|
||||||
bad_filters.append(filter_name)
|
|
||||||
if bad_filters:
|
|
||||||
msg = ", ".join(bad_filters)
|
|
||||||
raise exception.SchedulerHostFilterNotFound(filter_name=msg)
|
|
||||||
return good_filters
|
|
||||||
|
|
||||||
def _choose_host_weighers(self, weight_cls_names):
|
|
||||||
"""Since the caller may specify which weighers to use, we need
|
|
||||||
to have an authoritative list of what is permissible. This
|
|
||||||
function checks the weigher names against a predefined set
|
|
||||||
of acceptable weighers.
|
|
||||||
"""
|
|
||||||
if weight_cls_names is None:
|
|
||||||
weight_cls_names = FLAGS.scheduler_default_weighers
|
|
||||||
if not isinstance(weight_cls_names, (list, tuple)):
|
|
||||||
weight_cls_names = [weight_cls_names]
|
|
||||||
|
|
||||||
good_weighers = []
|
|
||||||
bad_weighers = []
|
|
||||||
for weigher_name in weight_cls_names:
|
|
||||||
found_class = False
|
|
||||||
for cls in self.weight_classes:
|
|
||||||
if cls.__name__ == weigher_name:
|
|
||||||
good_weighers.append(cls)
|
|
||||||
found_class = True
|
|
||||||
break
|
|
||||||
if not found_class:
|
|
||||||
bad_weighers.append(weigher_name)
|
|
||||||
if bad_weighers:
|
|
||||||
msg = ", ".join(bad_weighers)
|
|
||||||
raise exception.SchedulerHostWeigherNotFound(weigher_name=msg)
|
|
||||||
return good_weighers
|
|
||||||
|
|
||||||
def get_filtered_hosts(self, hosts, filter_properties,
|
|
||||||
filter_class_names=None):
|
|
||||||
"""Filter hosts and return only ones passing all filters"""
|
|
||||||
filter_classes = self._choose_host_filters(filter_class_names)
|
|
||||||
return self.filter_handler.get_filtered_objects(filter_classes,
|
|
||||||
hosts,
|
|
||||||
filter_properties)
|
|
||||||
|
|
||||||
def get_weighed_hosts(self, hosts, weight_properties,
|
|
||||||
weigher_class_names=None):
|
|
||||||
"""Weigh the hosts"""
|
|
||||||
weigher_classes = self._choose_host_weighers(weigher_class_names)
|
|
||||||
return self.weight_handler.get_weighed_objects(weigher_classes,
|
|
||||||
hosts,
|
|
||||||
weight_properties)
|
|
||||||
|
|
||||||
def update_service_capabilities(self, service_name, host, capabilities):
|
|
||||||
"""Update the per-service capabilities based on this notification."""
|
|
||||||
if service_name != 'volume':
|
|
||||||
LOG.debug(_('Ignoring %(service_name)s service update '
|
|
||||||
'from %(host)s'), locals())
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.debug(_("Received %(service_name)s service update from "
|
|
||||||
"%(host)s.") % locals())
|
|
||||||
|
|
||||||
# Copy the capabilities, so we don't modify the original dict
|
|
||||||
capab_copy = dict(capabilities)
|
|
||||||
capab_copy["timestamp"] = timeutils.utcnow() # Reported time
|
|
||||||
self.service_states[host] = capab_copy
|
|
||||||
|
|
||||||
def get_all_host_states(self, context):
|
|
||||||
"""Returns a dict of all the hosts the HostManager
|
|
||||||
knows about. Also, each of the consumable resources in HostState
|
|
||||||
are pre-populated and adjusted based on data in the db.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
{'192.168.1.100': HostState(), ...}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get resource usage across the available volume nodes:
|
|
||||||
topic = FLAGS.volume_topic
|
|
||||||
volume_services = db.service_get_all_by_topic(context, topic)
|
|
||||||
for service in volume_services:
|
|
||||||
if not utils.service_is_up(service) or service['disabled']:
|
|
||||||
LOG.warn(_("service is down or disabled."))
|
|
||||||
continue
|
|
||||||
host = service['host']
|
|
||||||
capabilities = self.service_states.get(host, None)
|
|
||||||
host_state = self.host_state_map.get(host)
|
|
||||||
if host_state:
|
|
||||||
# copy capabilities to host_state.capabilities
|
|
||||||
host_state.update_capabilities(capabilities,
|
|
||||||
dict(service.iteritems()))
|
|
||||||
else:
|
|
||||||
host_state = self.host_state_cls(host,
|
|
||||||
capabilities=capabilities,
|
|
||||||
service=
|
|
||||||
dict(service.iteritems()))
|
|
||||||
self.host_state_map[host] = host_state
|
|
||||||
# update host_state
|
|
||||||
host_state.update_from_volume_capability(capabilities)
|
|
||||||
|
|
||||||
return self.host_state_map.itervalues()
|
|
||||||
|
@ -21,7 +21,8 @@
|
|||||||
Scheduler Service
|
Scheduler Service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cinder import context
|
import functools
|
||||||
|
|
||||||
from cinder import db
|
from cinder import db
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder import flags
|
from cinder import flags
|
||||||
@ -31,15 +32,14 @@ from cinder.openstack.common import excutils
|
|||||||
from cinder.openstack.common import importutils
|
from cinder.openstack.common import importutils
|
||||||
from cinder.openstack.common import log as logging
|
from cinder.openstack.common import log as logging
|
||||||
from cinder.openstack.common.notifier import api as notifier
|
from cinder.openstack.common.notifier import api as notifier
|
||||||
from cinder.volume import rpcapi as volume_rpcapi
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
scheduler_driver_opt = cfg.StrOpt('scheduler_driver',
|
scheduler_driver_opt = cfg.StrOpt(
|
||||||
default='cinder.scheduler.filter_scheduler.'
|
'scheduler_driver',
|
||||||
'FilterScheduler',
|
default='cinder.scheduler.simple.SimpleScheduler',
|
||||||
help='Default scheduler driver to use')
|
help='Default driver to use for the scheduler')
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
FLAGS.register_opt(scheduler_driver_opt)
|
FLAGS.register_opt(scheduler_driver_opt)
|
||||||
@ -56,10 +56,6 @@ class SchedulerManager(manager.Manager):
|
|||||||
self.driver = importutils.import_object(scheduler_driver)
|
self.driver = importutils.import_object(scheduler_driver)
|
||||||
super(SchedulerManager, self).__init__(*args, **kwargs)
|
super(SchedulerManager, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def init_host(self):
|
|
||||||
ctxt = context.get_admin_context()
|
|
||||||
self.request_service_capabilities(ctxt)
|
|
||||||
|
|
||||||
def get_host_list(self, context):
|
def get_host_list(self, context):
|
||||||
"""Get a list of hosts from the HostManager."""
|
"""Get a list of hosts from the HostManager."""
|
||||||
return self.driver.get_host_list()
|
return self.driver.get_host_list()
|
||||||
@ -134,6 +130,3 @@ class SchedulerManager(manager.Manager):
|
|||||||
|
|
||||||
notifier.notify(context, notifier.publisher_id("scheduler"),
|
notifier.notify(context, notifier.publisher_id("scheduler"),
|
||||||
'scheduler.' + method, notifier.ERROR, payload)
|
'scheduler.' + method, notifier.ERROR, payload)
|
||||||
|
|
||||||
def request_service_capabilities(self, context):
|
|
||||||
volume_rpcapi.VolumeAPI().publish_service_capabilities(context)
|
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright (c) 2011 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
SchedulerOptions monitors a local .json file for changes and loads
|
|
||||||
it if needed. This file is converted to a data structure and passed
|
|
||||||
into the filtering and weighing functions which can use it for
|
|
||||||
dynamic configuration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from cinder import flags
|
|
||||||
from cinder.openstack.common import cfg
|
|
||||||
from cinder.openstack.common import log as logging
|
|
||||||
from cinder.openstack.common import timeutils
|
|
||||||
|
|
||||||
|
|
||||||
scheduler_json_config_location_opt = cfg.StrOpt(
|
|
||||||
'scheduler_json_config_location',
|
|
||||||
default='',
|
|
||||||
help='Absolute path to scheduler configuration JSON file.')
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
FLAGS.register_opt(scheduler_json_config_location_opt)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerOptions(object):
|
|
||||||
"""
|
|
||||||
SchedulerOptions monitors a local .json file for changes and loads it
|
|
||||||
if needed. This file is converted to a data structure and passed into
|
|
||||||
the filtering and weighing functions which can use it for dynamic
|
|
||||||
configuration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(SchedulerOptions, self).__init__()
|
|
||||||
self.data = {}
|
|
||||||
self.last_modified = None
|
|
||||||
self.last_checked = None
|
|
||||||
|
|
||||||
def _get_file_handle(self, filename):
|
|
||||||
"""Get file handle. Broken out for testing."""
|
|
||||||
return open(filename)
|
|
||||||
|
|
||||||
def _get_file_timestamp(self, filename):
|
|
||||||
"""Get the last modified datetime. Broken out for testing."""
|
|
||||||
try:
|
|
||||||
return os.path.getmtime(filename)
|
|
||||||
except os.error, e:
|
|
||||||
LOG.exception(_("Could not stat scheduler options file "
|
|
||||||
"%(filename)s: '%(e)s'"), locals())
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _load_file(self, handle):
|
|
||||||
"""Decode the JSON file. Broken out for testing."""
|
|
||||||
try:
|
|
||||||
return json.load(handle)
|
|
||||||
except ValueError, e:
|
|
||||||
LOG.exception(_("Could not decode scheduler options: "
|
|
||||||
"'%(e)s'") % locals())
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _get_time_now(self):
|
|
||||||
"""Get current UTC. Broken out for testing."""
|
|
||||||
return timeutils.utcnow()
|
|
||||||
|
|
||||||
def get_configuration(self, filename=None):
|
|
||||||
"""Check the json file for changes and load it if needed."""
|
|
||||||
if not filename:
|
|
||||||
filename = FLAGS.scheduler_json_config_location
|
|
||||||
if not filename:
|
|
||||||
return self.data
|
|
||||||
if self.last_checked:
|
|
||||||
now = self._get_time_now()
|
|
||||||
if now - self.last_checked < datetime.timedelta(minutes=5):
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
last_modified = self._get_file_timestamp(filename)
|
|
||||||
if (not last_modified or not self.last_modified or
|
|
||||||
last_modified > self.last_modified):
|
|
||||||
self.data = self._load_file(self._get_file_handle(filename))
|
|
||||||
self.last_modified = last_modified
|
|
||||||
if not self.data:
|
|
||||||
self.data = {}
|
|
||||||
|
|
||||||
return self.data
|
|
@ -1,14 +0,0 @@
|
|||||||
# Copyright (c) 2013 OpenStack, LLC.
|
|
||||||
# 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.
|
|
@ -1,50 +0,0 @@
|
|||||||
# Copyright (c) 2012 OpenStack, LLC.
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
Capacity Weigher. Weigh hosts by their available capacity.
|
|
||||||
|
|
||||||
The default is to spread volumes across all hosts evenly. If you prefer
|
|
||||||
stacking, you can set the 'capacity_weight_multiplier' option to a negative
|
|
||||||
number and the weighing has the opposite effect of the default.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
from cinder import flags
|
|
||||||
from cinder.openstack.common import cfg
|
|
||||||
from cinder.openstack.common.scheduler import weights
|
|
||||||
|
|
||||||
|
|
||||||
capacity_weight_opts = [
|
|
||||||
cfg.FloatOpt('capacity_weight_multiplier',
|
|
||||||
default=1.0,
|
|
||||||
help='Multiplier used for weighing volume capacity. '
|
|
||||||
'Negative numbers mean to stack vs spread.'),
|
|
||||||
]
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
FLAGS.register_opts(capacity_weight_opts)
|
|
||||||
|
|
||||||
|
|
||||||
class CapacityWeigher(weights.BaseHostWeigher):
|
|
||||||
def _weight_multiplier(self):
|
|
||||||
"""Override the weight multiplier."""
|
|
||||||
return FLAGS.capacity_weight_multiplier
|
|
||||||
|
|
||||||
def _weigh_object(self, host_state, weight_properties):
|
|
||||||
"""Higher weights win. We want spreading to be the default."""
|
|
||||||
reserved = float(host_state.reserved_percentage) / 100
|
|
||||||
free = math.floor(host_state.free_capacity_gb * (1 - reserved))
|
|
||||||
return free
|
|
@ -16,68 +16,45 @@
|
|||||||
Fakes For Scheduler tests.
|
Fakes For Scheduler tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import mox
|
|
||||||
|
|
||||||
from cinder import db
|
|
||||||
from cinder.openstack.common import timeutils
|
|
||||||
from cinder.scheduler import filter_scheduler
|
|
||||||
from cinder.scheduler import host_manager
|
from cinder.scheduler import host_manager
|
||||||
|
|
||||||
|
|
||||||
VOLUME_SERVICES = [
|
|
||||||
dict(id=1, host='host1', topic='volume', disabled=False,
|
|
||||||
availability_zone='zone1', updated_at=timeutils.utcnow()),
|
|
||||||
dict(id=2, host='host2', topic='volume', disabled=False,
|
|
||||||
availability_zone='zone1', updated_at=timeutils.utcnow()),
|
|
||||||
dict(id=3, host='host3', topic='volume', disabled=False,
|
|
||||||
availability_zone='zone2', updated_at=timeutils.utcnow()),
|
|
||||||
dict(id=4, host='host4', topic='volume', disabled=False,
|
|
||||||
availability_zone='zone3', updated_at=timeutils.utcnow()),
|
|
||||||
# service on host5 is disabled
|
|
||||||
dict(id=5, host='host5', topic='volume', disabled=True,
|
|
||||||
availability_zone='zone4', updated_at=timeutils.utcnow()),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFilterScheduler(filter_scheduler.FilterScheduler):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FakeFilterScheduler, self).__init__(*args, **kwargs)
|
|
||||||
self.host_manager = host_manager.HostManager()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeHostManager(host_manager.HostManager):
|
class FakeHostManager(host_manager.HostManager):
|
||||||
|
"""host1: free_ram_mb=1024-512-512=0, free_disk_gb=1024-512-512=0
|
||||||
|
host2: free_ram_mb=2048-512=1536 free_disk_gb=2048-512=1536
|
||||||
|
host3: free_ram_mb=4096-1024=3072 free_disk_gb=4096-1024=3072
|
||||||
|
host4: free_ram_mb=8192 free_disk_gb=8192"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(FakeHostManager, self).__init__()
|
super(FakeHostManager, self).__init__()
|
||||||
|
|
||||||
self.service_states = {
|
self.service_states = {
|
||||||
'host1': {'total_capacity_gb': 1024,
|
'host1': {
|
||||||
'free_capacity_gb': 1024,
|
'compute': {'host_memory_free': 1073741824},
|
||||||
'reserved_percentage': 10,
|
},
|
||||||
'timestamp': None},
|
'host2': {
|
||||||
'host2': {'total_capacity_gb': 2048,
|
'compute': {'host_memory_free': 2147483648},
|
||||||
'free_capacity_gb': 300,
|
},
|
||||||
'reserved_percentage': 10,
|
'host3': {
|
||||||
'timestamp': None},
|
'compute': {'host_memory_free': 3221225472},
|
||||||
'host3': {'total_capacity_gb': 512,
|
},
|
||||||
'free_capacity_gb': 512,
|
'host4': {
|
||||||
'reserved_percentage': 0,
|
'compute': {'host_memory_free': 999999999},
|
||||||
'timestamp': None},
|
},
|
||||||
'host4': {'total_capacity_gb': 2048,
|
|
||||||
'free_capacity_gb': 200,
|
|
||||||
'reserved_percentage': 5,
|
|
||||||
'timestamp': None},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_host_list_from_db(self, context):
|
||||||
|
return [
|
||||||
|
('host1', dict(free_disk_gb=1024, free_ram_mb=1024)),
|
||||||
|
('host2', dict(free_disk_gb=2048, free_ram_mb=2048)),
|
||||||
|
('host3', dict(free_disk_gb=4096, free_ram_mb=4096)),
|
||||||
|
('host4', dict(free_disk_gb=8192, free_ram_mb=8192)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FakeHostState(host_manager.HostState):
|
class FakeHostState(host_manager.HostState):
|
||||||
def __init__(self, host, attribute_dict):
|
def __init__(self, host, topic, attribute_dict):
|
||||||
super(FakeHostState, self).__init__(host)
|
super(FakeHostState, self).__init__(host, topic)
|
||||||
for (key, val) in attribute_dict.iteritems():
|
for (key, val) in attribute_dict.iteritems():
|
||||||
setattr(self, key, val)
|
setattr(self, key, val)
|
||||||
|
|
||||||
|
|
||||||
def mox_host_manager_db_calls(mock, context):
|
|
||||||
mock.StubOutWithMock(db, 'service_get_all_by_topic')
|
|
||||||
|
|
||||||
db.service_get_all_by_topic(mox.IgnoreArg(),
|
|
||||||
mox.IgnoreArg()).AndReturn(VOLUME_SERVICES)
|
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
# Copyright 2011-2012 OpenStack LLC.
|
|
||||||
# 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 Capacity Weigher.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder.openstack.common.scheduler.weights import HostWeightHandler
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.scheduler import fakes
|
|
||||||
|
|
||||||
|
|
||||||
class CapacityWeigherTestCase(test.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(CapacityWeigherTestCase, self).setUp()
|
|
||||||
self.host_manager = fakes.FakeHostManager()
|
|
||||||
self.weight_handler = HostWeightHandler('cinder.scheduler.weights')
|
|
||||||
self.weight_classes = self.weight_handler.get_all_classes()
|
|
||||||
|
|
||||||
def _get_weighed_host(self, hosts, weight_properties=None):
|
|
||||||
if weight_properties is None:
|
|
||||||
weight_properties = {}
|
|
||||||
return self.weight_handler.get_weighed_objects(self.weight_classes,
|
|
||||||
hosts,
|
|
||||||
weight_properties)[0]
|
|
||||||
|
|
||||||
def _get_all_hosts(self):
|
|
||||||
ctxt = context.get_admin_context()
|
|
||||||
fakes.mox_host_manager_db_calls(self.mox, ctxt)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
host_states = self.host_manager.get_all_host_states(ctxt)
|
|
||||||
self.mox.VerifyAll()
|
|
||||||
self.mox.ResetAll()
|
|
||||||
return host_states
|
|
||||||
|
|
||||||
def test_default_of_spreading_first(self):
|
|
||||||
hostinfo_list = self._get_all_hosts()
|
|
||||||
|
|
||||||
# host1: free_capacity_gb=1024, free=1024*(1-0.1)
|
|
||||||
# host2: free_capacity_gb=300, free=300*(1-0.1)
|
|
||||||
# host3: free_capacity_gb=512, free=512
|
|
||||||
# host4: free_capacity_gb=200, free=200*(1-0.05)
|
|
||||||
|
|
||||||
# so, host1 should win:
|
|
||||||
weighed_host = self._get_weighed_host(hostinfo_list)
|
|
||||||
self.assertEqual(weighed_host.weight, 921.0)
|
|
||||||
self.assertEqual(weighed_host.obj.host, 'host1')
|
|
||||||
|
|
||||||
def test_capacity_weight_multiplier1(self):
|
|
||||||
self.flags(capacity_weight_multiplier=-1.0)
|
|
||||||
hostinfo_list = self._get_all_hosts()
|
|
||||||
|
|
||||||
# host1: free_capacity_gb=1024, free=-1024*(1-0.1)
|
|
||||||
# host2: free_capacity_gb=300, free=-300*(1-0.1)
|
|
||||||
# host3: free_capacity_gb=512, free=-512
|
|
||||||
# host4: free_capacity_gb=200, free=-200*(1-0.05)
|
|
||||||
|
|
||||||
# so, host4 should win:
|
|
||||||
weighed_host = self._get_weighed_host(hostinfo_list)
|
|
||||||
self.assertEqual(weighed_host.weight, -190.0)
|
|
||||||
self.assertEqual(weighed_host.obj.host, 'host4')
|
|
||||||
|
|
||||||
def test_capacity_weight_multiplier2(self):
|
|
||||||
self.flags(capacity_weight_multiplier=2.0)
|
|
||||||
hostinfo_list = self._get_all_hosts()
|
|
||||||
|
|
||||||
# host1: free_capacity_gb=1024, free=1024*(1-0.1)*2
|
|
||||||
# host2: free_capacity_gb=300, free=300*(1-0.1)*2
|
|
||||||
# host3: free_capacity_gb=512, free=512*2
|
|
||||||
# host4: free_capacity_gb=200, free=200*(1-0.05)*2
|
|
||||||
|
|
||||||
# so, host1 should win:
|
|
||||||
weighed_host = self._get_weighed_host(hostinfo_list)
|
|
||||||
self.assertEqual(weighed_host.weight, 921.0 * 2)
|
|
||||||
self.assertEqual(weighed_host.obj.host, 'host1')
|
|
@ -1,107 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack LLC.
|
|
||||||
# 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 Filter Scheduler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mox
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.openstack.common.scheduler import weights
|
|
||||||
from cinder.scheduler import driver
|
|
||||||
from cinder.scheduler import filter_scheduler
|
|
||||||
from cinder.scheduler import host_manager
|
|
||||||
from cinder.tests.scheduler import fakes
|
|
||||||
from cinder.tests.scheduler import test_scheduler
|
|
||||||
|
|
||||||
|
|
||||||
def fake_get_filtered_hosts(hosts, filter_properties):
|
|
||||||
return list(hosts)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|
||||||
"""Test case for Filter Scheduler."""
|
|
||||||
|
|
||||||
driver_cls = filter_scheduler.FilterScheduler
|
|
||||||
|
|
||||||
def test_create_volume_no_hosts(self):
|
|
||||||
"""
|
|
||||||
Ensure empty hosts & child_zones result in NoValidHosts exception.
|
|
||||||
"""
|
|
||||||
def _fake_empty_call_zone_method(*args, **kwargs):
|
|
||||||
return []
|
|
||||||
|
|
||||||
sched = fakes.FakeFilterScheduler()
|
|
||||||
|
|
||||||
fake_context = context.RequestContext('user', 'project')
|
|
||||||
request_spec = {'volume_properties': {'project_id': 1,
|
|
||||||
'size': 1},
|
|
||||||
'volume_type': {'name': 'LVM_iSCSI'},
|
|
||||||
'volume_id': ['fake-id1']}
|
|
||||||
self.assertRaises(exception.NoValidHost, sched.schedule_create_volume,
|
|
||||||
fake_context, request_spec, None)
|
|
||||||
|
|
||||||
def test_create_volume_non_admin(self):
|
|
||||||
"""Test creating an instance locally using run_instance, passing
|
|
||||||
a non-admin context. DB actions should work."""
|
|
||||||
self.was_admin = False
|
|
||||||
|
|
||||||
def fake_get(context, *args, **kwargs):
|
|
||||||
# make sure this is called with admin context, even though
|
|
||||||
# we're using user context below
|
|
||||||
self.was_admin = context.is_admin
|
|
||||||
return {}
|
|
||||||
|
|
||||||
sched = fakes.FakeFilterScheduler()
|
|
||||||
self.stubs.Set(sched.host_manager, 'get_all_host_states', fake_get)
|
|
||||||
|
|
||||||
fake_context = context.RequestContext('user', 'project')
|
|
||||||
|
|
||||||
request_spec = {'volume_properties': {'project_id': 1,
|
|
||||||
'size': 1},
|
|
||||||
'volume_type': {'name': 'LVM_iSCSI'},
|
|
||||||
'volume_id': ['fake-id1']}
|
|
||||||
self.assertRaises(exception.NoValidHost, sched.schedule_create_volume,
|
|
||||||
fake_context, request_spec, None)
|
|
||||||
self.assertTrue(self.was_admin)
|
|
||||||
|
|
||||||
def test_schedule_happy_day(self):
|
|
||||||
"""Make sure there's nothing glaringly wrong with _schedule()
|
|
||||||
by doing a happy day pass through."""
|
|
||||||
|
|
||||||
self.next_weight = 1.0
|
|
||||||
|
|
||||||
def _fake_weigh_objects(_self, functions, hosts, options):
|
|
||||||
self.next_weight += 2.0
|
|
||||||
host_state = hosts[0]
|
|
||||||
return [weights.WeighedHost(host_state, self.next_weight)]
|
|
||||||
|
|
||||||
sched = fakes.FakeFilterScheduler()
|
|
||||||
fake_context = context.RequestContext('user', 'project',
|
|
||||||
is_admin=True)
|
|
||||||
|
|
||||||
self.stubs.Set(sched.host_manager, 'get_filtered_hosts',
|
|
||||||
fake_get_filtered_hosts)
|
|
||||||
self.stubs.Set(weights.HostWeightHandler,
|
|
||||||
'get_weighed_objects', _fake_weigh_objects)
|
|
||||||
fakes.mox_host_manager_db_calls(self.mox, fake_context)
|
|
||||||
|
|
||||||
request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
|
|
||||||
'volume_properties': {'project_id': 1,
|
|
||||||
'size': 1}}
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
weighed_host = sched._schedule(fake_context, request_spec, {})
|
|
||||||
self.assertTrue(weighed_host.obj is not None)
|
|
@ -1,99 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack LLC. # 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 Scheduler Host Filters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import httplib
|
|
||||||
import stubout
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import db
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.openstack.common import jsonutils
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.scheduler import fakes
|
|
||||||
from cinder import utils
|
|
||||||
|
|
||||||
|
|
||||||
DATA = ''
|
|
||||||
|
|
||||||
|
|
||||||
def stub_out_https_backend(stubs):
|
|
||||||
"""
|
|
||||||
Stubs out the httplib.HTTPRequest.getresponse to return
|
|
||||||
faked-out data instead of grabbing actual contents of a resource
|
|
||||||
|
|
||||||
The stubbed getresponse() returns an iterator over
|
|
||||||
the data "I am a teapot, short and stout\n"
|
|
||||||
|
|
||||||
:param stubs: Set of stubout stubs
|
|
||||||
"""
|
|
||||||
|
|
||||||
class FakeHTTPResponse(object):
|
|
||||||
|
|
||||||
def read(self):
|
|
||||||
return DATA
|
|
||||||
|
|
||||||
def fake_do_request(self, *args, **kwargs):
|
|
||||||
return httplib.OK, FakeHTTPResponse()
|
|
||||||
|
|
||||||
|
|
||||||
class HostFiltersTestCase(test.TestCase):
|
|
||||||
"""Test case for host filters."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HostFiltersTestCase, self).setUp()
|
|
||||||
self.stubs = stubout.StubOutForTesting()
|
|
||||||
stub_out_https_backend(self.stubs)
|
|
||||||
self.context = context.RequestContext('fake', 'fake')
|
|
||||||
self.json_query = jsonutils.dumps(
|
|
||||||
['and', ['>=', '$free_capacity_gb', 1024],
|
|
||||||
['>=', '$total_capacity_gb', 10 * 1024]])
|
|
||||||
# This has a side effect of testing 'get_filter_classes'
|
|
||||||
# when specifying a method (in this case, our standard filters)
|
|
||||||
filter_handler = filters.HostFilterHandler('cinder.scheduler.filters')
|
|
||||||
classes = filter_handler.get_all_classes()
|
|
||||||
self.class_map = {}
|
|
||||||
for cls in classes:
|
|
||||||
self.class_map[cls.__name__] = cls
|
|
||||||
|
|
||||||
def _stub_service_is_up(self, ret_value):
|
|
||||||
def fake_service_is_up(service):
|
|
||||||
return ret_value
|
|
||||||
self.stubs.Set(utils, 'service_is_up', fake_service_is_up)
|
|
||||||
|
|
||||||
def test_capacity_filter_passes(self):
|
|
||||||
self._stub_service_is_up(True)
|
|
||||||
filt_cls = self.class_map['CapacityFilter']()
|
|
||||||
filter_properties = {'size': 100}
|
|
||||||
service = {'disabled': False}
|
|
||||||
host = fakes.FakeHostState('host1',
|
|
||||||
{'free_capacity_gb': 200,
|
|
||||||
'updated_at': None,
|
|
||||||
'service': service})
|
|
||||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
|
||||||
|
|
||||||
def test_capacity_filter_fails(self):
|
|
||||||
self._stub_service_is_up(True)
|
|
||||||
filt_cls = self.class_map['CapacityFilter']()
|
|
||||||
filter_properties = {'size': 100}
|
|
||||||
service = {'disabled': False}
|
|
||||||
host = fakes.FakeHostState('host1',
|
|
||||||
{'free_capacity_gb': 120,
|
|
||||||
'reserved_percentage': 20,
|
|
||||||
'updated_at': None,
|
|
||||||
'service': service})
|
|
||||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
|
@ -1,176 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack, LLC
|
|
||||||
# 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 HostManager
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from cinder import db
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import flags
|
|
||||||
from cinder.openstack.common.scheduler import filters
|
|
||||||
from cinder.openstack.common import timeutils
|
|
||||||
from cinder.scheduler import host_manager
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.scheduler import fakes
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFilterClass1(filters.BaseHostFilter):
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFilterClass2(filters.BaseHostFilter):
|
|
||||||
def host_passes(self, host_state, filter_properties):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HostManagerTestCase(test.TestCase):
|
|
||||||
"""Test case for HostManager class"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HostManagerTestCase, self).setUp()
|
|
||||||
self.host_manager = host_manager.HostManager()
|
|
||||||
self.fake_hosts = [host_manager.HostState('fake_host%s' % x)
|
|
||||||
for x in xrange(1, 5)]
|
|
||||||
|
|
||||||
def test_choose_host_filters_not_found(self):
|
|
||||||
self.flags(scheduler_default_filters='FakeFilterClass3')
|
|
||||||
self.host_manager.filter_classes = [FakeFilterClass1,
|
|
||||||
FakeFilterClass2]
|
|
||||||
self.assertRaises(exception.SchedulerHostFilterNotFound,
|
|
||||||
self.host_manager._choose_host_filters, None)
|
|
||||||
|
|
||||||
def test_choose_host_filters(self):
|
|
||||||
self.flags(scheduler_default_filters=['FakeFilterClass2'])
|
|
||||||
self.host_manager.filter_classes = [FakeFilterClass1,
|
|
||||||
FakeFilterClass2]
|
|
||||||
|
|
||||||
# Test 'volume' returns 1 correct function
|
|
||||||
filter_classes = self.host_manager._choose_host_filters(None)
|
|
||||||
self.assertEqual(len(filter_classes), 1)
|
|
||||||
self.assertEqual(filter_classes[0].__name__, 'FakeFilterClass2')
|
|
||||||
|
|
||||||
def _mock_get_filtered_hosts(self, info, specified_filters=None):
|
|
||||||
self.mox.StubOutWithMock(self.host_manager, '_choose_host_filters')
|
|
||||||
|
|
||||||
info['got_objs'] = []
|
|
||||||
info['got_fprops'] = []
|
|
||||||
|
|
||||||
def fake_filter_one(_self, obj, filter_props):
|
|
||||||
info['got_objs'].append(obj)
|
|
||||||
info['got_fprops'].append(filter_props)
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.stubs.Set(FakeFilterClass1, '_filter_one', fake_filter_one)
|
|
||||||
self.host_manager._choose_host_filters(specified_filters).AndReturn(
|
|
||||||
[FakeFilterClass1])
|
|
||||||
|
|
||||||
def _verify_result(self, info, result):
|
|
||||||
for x in info['got_fprops']:
|
|
||||||
self.assertEqual(x, info['expected_fprops'])
|
|
||||||
self.assertEqual(set(info['expected_objs']), set(info['got_objs']))
|
|
||||||
self.assertEqual(set(result), set(info['got_objs']))
|
|
||||||
|
|
||||||
def test_get_filtered_hosts(self):
|
|
||||||
fake_properties = {'moo': 1, 'cow': 2}
|
|
||||||
|
|
||||||
info = {'expected_objs': self.fake_hosts,
|
|
||||||
'expected_fprops': fake_properties}
|
|
||||||
|
|
||||||
self._mock_get_filtered_hosts(info)
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
|
||||||
fake_properties)
|
|
||||||
self._verify_result(info, result)
|
|
||||||
|
|
||||||
def test_update_service_capabilities(self):
|
|
||||||
service_states = self.host_manager.service_states
|
|
||||||
self.assertDictMatch(service_states, {})
|
|
||||||
self.mox.StubOutWithMock(timeutils, 'utcnow')
|
|
||||||
timeutils.utcnow().AndReturn(31337)
|
|
||||||
timeutils.utcnow().AndReturn(31338)
|
|
||||||
timeutils.utcnow().AndReturn(31339)
|
|
||||||
|
|
||||||
host1_volume_capabs = dict(free_capacity_gb=4321, timestamp=1)
|
|
||||||
host2_volume_capabs = dict(free_capacity_gb=5432, timestamp=1)
|
|
||||||
host3_volume_capabs = dict(free_capacity_gb=6543, timestamp=1)
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
service_name = 'volume'
|
|
||||||
self.host_manager.update_service_capabilities(service_name, 'host1',
|
|
||||||
host1_volume_capabs)
|
|
||||||
self.host_manager.update_service_capabilities(service_name, 'host2',
|
|
||||||
host2_volume_capabs)
|
|
||||||
self.host_manager.update_service_capabilities(service_name, 'host3',
|
|
||||||
host3_volume_capabs)
|
|
||||||
|
|
||||||
# Make sure dictionary isn't re-assigned
|
|
||||||
self.assertEqual(self.host_manager.service_states, service_states)
|
|
||||||
# Make sure original dictionary wasn't copied
|
|
||||||
self.assertEqual(host1_volume_capabs['timestamp'], 1)
|
|
||||||
|
|
||||||
host1_volume_capabs['timestamp'] = 31337
|
|
||||||
host2_volume_capabs['timestamp'] = 31338
|
|
||||||
host3_volume_capabs['timestamp'] = 31339
|
|
||||||
|
|
||||||
expected = {'host1': host1_volume_capabs,
|
|
||||||
'host2': host2_volume_capabs,
|
|
||||||
'host3': host3_volume_capabs}
|
|
||||||
self.assertDictMatch(service_states, expected)
|
|
||||||
|
|
||||||
def test_get_all_host_states(self):
|
|
||||||
context = 'fake_context'
|
|
||||||
topic = FLAGS.volume_topic
|
|
||||||
|
|
||||||
self.mox.StubOutWithMock(db, 'service_get_all_by_topic')
|
|
||||||
self.mox.StubOutWithMock(host_manager.LOG, 'warn')
|
|
||||||
|
|
||||||
ret_services = fakes.VOLUME_SERVICES
|
|
||||||
db.service_get_all_by_topic(context, topic).AndReturn(ret_services)
|
|
||||||
# Disabled service
|
|
||||||
host_manager.LOG.warn("service is down or disabled.")
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
self.host_manager.get_all_host_states(context)
|
|
||||||
host_state_map = self.host_manager.host_state_map
|
|
||||||
|
|
||||||
self.assertEqual(len(host_state_map), 4)
|
|
||||||
# Check that service is up
|
|
||||||
for i in xrange(4):
|
|
||||||
volume_node = fakes.VOLUME_SERVICES[i]
|
|
||||||
host = volume_node['host']
|
|
||||||
self.assertEqual(host_state_map[host].service,
|
|
||||||
volume_node)
|
|
||||||
|
|
||||||
|
|
||||||
class HostStateTestCase(test.TestCase):
|
|
||||||
"""Test case for HostState class"""
|
|
||||||
|
|
||||||
def test_update_from_volume_capability(self):
|
|
||||||
fake_host = host_manager.HostState('host1')
|
|
||||||
self.assertEqual(fake_host.free_capacity_gb, 0)
|
|
||||||
|
|
||||||
volume_capability = {'total_capacity_gb': 1024,
|
|
||||||
'free_capacity_gb': 512,
|
|
||||||
'reserved_percentage': 0,
|
|
||||||
'timestamp': None}
|
|
||||||
|
|
||||||
fake_host.update_from_volume_capability(volume_capability)
|
|
||||||
self.assertEqual(fake_host.free_capacity_gb, 512)
|
|
@ -1,138 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack LLC.
|
|
||||||
# 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 PickledScheduler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
from cinder.openstack.common import jsonutils
|
|
||||||
from cinder.scheduler import scheduler_options
|
|
||||||
from cinder import test
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSchedulerOptions(scheduler_options.SchedulerOptions):
|
|
||||||
def __init__(self, last_checked, now, file_old, file_now, data, filedata):
|
|
||||||
super(FakeSchedulerOptions, self).__init__()
|
|
||||||
# Change internals ...
|
|
||||||
self.last_modified = file_old
|
|
||||||
self.last_checked = last_checked
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
# For overrides ...
|
|
||||||
self._time_now = now
|
|
||||||
self._file_now = file_now
|
|
||||||
self._file_data = filedata
|
|
||||||
|
|
||||||
self.file_was_loaded = False
|
|
||||||
|
|
||||||
def _get_file_timestamp(self, filename):
|
|
||||||
return self._file_now
|
|
||||||
|
|
||||||
def _get_file_handle(self, filename):
|
|
||||||
self.file_was_loaded = True
|
|
||||||
return StringIO.StringIO(self._file_data)
|
|
||||||
|
|
||||||
def _get_time_now(self):
|
|
||||||
return self._time_now
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerOptionsTestCase(test.TestCase):
|
|
||||||
def test_get_configuration_first_time_no_flag(self):
|
|
||||||
last_checked = None
|
|
||||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_old = None
|
|
||||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
data = dict(a=1, b=2, c=3)
|
|
||||||
jdata = jsonutils.dumps(data)
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
{}, jdata)
|
|
||||||
self.assertEquals({}, fake.get_configuration())
|
|
||||||
self.assertFalse(fake.file_was_loaded)
|
|
||||||
|
|
||||||
def test_get_configuration_first_time_empty_file(self):
|
|
||||||
last_checked = None
|
|
||||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_old = None
|
|
||||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
data = dict(a=1, b=2, c=3)
|
|
||||||
jdata = ""
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
{}, jdata)
|
|
||||||
self.assertEquals({}, fake.get_configuration('foo.json'))
|
|
||||||
self.assertTrue(fake.file_was_loaded)
|
|
||||||
|
|
||||||
def test_get_configuration_first_time_happy_day(self):
|
|
||||||
last_checked = None
|
|
||||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_old = None
|
|
||||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
data = dict(a=1, b=2, c=3)
|
|
||||||
jdata = jsonutils.dumps(data)
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
{}, jdata)
|
|
||||||
self.assertEquals(data, fake.get_configuration('foo.json'))
|
|
||||||
self.assertTrue(fake.file_was_loaded)
|
|
||||||
|
|
||||||
def test_get_configuration_second_time_no_change(self):
|
|
||||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
|
||||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
data = dict(a=1, b=2, c=3)
|
|
||||||
jdata = jsonutils.dumps(data)
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
data, jdata)
|
|
||||||
self.assertEquals(data, fake.get_configuration('foo.json'))
|
|
||||||
self.assertFalse(fake.file_was_loaded)
|
|
||||||
|
|
||||||
def test_get_configuration_second_time_too_fast(self):
|
|
||||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
|
||||||
now = datetime.datetime(2011, 1, 1, 1, 1, 2)
|
|
||||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_now = datetime.datetime(2013, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
old_data = dict(a=1, b=2, c=3)
|
|
||||||
data = dict(a=11, b=12, c=13)
|
|
||||||
jdata = jsonutils.dumps(data)
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
old_data, jdata)
|
|
||||||
self.assertEquals(old_data, fake.get_configuration('foo.json'))
|
|
||||||
self.assertFalse(fake.file_was_loaded)
|
|
||||||
|
|
||||||
def test_get_configuration_second_time_change(self):
|
|
||||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
|
||||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
|
||||||
file_now = datetime.datetime(2013, 1, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
old_data = dict(a=1, b=2, c=3)
|
|
||||||
data = dict(a=11, b=12, c=13)
|
|
||||||
jdata = jsonutils.dumps(data)
|
|
||||||
|
|
||||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
|
||||||
old_data, jdata)
|
|
||||||
self.assertEquals(data, fake.get_configuration('foo.json'))
|
|
||||||
self.assertTrue(fake.file_was_loaded)
|
|
@ -30,7 +30,6 @@ from cinder import flags
|
|||||||
from cinder.image import image_utils
|
from cinder.image import image_utils
|
||||||
from cinder.openstack.common import cfg
|
from cinder.openstack.common import cfg
|
||||||
from cinder.openstack.common import log as logging
|
from cinder.openstack.common import log as logging
|
||||||
from cinder.openstack.common import timeutils
|
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder.volume import iscsi
|
from cinder.volume import iscsi
|
||||||
|
|
||||||
@ -62,11 +61,7 @@ volume_opts = [
|
|||||||
help='use this ip for iscsi'),
|
help='use this ip for iscsi'),
|
||||||
cfg.IntOpt('iscsi_port',
|
cfg.IntOpt('iscsi_port',
|
||||||
default=3260,
|
default=3260,
|
||||||
help='The port that the iSCSI daemon is listening on'),
|
help='The port that the iSCSI daemon is listening on'), ]
|
||||||
cfg.IntOpt('reserved_percentage',
|
|
||||||
default=0,
|
|
||||||
help='The percentage of backend capacity is reserved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
FLAGS.register_opts(volume_opts)
|
FLAGS.register_opts(volume_opts)
|
||||||
@ -78,7 +73,6 @@ class VolumeDriver(object):
|
|||||||
# NOTE(vish): db is set by Manager
|
# NOTE(vish): db is set by Manager
|
||||||
self.db = None
|
self.db = None
|
||||||
self.set_execute(execute)
|
self.set_execute(execute)
|
||||||
self._stats = {}
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
def set_execute(self, execute):
|
||||||
self._execute = execute
|
self._execute = execute
|
||||||
@ -625,49 +619,6 @@ class ISCSIDriver(VolumeDriver):
|
|||||||
def terminate_connection(self, volume, connector, **kwargs):
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
"""Get volume status.
|
|
||||||
|
|
||||||
If 'refresh' is True, run update the stats first."""
|
|
||||||
if refresh:
|
|
||||||
self._update_volume_status()
|
|
||||||
|
|
||||||
return self._stats
|
|
||||||
|
|
||||||
def _update_volume_status(self):
|
|
||||||
"""Retrieve status info from volume group."""
|
|
||||||
|
|
||||||
LOG.debug(_("Updating volume status"))
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# Note(zhiteng): These information are driver/backend specific,
|
|
||||||
# each driver may define these values in its own config options
|
|
||||||
# or fetch from driver specific configuration file.
|
|
||||||
data["volume_backend_name"] = 'LVM_iSCSI'
|
|
||||||
data["vendor_name"] = 'Open Source'
|
|
||||||
data["driver_version"] = '1.0'
|
|
||||||
data["storage_protocol"] = 'iSCSI'
|
|
||||||
|
|
||||||
data['total_capacity_gb'] = 0
|
|
||||||
data['free_capacity_gb'] = 0
|
|
||||||
data['reserved_percentage'] = FLAGS.reserved_percentage
|
|
||||||
data['QoS_support'] = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
out, err = self._execute('vgs', '--noheadings', '--nosuffix',
|
|
||||||
'--unit=G', '-o', 'name,size,free',
|
|
||||||
FLAGS.volume_group, run_as_root=True)
|
|
||||||
except exception.ProcessExecutionError as exc:
|
|
||||||
LOG.error(_("Error retrieving volume status: "), exc.stderr)
|
|
||||||
out = False
|
|
||||||
|
|
||||||
if out:
|
|
||||||
volume = out.split()
|
|
||||||
data['total_capacity_gb'] = float(volume[1])
|
|
||||||
data['free_capacity_gb'] = float(volume[2])
|
|
||||||
|
|
||||||
self._stats = data
|
|
||||||
|
|
||||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||||
"""Fetch the image from image_service and write it to the volume."""
|
"""Fetch the image from image_service and write it to the volume."""
|
||||||
image_utils.fetch_to_raw(context,
|
image_utils.fetch_to_raw(context,
|
||||||
|
@ -61,7 +61,9 @@ volume_manager_opts = [
|
|||||||
cfg.StrOpt('volume_driver',
|
cfg.StrOpt('volume_driver',
|
||||||
default='cinder.volume.driver.ISCSIDriver',
|
default='cinder.volume.driver.ISCSIDriver',
|
||||||
help='Driver to use for volume creation'),
|
help='Driver to use for volume creation'),
|
||||||
]
|
cfg.BoolOpt('volume_force_update_capabilities',
|
||||||
|
default=False,
|
||||||
|
help='if True will force update capabilities on each check'), ]
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
FLAGS.register_opts(volume_manager_opts)
|
FLAGS.register_opts(volume_manager_opts)
|
||||||
@ -101,7 +103,7 @@ MAPPING = {
|
|||||||
class VolumeManager(manager.SchedulerDependentManager):
|
class VolumeManager(manager.SchedulerDependentManager):
|
||||||
"""Manages attachable block storage devices."""
|
"""Manages attachable block storage devices."""
|
||||||
|
|
||||||
RPC_API_VERSION = '1.2'
|
RPC_API_VERSION = '1.1'
|
||||||
|
|
||||||
def __init__(self, volume_driver=None, *args, **kwargs):
|
def __init__(self, volume_driver=None, *args, **kwargs):
|
||||||
"""Load the driver from the one specified in args, or from flags."""
|
"""Load the driver from the one specified in args, or from flags."""
|
||||||
@ -118,6 +120,7 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
# NOTE(vish): Implementation specific db handling is done
|
# NOTE(vish): Implementation specific db handling is done
|
||||||
# by the driver.
|
# by the driver.
|
||||||
self.driver.db = self.db
|
self.driver.db = self.db
|
||||||
|
self._last_volume_stats = []
|
||||||
|
|
||||||
def init_host(self):
|
def init_host(self):
|
||||||
"""Do any initialization that needs to be run if this is a
|
"""Do any initialization that needs to be run if this is a
|
||||||
@ -141,9 +144,6 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
LOG.info(_('Resuming delete on volume: %s') % volume['id'])
|
LOG.info(_('Resuming delete on volume: %s') % volume['id'])
|
||||||
self.delete_volume(ctxt, volume['id'])
|
self.delete_volume(ctxt, volume['id'])
|
||||||
|
|
||||||
# collect and publish service capabilities
|
|
||||||
self.publish_service_capabilities(ctxt)
|
|
||||||
|
|
||||||
def create_volume(self, context, volume_id, snapshot_id=None,
|
def create_volume(self, context, volume_id, snapshot_id=None,
|
||||||
image_id=None, source_volid=None):
|
image_id=None, source_volid=None):
|
||||||
"""Creates and exports the volume."""
|
"""Creates and exports the volume."""
|
||||||
@ -490,19 +490,33 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
volume_ref = self.db.volume_get(context, volume_id)
|
volume_ref = self.db.volume_get(context, volume_id)
|
||||||
self.driver.terminate_connection(volume_ref, connector, force=force)
|
self.driver.terminate_connection(volume_ref, connector, force=force)
|
||||||
|
|
||||||
|
def _volume_stats_changed(self, stat1, stat2):
|
||||||
|
if FLAGS.volume_force_update_capabilities:
|
||||||
|
return True
|
||||||
|
if len(stat1) != len(stat2):
|
||||||
|
return True
|
||||||
|
for (k, v) in stat1.iteritems():
|
||||||
|
if (k, v) not in stat2.iteritems():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@manager.periodic_task
|
@manager.periodic_task
|
||||||
def _report_driver_status(self, context):
|
def _report_driver_status(self, context):
|
||||||
LOG.info(_("Updating volume status"))
|
|
||||||
volume_stats = self.driver.get_volume_stats(refresh=True)
|
volume_stats = self.driver.get_volume_stats(refresh=True)
|
||||||
if volume_stats:
|
if volume_stats:
|
||||||
# This will grab info about the host and queue it
|
LOG.info(_("Checking volume capabilities"))
|
||||||
# to be sent to the Schedulers.
|
|
||||||
self.update_service_capabilities(volume_stats)
|
|
||||||
|
|
||||||
def publish_service_capabilities(self, context):
|
if self._volume_stats_changed(self._last_volume_stats,
|
||||||
""" Collect driver status and then publish """
|
volume_stats):
|
||||||
self._report_driver_status(context)
|
LOG.info(_("New capabilities found: %s"), volume_stats)
|
||||||
self._publish_service_capabilities(context)
|
self._last_volume_stats = volume_stats
|
||||||
|
|
||||||
|
# This will grab info about the host and queue it
|
||||||
|
# to be sent to the Schedulers.
|
||||||
|
self.update_service_capabilities(self._last_volume_stats)
|
||||||
|
else:
|
||||||
|
# avoid repeating fanouts
|
||||||
|
self.update_service_capabilities(None)
|
||||||
|
|
||||||
def _reset_stats(self):
|
def _reset_stats(self):
|
||||||
LOG.info(_("Clear capabilities"))
|
LOG.info(_("Clear capabilities"))
|
||||||
|
@ -34,7 +34,6 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
|
|||||||
|
|
||||||
1.0 - Initial version.
|
1.0 - Initial version.
|
||||||
1.1 - Adds clone volume option to create_volume.
|
1.1 - Adds clone volume option to create_volume.
|
||||||
1.2 - Add publish_service_capabilities() method.
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BASE_RPC_API_VERSION = '1.0'
|
BASE_RPC_API_VERSION = '1.0'
|
||||||
@ -115,7 +114,3 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
|
|||||||
topic=rpc.queue_get_for(ctxt,
|
topic=rpc.queue_get_for(ctxt,
|
||||||
self.topic,
|
self.topic,
|
||||||
volume['host']))
|
volume['host']))
|
||||||
|
|
||||||
def publish_service_capabilities(self, ctxt):
|
|
||||||
self.fanout_cast(ctxt, self.make_msg('publish_service_capabilities'),
|
|
||||||
version='1.2')
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
|
||||||
# The list of modules to copy from openstack-common
|
# The list of modules to copy from openstack-common
|
||||||
modules=cfg,exception,excutils,gettextutils,importutils,iniparser,jsonutils,local,rpc,timeutils,log,setup,notifier,context,network_utils,policy,uuidutils,lockutils,fileutils,gettextutils,scheduler,scheduler.filters,scheduler.weights
|
modules=cfg,exception,excutils,gettextutils,importutils,iniparser,jsonutils,local,rpc,timeutils,log,setup,notifier,context,network_utils,policy,uuidutils,lockutils,fileutils,gettextutils
|
||||||
|
|
||||||
# The base module to hold the copy of openstack.common
|
# The base module to hold the copy of openstack.common
|
||||||
base=cinder
|
base=cinder
|
||||||
|
21
setup.py
21
setup.py
@ -23,23 +23,6 @@ from cinder import version
|
|||||||
|
|
||||||
requires = common_setup.parse_requirements()
|
requires = common_setup.parse_requirements()
|
||||||
|
|
||||||
filters = [
|
|
||||||
"AvailabilityZoneFilter = "
|
|
||||||
"cinder.openstack.common.scheduler.filters."
|
|
||||||
"availability_zone_filter:AvailabilityZoneFilter",
|
|
||||||
"CapabilitiesFilter = "
|
|
||||||
"cinder.openstack.common.scheduler.filters."
|
|
||||||
"capabilities_filter:CapabilitiesFilter",
|
|
||||||
"CapacityFilter = "
|
|
||||||
"cinder.scheduler.filters.capacity_filter:CapacityFilter",
|
|
||||||
"JsonFilter = "
|
|
||||||
"cinder.openstack.common.scheduler.filters.json_filter:JsonFilter",
|
|
||||||
]
|
|
||||||
|
|
||||||
weights = [
|
|
||||||
"CapacityWeigher = cinder.scheduler.weights.capacity:CapacityWeigher",
|
|
||||||
]
|
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='cinder',
|
name='cinder',
|
||||||
version=version.canonical_version_string(),
|
version=version.canonical_version_string(),
|
||||||
@ -60,10 +43,6 @@ setuptools.setup(
|
|||||||
cmdclass=common_setup.get_cmdclass(),
|
cmdclass=common_setup.get_cmdclass(),
|
||||||
packages=setuptools.find_packages(exclude=['bin', 'smoketests']),
|
packages=setuptools.find_packages(exclude=['bin', 'smoketests']),
|
||||||
install_requires=requires,
|
install_requires=requires,
|
||||||
entry_points={
|
|
||||||
'cinder.scheduler.filters': filters,
|
|
||||||
'cinder.scheduler.weights': weights,
|
|
||||||
},
|
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
test_suite='nose.collector',
|
test_suite='nose.collector',
|
||||||
setup_requires=['setuptools_git>=0.4'],
|
setup_requires=['setuptools_git>=0.4'],
|
||||||
|
@ -12,7 +12,6 @@ greenlet>=0.3.1
|
|||||||
PasteDeploy==1.5.0
|
PasteDeploy==1.5.0
|
||||||
paste
|
paste
|
||||||
sqlalchemy-migrate>=0.7.2
|
sqlalchemy-migrate>=0.7.2
|
||||||
stevedore>=0.8.0
|
|
||||||
suds==0.4
|
suds==0.4
|
||||||
paramiko
|
paramiko
|
||||||
Babel>=0.9.6
|
Babel>=0.9.6
|
||||||
|
Loading…
x
Reference in New Issue
Block a user