summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-01-09 09:59:11 +0000
committerGerrit Code Review <review@openstack.org>2013-01-09 09:59:11 +0000
commit141a4cab68cae6eef7f3fcc22b40e6c708e19126 (patch)
tree1e49be1ad875956d0a8b5f4404e67cc7ffdfa42a
parentc622cc7a103e946726bd968a05990140dd1836b7 (diff)
parent8dd2140c4c03582965d74232c7b61aef34e43661 (diff)
downloadcinder-141a4cab68cae6eef7f3fcc22b40e6c708e19126.tar.gz
Merge "Implement filter scheduler"grizzly-2
-rw-r--r--cinder/exception.py8
-rw-r--r--cinder/openstack/common/scheduler/__init__.py0
-rw-r--r--cinder/openstack/common/scheduler/filter.py71
-rw-r--r--cinder/openstack/common/scheduler/filters/__init__.py41
-rw-r--r--cinder/openstack/common/scheduler/filters/availability_zone_filter.py30
-rw-r--r--cinder/openstack/common/scheduler/filters/capabilities_filter.py63
-rw-r--r--cinder/openstack/common/scheduler/filters/extra_specs_ops.py68
-rw-r--r--cinder/openstack/common/scheduler/filters/json_filter.py150
-rw-r--r--cinder/openstack/common/scheduler/weight.py91
-rw-r--r--cinder/openstack/common/scheduler/weights/__init__.py45
-rw-r--r--cinder/scheduler/filter_scheduler.py130
-rw-r--r--cinder/scheduler/filters/__init__.py14
-rw-r--r--cinder/scheduler/filters/capacity_filter.py44
-rw-r--r--cinder/scheduler/host_manager.py257
-rw-r--r--cinder/scheduler/manager.py19
-rw-r--r--cinder/scheduler/scheduler_options.py105
-rw-r--r--cinder/scheduler/weights/__init__.py14
-rw-r--r--cinder/scheduler/weights/capacity.py50
-rw-r--r--cinder/tests/scheduler/fakes.py77
-rw-r--r--cinder/tests/scheduler/test_capacity_weigher.py87
-rw-r--r--cinder/tests/scheduler/test_filter_scheduler.py107
-rw-r--r--cinder/tests/scheduler/test_host_filters.py99
-rw-r--r--cinder/tests/scheduler/test_host_manager.py176
-rw-r--r--cinder/tests/scheduler/test_scheduler_options.py138
-rw-r--r--cinder/volume/driver.py51
-rw-r--r--cinder/volume/manager.py42
-rw-r--r--cinder/volume/rpcapi.py5
-rw-r--r--openstack-common.conf2
-rw-r--r--setup.py21
-rw-r--r--tools/pip-requires1
30 files changed, 1935 insertions, 71 deletions
diff --git a/cinder/exception.py b/cinder/exception.py
index 7cb9bd993..3abed63d8 100644
--- a/cinder/exception.py
+++ b/cinder/exception.py
@@ -314,6 +314,14 @@ class HostNotFound(NotFound):
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):
message = _("Could not find binary %(binary)s on host %(host)s.")
diff --git a/cinder/openstack/common/scheduler/__init__.py b/cinder/openstack/common/scheduler/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/cinder/openstack/common/scheduler/__init__.py
diff --git a/cinder/openstack/common/scheduler/filter.py b/cinder/openstack/common/scheduler/filter.py
new file mode 100644
index 000000000..0bdb10d0f
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filter.py
@@ -0,0 +1,71 @@
+# 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)
diff --git a/cinder/openstack/common/scheduler/filters/__init__.py b/cinder/openstack/common/scheduler/filters/__init__.py
new file mode 100644
index 000000000..e47f835ba
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filters/__init__.py
@@ -0,0 +1,41 @@
+# 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)
diff --git a/cinder/openstack/common/scheduler/filters/availability_zone_filter.py b/cinder/openstack/common/scheduler/filters/availability_zone_filter.py
new file mode 100644
index 000000000..0be4bd18a
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filters/availability_zone_filter.py
@@ -0,0 +1,30 @@
+# 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
diff --git a/cinder/openstack/common/scheduler/filters/capabilities_filter.py b/cinder/openstack/common/scheduler/filters/capabilities_filter.py
new file mode 100644
index 000000000..cd84460d0
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filters/capabilities_filter.py
@@ -0,0 +1,63 @@
+# 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
diff --git a/cinder/openstack/common/scheduler/filters/extra_specs_ops.py b/cinder/openstack/common/scheduler/filters/extra_specs_ops.py
new file mode 100644
index 000000000..f4d4ff44b
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filters/extra_specs_ops.py
@@ -0,0 +1,68 @@
+# 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
diff --git a/cinder/openstack/common/scheduler/filters/json_filter.py b/cinder/openstack/common/scheduler/filters/json_filter.py
new file mode 100644
index 000000000..0e3bb474a
--- /dev/null
+++ b/cinder/openstack/common/scheduler/filters/json_filter.py
@@ -0,0 +1,150 @@
+# 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
diff --git a/cinder/openstack/common/scheduler/weight.py b/cinder/openstack/common/scheduler/weight.py
new file mode 100644
index 000000000..c5df9da02
--- /dev/null
+++ b/cinder/openstack/common/scheduler/weight.py
@@ -0,0 +1,91 @@
+# 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)
diff --git a/cinder/openstack/common/scheduler/weights/__init__.py b/cinder/openstack/common/scheduler/weights/__init__.py
new file mode 100644
index 000000000..dd4b33209
--- /dev/null
+++ b/cinder/openstack/common/scheduler/weights/__init__.py
@@ -0,0 +1,45 @@
+# 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)
diff --git a/cinder/scheduler/filter_scheduler.py b/cinder/scheduler/filter_scheduler.py
new file mode 100644
index 000000000..ea8dc4ed7
--- /dev/null
+++ b/cinder/scheduler/filter_scheduler.py
@@ -0,0 +1,130 @@
+# 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
diff --git a/cinder/scheduler/filters/__init__.py b/cinder/scheduler/filters/__init__.py
new file mode 100644
index 000000000..ce4951de9
--- /dev/null
+++ b/cinder/scheduler/filters/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/cinder/scheduler/filters/capacity_filter.py b/cinder/scheduler/filters/capacity_filter.py
new file mode 100644
index 000000000..edd5024b7
--- /dev/null
+++ b/cinder/scheduler/filters/capacity_filter.py
@@ -0,0 +1,44 @@
+# 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
diff --git a/cinder/scheduler/host_manager.py b/cinder/scheduler/host_manager.py
index 99632bc86..6a76136c9 100644
--- a/cinder/scheduler/host_manager.py
+++ b/cinder/scheduler/host_manager.py
@@ -17,20 +17,261 @@
Manage hosts in the current zone.
"""
-# FIXME(ja): this code was written only for compute. re-implement for volumes
+import UserDict
+
+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):
- pass
+ """Mutable and immutable information tracked for a host."""
+
+ 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):
+ """Base HostManager class."""
+
+ host_state_cls = HostState
+
+ def __init__(self):
+ self.service_states = {} # { <host>: {<service>: {cap k : v}}}
+ 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):
+ """Since the caller may specify which filters to use we need
+ 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.
- def get_host_list(self, *args):
- pass
+ For example:
+ {'192.168.1.100': HostState(), ...}
+ """
- def update_service_capabilities(self, *args):
- pass
+ # 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)
- def get_service_capabilities(self, *args):
- pass
+ return self.host_state_map.itervalues()
diff --git a/cinder/scheduler/manager.py b/cinder/scheduler/manager.py
index a423e8dc7..92247a89c 100644
--- a/cinder/scheduler/manager.py
+++ b/cinder/scheduler/manager.py
@@ -21,8 +21,7 @@
Scheduler Service
"""
-import functools
-
+from cinder import context
from cinder import db
from cinder import exception
from cinder import flags
@@ -32,14 +31,15 @@ from cinder.openstack.common import excutils
from cinder.openstack.common import importutils
from cinder.openstack.common import log as logging
from cinder.openstack.common.notifier import api as notifier
+from cinder.volume import rpcapi as volume_rpcapi
LOG = logging.getLogger(__name__)
-scheduler_driver_opt = cfg.StrOpt(
- 'scheduler_driver',
- default='cinder.scheduler.simple.SimpleScheduler',
- help='Default driver to use for the scheduler')
+scheduler_driver_opt = cfg.StrOpt('scheduler_driver',
+ default='cinder.scheduler.filter_scheduler.'
+ 'FilterScheduler',
+ help='Default scheduler driver to use')
FLAGS = flags.FLAGS
FLAGS.register_opt(scheduler_driver_opt)
@@ -56,6 +56,10 @@ class SchedulerManager(manager.Manager):
self.driver = importutils.import_object(scheduler_driver)
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):
"""Get a list of hosts from the HostManager."""
return self.driver.get_host_list()
@@ -130,3 +134,6 @@ class SchedulerManager(manager.Manager):
notifier.notify(context, notifier.publisher_id("scheduler"),
'scheduler.' + method, notifier.ERROR, payload)
+
+ def request_service_capabilities(self, context):
+ volume_rpcapi.VolumeAPI().publish_service_capabilities(context)
diff --git a/cinder/scheduler/scheduler_options.py b/cinder/scheduler/scheduler_options.py
new file mode 100644
index 000000000..039831b1d
--- /dev/null
+++ b/cinder/scheduler/scheduler_options.py
@@ -0,0 +1,105 @@
+# 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
diff --git a/cinder/scheduler/weights/__init__.py b/cinder/scheduler/weights/__init__.py
new file mode 100644
index 000000000..ce4951de9
--- /dev/null
+++ b/cinder/scheduler/weights/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/cinder/scheduler/weights/capacity.py b/cinder/scheduler/weights/capacity.py
new file mode 100644
index 000000000..2bb5a0b25
--- /dev/null
+++ b/cinder/scheduler/weights/capacity.py
@@ -0,0 +1,50 @@
+# 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
diff --git a/cinder/tests/scheduler/fakes.py b/cinder/tests/scheduler/fakes.py
index 97a4f8cd8..6668a1087 100644
--- a/cinder/tests/scheduler/fakes.py
+++ b/cinder/tests/scheduler/fakes.py
@@ -16,45 +16,68 @@
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
-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"""
+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):
def __init__(self):
super(FakeHostManager, self).__init__()
self.service_states = {
- 'host1': {
- 'compute': {'host_memory_free': 1073741824},
- },
- 'host2': {
- 'compute': {'host_memory_free': 2147483648},
- },
- 'host3': {
- 'compute': {'host_memory_free': 3221225472},
- },
- 'host4': {
- 'compute': {'host_memory_free': 999999999},
- },
+ 'host1': {'total_capacity_gb': 1024,
+ 'free_capacity_gb': 1024,
+ 'reserved_percentage': 10,
+ 'timestamp': None},
+ 'host2': {'total_capacity_gb': 2048,
+ 'free_capacity_gb': 300,
+ 'reserved_percentage': 10,
+ 'timestamp': None},
+ 'host3': {'total_capacity_gb': 512,
+ 'free_capacity_gb': 512,
+ 'reserved_percentage': 0,
+ '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):
- def __init__(self, host, topic, attribute_dict):
- super(FakeHostState, self).__init__(host, topic)
+ def __init__(self, host, attribute_dict):
+ super(FakeHostState, self).__init__(host)
for (key, val) in attribute_dict.iteritems():
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)
diff --git a/cinder/tests/scheduler/test_capacity_weigher.py b/cinder/tests/scheduler/test_capacity_weigher.py
new file mode 100644
index 000000000..471364f9c
--- /dev/null
+++ b/cinder/tests/scheduler/test_capacity_weigher.py
@@ -0,0 +1,87 @@
+# 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')
diff --git a/cinder/tests/scheduler/test_filter_scheduler.py b/cinder/tests/scheduler/test_filter_scheduler.py
new file mode 100644
index 000000000..6f56575f7
--- /dev/null
+++ b/cinder/tests/scheduler/test_filter_scheduler.py
@@ -0,0 +1,107 @@
+# 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)
diff --git a/cinder/tests/scheduler/test_host_filters.py b/cinder/tests/scheduler/test_host_filters.py
new file mode 100644
index 000000000..5bcffd63c
--- /dev/null
+++ b/cinder/tests/scheduler/test_host_filters.py
@@ -0,0 +1,99 @@
+# 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))
diff --git a/cinder/tests/scheduler/test_host_manager.py b/cinder/tests/scheduler/test_host_manager.py
new file mode 100644
index 000000000..0d2207ea7
--- /dev/null
+++ b/cinder/tests/scheduler/test_host_manager.py
@@ -0,0 +1,176 @@
+# 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)
diff --git a/cinder/tests/scheduler/test_scheduler_options.py b/cinder/tests/scheduler/test_scheduler_options.py
new file mode 100644
index 000000000..7ecb772ba
--- /dev/null
+++ b/cinder/tests/scheduler/test_scheduler_options.py
@@ -0,0 +1,138 @@
+# 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)
diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py
index 1ae2e95c0..2450f2b67 100644
--- a/cinder/volume/driver.py
+++ b/cinder/volume/driver.py
@@ -30,6 +30,7 @@ from cinder import flags
from cinder.image import image_utils
from cinder.openstack.common import cfg
from cinder.openstack.common import log as logging
+from cinder.openstack.common import timeutils
from cinder import utils
from cinder.volume import iscsi
@@ -61,7 +62,11 @@ volume_opts = [
help='use this ip for iscsi'),
cfg.IntOpt('iscsi_port',
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.register_opts(volume_opts)
@@ -73,6 +78,7 @@ class VolumeDriver(object):
# NOTE(vish): db is set by Manager
self.db = None
self.set_execute(execute)
+ self._stats = {}
def set_execute(self, execute):
self._execute = execute
@@ -619,6 +625,49 @@ class ISCSIDriver(VolumeDriver):
def terminate_connection(self, volume, connector, **kwargs):
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):
"""Fetch the image from image_service and write it to the volume."""
image_utils.fetch_to_raw(context,
diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py
index 7400f62a7..3567c985d 100644
--- a/cinder/volume/manager.py
+++ b/cinder/volume/manager.py
@@ -61,9 +61,7 @@ volume_manager_opts = [
cfg.StrOpt('volume_driver',
default='cinder.volume.driver.ISCSIDriver',
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.register_opts(volume_manager_opts)
@@ -103,7 +101,7 @@ MAPPING = {
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
- RPC_API_VERSION = '1.1'
+ RPC_API_VERSION = '1.2'
def __init__(self, volume_driver=None, *args, **kwargs):
"""Load the driver from the one specified in args, or from flags."""
@@ -120,7 +118,6 @@ class VolumeManager(manager.SchedulerDependentManager):
# NOTE(vish): Implementation specific db handling is done
# by the driver.
self.driver.db = self.db
- self._last_volume_stats = []
def init_host(self):
"""Do any initialization that needs to be run if this is a
@@ -144,6 +141,9 @@ class VolumeManager(manager.SchedulerDependentManager):
LOG.info(_('Resuming delete on volume: %s') % 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,
image_id=None, source_volid=None):
"""Creates and exports the volume."""
@@ -490,33 +490,19 @@ class VolumeManager(manager.SchedulerDependentManager):
volume_ref = self.db.volume_get(context, volume_id)
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
def _report_driver_status(self, context):
+ LOG.info(_("Updating volume status"))
volume_stats = self.driver.get_volume_stats(refresh=True)
if volume_stats:
- LOG.info(_("Checking volume capabilities"))
-
- if self._volume_stats_changed(self._last_volume_stats,
- volume_stats):
- LOG.info(_("New capabilities found: %s"), volume_stats)
- 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)
+ # This will grab info about the host and queue it
+ # to be sent to the Schedulers.
+ self.update_service_capabilities(volume_stats)
+
+ def publish_service_capabilities(self, context):
+ """ Collect driver status and then publish """
+ self._report_driver_status(context)
+ self._publish_service_capabilities(context)
def _reset_stats(self):
LOG.info(_("Clear capabilities"))
diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py
index 54bfabd03..0f7621016 100644
--- a/cinder/volume/rpcapi.py
+++ b/cinder/volume/rpcapi.py
@@ -34,6 +34,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
1.0 - Initial version.
1.1 - Adds clone volume option to create_volume.
+ 1.2 - Add publish_service_capabilities() method.
'''
BASE_RPC_API_VERSION = '1.0'
@@ -114,3 +115,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
topic=rpc.queue_get_for(ctxt,
self.topic,
volume['host']))
+
+ def publish_service_capabilities(self, ctxt):
+ self.fanout_cast(ctxt, self.make_msg('publish_service_capabilities'),
+ version='1.2')
diff --git a/openstack-common.conf b/openstack-common.conf
index 3f8044fcb..93efc6ff0 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -1,7 +1,7 @@
[DEFAULT]
# 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
+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
# The base module to hold the copy of openstack.common
base=cinder
diff --git a/setup.py b/setup.py
index 55f69c5fa..6b7cccf3a 100644
--- a/setup.py
+++ b/setup.py
@@ -23,6 +23,23 @@ from cinder import version
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(
name='cinder',
version=version.canonical_version_string(),
@@ -43,6 +60,10 @@ setuptools.setup(
cmdclass=common_setup.get_cmdclass(),
packages=setuptools.find_packages(exclude=['bin', 'smoketests']),
install_requires=requires,
+ entry_points={
+ 'cinder.scheduler.filters': filters,
+ 'cinder.scheduler.weights': weights,
+ },
include_package_data=True,
test_suite='nose.collector',
setup_requires=['setuptools_git>=0.4'],
diff --git a/tools/pip-requires b/tools/pip-requires
index 9ef13a00d..556a7012d 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -12,6 +12,7 @@ greenlet>=0.3.1
PasteDeploy==1.5.0
paste
sqlalchemy-migrate>=0.7.2
+stevedore>=0.8.0
suds==0.4
paramiko
Babel>=0.9.6