summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-05-28 20:38:05 +0000
committerGerrit Code Review <review@openstack.org>2013-05-28 20:38:05 +0000
commita3e6133628310b0f41fad7271b048e30c0401e62 (patch)
treecae4c883ec92e867c99d6248f191afdfad5aa0ab
parente18bb9f80bef34846913e667fc77e4d1132480ec (diff)
parent7a5ed3e76766596e45716d0fc05ff1e6e9199213 (diff)
downloadnova-a3e6133628310b0f41fad7271b048e30c0401e62.tar.gz
nova-a3e6133628310b0f41fad7271b048e30c0401e62.tar.xz
nova-a3e6133628310b0f41fad7271b048e30c0401e62.zip
Merge "Cells: Add filtering and weight support"
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/cells/filters/__init__.py62
-rw-r--r--nova/cells/filters/target_cell.py68
-rw-r--r--nova/cells/scheduler.py99
-rw-r--r--nova/cells/weights/__init__.py43
-rw-r--r--nova/cells/weights/ram_by_instance_type.py54
-rw-r--r--nova/cells/weights/weight_offset.py33
-rw-r--r--nova/filters.py14
-rw-r--r--nova/tests/cells/test_cells_filters.py121
-rw-r--r--nova/tests/cells/test_cells_scheduler.py182
-rw-r--r--nova/tests/cells/test_cells_weights.py165
-rw-r--r--nova/tests/fake_policy.py2
-rw-r--r--nova/tests/test_filters.py33
13 files changed, 846 insertions, 31 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index d6524b6a2..b09f5f473 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -3,6 +3,7 @@
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
"default": "rule:admin_or_owner",
+ "cells_scheduler_filter:TargetCellFilter": "is_admin:True",
"compute:create": "",
"compute:create:attach_network": "",
diff --git a/nova/cells/filters/__init__.py b/nova/cells/filters/__init__.py
new file mode 100644
index 000000000..f9b6d7a15
--- /dev/null
+++ b/nova/cells/filters/__init__.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+
+"""
+Cell scheduler filters
+"""
+
+from nova import filters
+from nova.openstack.common import log as logging
+from nova import policy
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseCellFilter(filters.BaseFilter):
+ """Base class for cell filters."""
+
+ def authorized(self, ctxt):
+ """Return whether or not the context is authorized for this filter
+ based on policy.
+ The policy action is "cells_scheduler_filter:<name>" where <name>
+ is the name of the filter class.
+ """
+ name = 'cells_scheduler_filter:' + self.__class__.__name__
+ target = {'project_id': ctxt.project_id,
+ 'user_id': ctxt.user_id}
+ return policy.enforce(ctxt, name, target, do_raise=False)
+
+ def _filter_one(self, cell, filter_properties):
+ return self.cell_passes(cell, filter_properties)
+
+ def cell_passes(self, cell, filter_properties):
+ """Return True if the CellState passes the filter, otherwise False.
+ Override this in a subclass.
+ """
+ raise NotImplementedError()
+
+
+class CellFilterHandler(filters.BaseFilterHandler):
+ def __init__(self):
+ super(CellFilterHandler, self).__init__(BaseCellFilter)
+
+
+def all_filters():
+ """Return a list of filter classes found in this directory.
+
+ This method is used as the default for available scheduler filters
+ and should return a list of all filter classes available.
+ """
+ return CellFilterHandler().get_all_classes()
diff --git a/nova/cells/filters/target_cell.py b/nova/cells/filters/target_cell.py
new file mode 100644
index 000000000..ab5adb1e8
--- /dev/null
+++ b/nova/cells/filters/target_cell.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+
+"""
+Target cell filter.
+
+A scheduler hint of 'target_cell' with a value of a full cell name may be
+specified to route a build to a particular cell. No error handling is
+done as there's no way to know whether the full path is a valid.
+"""
+
+from nova.cells import filters
+from nova.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class TargetCellFilter(filters.BaseCellFilter):
+ """Target cell filter. Works by specifying a scheduler hint of
+ 'target_cell'. The value should be the full cell path.
+ """
+
+ def filter_all(self, cells, filter_properties):
+ """Override filter_all() which operates on the full list
+ of cells...
+ """
+ scheduler_hints = filter_properties.get('scheduler_hints')
+ if not scheduler_hints:
+ return cells
+
+ # This filter only makes sense at the top level, as a full
+ # cell name is specified. So we pop 'target_cell' out of the
+ # hints dict.
+ cell_name = scheduler_hints.pop('target_cell', None)
+ if not cell_name:
+ return cells
+
+ # This authorization is after popping off target_cell, so
+ # that in case this fails, 'target_cell' is not left in the
+ # dict when child cells go to schedule.
+ if not self.authorized(filter_properties['context']):
+ # No filtering, if not authorized.
+ return cells
+
+ LOG.info(_("Forcing direct route to %(cell_name)s because "
+ "of 'target_cell' scheduler hint"),
+ {'cell_name': cell_name})
+
+ scheduler = filter_properties['scheduler']
+ if cell_name == filter_properties['routing_path']:
+ return [scheduler.state_manager.get_my_state()]
+ ctxt = filter_properties['context']
+ scheduler.msg_runner.schedule_run_instance(ctxt, cell_name,
+ filter_properties['host_sched_kwargs'])
+ # Returning None means to skip further scheduling, because we
+ # handled it.
diff --git a/nova/cells/scheduler.py b/nova/cells/scheduler.py
index 18389dbc5..c95498fa0 100644
--- a/nova/cells/scheduler.py
+++ b/nova/cells/scheduler.py
@@ -16,11 +16,13 @@
"""
Cells Scheduler
"""
-import random
+import copy
import time
from oslo.config import cfg
+from nova.cells import filters
+from nova.cells import weights
from nova import compute
from nova.compute import instance_actions
from nova.compute import utils as compute_utils
@@ -31,6 +33,16 @@ from nova.openstack.common import log as logging
from nova.scheduler import rpcapi as scheduler_rpcapi
cell_scheduler_opts = [
+ cfg.ListOpt('scheduler_filter_classes',
+ default=['nova.cells.filters.all_filters'],
+ help='Filter classes the cells scheduler should use. '
+ 'An entry of "nova.cells.filters.all_filters"'
+ 'maps to all cells filters included with nova.'),
+ cfg.ListOpt('scheduler_weight_classes',
+ default=['nova.cells.weights.all_weighers'],
+ help='Weigher classes the cells scheduler should use. '
+ 'An entry of "nova.cells.weights.all_weighers"'
+ 'maps to all cell weighers included with nova.'),
cfg.IntOpt('scheduler_retries',
default=10,
help='How many retries when no cells are available.'),
@@ -55,6 +67,12 @@ class CellsScheduler(base.Base):
self.state_manager = msg_runner.state_manager
self.compute_api = compute.API()
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
+ self.filter_handler = filters.CellFilterHandler()
+ self.filter_classes = self.filter_handler.get_matching_classes(
+ CONF.cells.scheduler_filter_classes)
+ self.weight_handler = weights.CellWeightHandler()
+ self.weigher_classes = self.weight_handler.get_matching_classes(
+ CONF.cells.scheduler_weight_classes)
def _create_instances_here(self, ctxt, request_spec):
instance_values = request_spec['instance_properties']
@@ -79,11 +97,11 @@ class CellsScheduler(base.Base):
self.db.action_start(ctxt, action)
def _get_possible_cells(self):
- cells = set(self.state_manager.get_child_cells())
+ cells = self.state_manager.get_child_cells()
our_cell = self.state_manager.get_my_state()
# Include our cell in the list, if we have any capacity info
if not cells or our_cell.capacities:
- cells.add(our_cell)
+ cells.append(our_cell)
return cells
def _run_instance(self, message, host_sched_kwargs):
@@ -91,33 +109,66 @@ class CellsScheduler(base.Base):
to try, raise exception.NoCellsAvailable
"""
ctxt = message.ctxt
+ routing_path = message.routing_path
request_spec = host_sched_kwargs['request_spec']
- # The message we might forward to a child cell
+ LOG.debug(_("Scheduling with routing_path=%(routing_path)s"),
+ {'routing_path': routing_path})
+
+ filter_properties = copy.copy(host_sched_kwargs['filter_properties'])
+ filter_properties.update({'context': ctxt,
+ 'scheduler': self,
+ 'routing_path': routing_path,
+ 'host_sched_kwargs': host_sched_kwargs,
+ 'request_spec': request_spec})
+
cells = self._get_possible_cells()
+ cells = self.filter_handler.get_filtered_objects(self.filter_classes,
+ cells,
+ filter_properties)
+ # NOTE(comstud): I know this reads weird, but the 'if's are nested
+ # this way to optimize for the common case where 'cells' is a list
+ # containing at least 1 entry.
if not cells:
+ if cells is None:
+ # None means to bypass further scheduling as a filter
+ # took care of everything.
+ return
raise exception.NoCellsAvailable()
- cells = list(cells)
- # Random selection for now
- random.shuffle(cells)
- target_cell = cells[0]
-
- LOG.debug(_("Scheduling with routing_path=%(routing_path)s"),
- {'routing_path': message.routing_path})
-
- if target_cell.is_me:
- # Need to create instance DB entries as the host scheduler
- # expects that the instance(s) already exists.
- self._create_instances_here(ctxt, request_spec)
- # Need to record the create action in the db as the scheduler
- # expects it to already exist.
- self._create_action_here(ctxt, request_spec['instance_uuids'])
- self.scheduler_rpcapi.run_instance(ctxt,
- **host_sched_kwargs)
- return
- self.msg_runner.schedule_run_instance(ctxt, target_cell,
- host_sched_kwargs)
+ weighted_cells = self.weight_handler.get_weighed_objects(
+ self.weigher_classes, cells, filter_properties)
+ LOG.debug(_("Weighted cells: %(weighted_cells)s"),
+ {'weighted_cells': weighted_cells})
+
+ # Keep trying until one works
+ for weighted_cell in weighted_cells:
+ cell = weighted_cell.obj
+ try:
+ if cell.is_me:
+ # Need to create instance DB entry as scheduler
+ # thinks it's already created... At least how things
+ # currently work.
+ self._create_instances_here(ctxt, request_spec)
+ # Need to record the create action in the db as the
+ # scheduler expects it to already exist.
+ self._create_action_here(
+ ctxt, request_spec['instance_uuids'])
+ self.scheduler_rpcapi.run_instance(ctxt,
+ **host_sched_kwargs)
+ return
+ # Forward request to cell
+ self.msg_runner.schedule_run_instance(ctxt, cell,
+ host_sched_kwargs)
+ return
+ except Exception:
+ LOG.exception(_("Couldn't communicate with cell '%s'") %
+ cell.name)
+ # FIXME(comstud): Would be nice to kick this back up so that
+ # the parent cell could retry, if we had a parent.
+ msg = _("Couldn't communicate with any cells")
+ LOG.error(msg)
+ raise exception.NoCellsAvailable()
def run_instance(self, message, host_sched_kwargs):
"""Pick a cell where we should create a new instance."""
diff --git a/nova/cells/weights/__init__.py b/nova/cells/weights/__init__.py
new file mode 100644
index 000000000..202a6a31a
--- /dev/null
+++ b/nova/cells/weights/__init__.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+
+"""
+Cell Scheduler weights
+"""
+
+from nova import weights
+
+
+class WeightedCell(weights.WeighedObject):
+ def __repr__(self):
+ return "WeightedCell [cell: %s, weight: %s]" % (
+ self.obj.name, self.weight)
+
+
+class BaseCellWeigher(weights.BaseWeigher):
+ """Base class for cell weights."""
+ pass
+
+
+class CellWeightHandler(weights.BaseWeightHandler):
+ object_class = WeightedCell
+
+ def __init__(self):
+ super(CellWeightHandler, self).__init__(BaseCellWeigher)
+
+
+def all_weighers():
+ """Return a list of weight plugin classes found in this directory."""
+ return CellWeightHandler().get_all_classes()
diff --git a/nova/cells/weights/ram_by_instance_type.py b/nova/cells/weights/ram_by_instance_type.py
new file mode 100644
index 000000000..1a1d164d2
--- /dev/null
+++ b/nova/cells/weights/ram_by_instance_type.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+
+"""
+Weigh cells by memory needed in a way that spreads instances.
+"""
+from oslo.config import cfg
+
+from nova.cells import weights
+
+ram_weigher_opts = [
+ cfg.FloatOpt('ram_weight_multiplier',
+ default=10.0,
+ help='Multiplier used for weighing ram. Negative '
+ 'numbers mean to stack vs spread.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(ram_weigher_opts, group='cells')
+
+
+class RamByInstanceTypeWeigher(weights.BaseCellWeigher):
+ """Weigh cells by instance_type requested."""
+
+ def _weight_multiplier(self):
+ return CONF.cells.ram_weight_multiplier
+
+ def _weigh_object(self, cell, weight_properties):
+ """
+ Use the 'ram_free' for a particular instance_type advertised from a
+ child cell's capacity to compute a weight. We want to direct the
+ build to a cell with a higher capacity. Since higher weights win,
+ we just return the number of units available for the instance_type.
+ """
+ request_spec = weight_properties['request_spec']
+ instance_type = request_spec['instance_type']
+ memory_needed = instance_type['memory_mb']
+
+ ram_free = cell.capacities.get('ram_free', {})
+ units_by_mb = ram_free.get('units_by_mb', {})
+
+ return units_by_mb.get(str(memory_needed), 0)
diff --git a/nova/cells/weights/weight_offset.py b/nova/cells/weights/weight_offset.py
new file mode 100644
index 000000000..14348ee20
--- /dev/null
+++ b/nova/cells/weights/weight_offset.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+
+"""
+Weigh cells by their weight_offset in the DB. Cells with higher
+weight_offsets in the DB will be preferred.
+"""
+
+from nova.cells import weights
+
+
+class WeightOffsetWeigher(weights.BaseCellWeigher):
+ """
+ Weight cell by weight_offset db field.
+ Originally designed so you can set a default cell by putting
+ its weight_offset to 999999999999999 (highest weight wins)
+ """
+
+ def _weigh_object(self, cell, weight_properties):
+ """Returns whatever was in the DB for weight_offset."""
+ return cell.db_info.get('weight_offset', 0)
diff --git a/nova/filters.py b/nova/filters.py
index 18e3a7d66..4b7f9ff10 100644
--- a/nova/filters.py
+++ b/nova/filters.py
@@ -54,8 +54,14 @@ class BaseFilterHandler(loadables.BaseLoader):
list_objs = list(objs)
LOG.debug("Starting with %d host(s)", len(list_objs))
for filter_cls in filter_classes:
- list_objs = list(filter_cls().filter_all(list_objs,
- filter_properties))
- LOG.debug("Filter %s returned %d host(s)",
- filter_cls.__name__, len(list_objs))
+ cls_name = filter_cls.__name__
+ objs = filter_cls().filter_all(list_objs,
+ filter_properties)
+ if objs is None:
+ LOG.debug("Filter %(cls_name)s says to stop filtering",
+ {'cls_name': cls_name})
+ return
+ list_objs = list(objs)
+ LOG.debug("Filter %(cls_name)s returned %(obj_len)d host(s)",
+ {'cls_name': cls_name, 'obj_len': len(list_objs)})
return list_objs
diff --git a/nova/tests/cells/test_cells_filters.py b/nova/tests/cells/test_cells_filters.py
new file mode 100644
index 000000000..e11e6c640
--- /dev/null
+++ b/nova/tests/cells/test_cells_filters.py
@@ -0,0 +1,121 @@
+# Copyright (c) 2012-2013 Rackspace Hosting
+# 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.
+"""
+Unit Tests for cells scheduler filters.
+"""
+
+from nova.cells import filters
+from nova import context
+from nova import test
+from nova.tests.cells import fakes
+
+
+class FiltersTestCase(test.TestCase):
+ """Makes sure the proper filters are in the directory."""
+
+ def test_all_filters(self):
+ filter_classes = filters.all_filters()
+ class_names = [cls.__name__ for cls in filter_classes]
+ self.assertIn("TargetCellFilter", class_names)
+
+
+class _FilterTestClass(test.TestCase):
+ """Base class for testing individual filter plugins."""
+ filter_cls_name = None
+
+ def setUp(self):
+ super(_FilterTestClass, self).setUp()
+ fakes.init(self)
+ self.msg_runner = fakes.get_message_runner('api-cell')
+ self.scheduler = self.msg_runner.scheduler
+ self.my_cell_state = self.msg_runner.state_manager.get_my_state()
+ self.filter_handler = filters.CellFilterHandler()
+ self.filter_classes = self.filter_handler.get_matching_classes(
+ [self.filter_cls_name])
+ self.context = context.RequestContext('fake', 'fake',
+ is_admin=True)
+
+ def _filter_cells(self, cells, filter_properties):
+ return self.filter_handler.get_filtered_objects(self.filter_classes,
+ cells,
+ filter_properties)
+
+
+class TestTargetCellFilter(_FilterTestClass):
+ filter_cls_name = 'nova.cells.filters.target_cell.TargetCellFilter'
+
+ def test_missing_scheduler_hints(self):
+ cells = [1, 2, 3]
+ # No filtering
+ filter_props = {'context': self.context}
+ self.assertEqual(cells, self._filter_cells(cells, filter_props))
+
+ def test_no_target_cell_hint(self):
+ cells = [1, 2, 3]
+ filter_props = {'scheduler_hints': {},
+ 'context': self.context}
+ # No filtering
+ self.assertEqual(cells, self._filter_cells(cells, filter_props))
+
+ def test_target_cell_specified_me(self):
+ cells = [1, 2, 3]
+ target_cell = 'fake!cell!path'
+ current_cell = 'fake!cell!path'
+ filter_props = {'scheduler_hints': {'target_cell': target_cell},
+ 'routing_path': current_cell,
+ 'scheduler': self.scheduler,
+ 'context': self.context}
+ # Only myself in the list.
+ self.assertEqual([self.my_cell_state],
+ self._filter_cells(cells, filter_props))
+
+ def test_target_cell_specified_me_but_not_admin(self):
+ ctxt = context.RequestContext('fake', 'fake')
+ cells = [1, 2, 3]
+ target_cell = 'fake!cell!path'
+ current_cell = 'fake!cell!path'
+ filter_props = {'scheduler_hints': {'target_cell': target_cell},
+ 'routing_path': current_cell,
+ 'scheduler': self.scheduler,
+ 'context': ctxt}
+ # No filtering, because not an admin.
+ self.assertEqual(cells, self._filter_cells(cells, filter_props))
+
+ def test_target_cell_specified_not_me(self):
+ info = {}
+
+ def _fake_sched_run_instance(ctxt, cell, sched_kwargs):
+ info['ctxt'] = ctxt
+ info['cell'] = cell
+ info['sched_kwargs'] = sched_kwargs
+
+ self.stubs.Set(self.msg_runner, 'schedule_run_instance',
+ _fake_sched_run_instance)
+ cells = [1, 2, 3]
+ target_cell = 'fake!cell!path'
+ current_cell = 'not!the!same'
+ filter_props = {'scheduler_hints': {'target_cell': target_cell},
+ 'routing_path': current_cell,
+ 'scheduler': self.scheduler,
+ 'context': self.context,
+ 'host_sched_kwargs': 'meow'}
+ # None is returned to bypass further scheduling.
+ self.assertEqual(None,
+ self._filter_cells(cells, filter_props))
+ # The filter should have re-scheduled to the child cell itself.
+ expected_info = {'ctxt': self.context,
+ 'cell': 'fake!cell!path',
+ 'sched_kwargs': 'meow'}
+ self.assertEqual(expected_info, info)
diff --git a/nova/tests/cells/test_cells_scheduler.py b/nova/tests/cells/test_cells_scheduler.py
index c9e626385..c8f90619e 100644
--- a/nova/tests/cells/test_cells_scheduler.py
+++ b/nova/tests/cells/test_cells_scheduler.py
@@ -19,6 +19,8 @@ import time
from oslo.config import cfg
+from nova.cells import filters
+from nova.cells import weights
from nova.compute import vm_states
from nova import context
from nova import db
@@ -29,6 +31,26 @@ from nova.tests.cells import fakes
CONF = cfg.CONF
CONF.import_opt('scheduler_retries', 'nova.cells.scheduler', group='cells')
+CONF.import_opt('scheduler_filter_classes', 'nova.cells.scheduler',
+ group='cells')
+CONF.import_opt('scheduler_weight_classes', 'nova.cells.scheduler',
+ group='cells')
+
+
+class FakeFilterClass1(filters.BaseCellFilter):
+ pass
+
+
+class FakeFilterClass2(filters.BaseCellFilter):
+ pass
+
+
+class FakeWeightClass1(weights.BaseCellWeigher):
+ pass
+
+
+class FakeWeightClass2(weights.BaseCellWeigher):
+ pass
class CellsSchedulerTestCase(test.TestCase):
@@ -36,6 +58,11 @@ class CellsSchedulerTestCase(test.TestCase):
def setUp(self):
super(CellsSchedulerTestCase, self).setUp()
+ self.flags(scheduler_filter_classes=[], scheduler_weight_classes=[],
+ group='cells')
+ self._init_cells_scheduler()
+
+ def _init_cells_scheduler(self):
fakes.init(self)
self.msg_runner = fakes.get_message_runner('api-cell')
self.scheduler = self.msg_runner.scheduler
@@ -109,7 +136,8 @@ class CellsSchedulerTestCase(test.TestCase):
self.stubs.Set(self.msg_runner, 'schedule_run_instance',
msg_runner_schedule_run_instance)
- host_sched_kwargs = {'request_spec': self.request_spec}
+ host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {}}
self.msg_runner.schedule_run_instance(self.ctxt,
self.my_cell_state, host_sched_kwargs)
@@ -138,6 +166,7 @@ class CellsSchedulerTestCase(test.TestCase):
'run_instance', fake_rpc_run_instance)
host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {},
'other': 'stuff'}
self.msg_runner.schedule_run_instance(self.ctxt,
self.my_cell_state, host_sched_kwargs)
@@ -149,7 +178,8 @@ class CellsSchedulerTestCase(test.TestCase):
def test_run_instance_retries_when_no_cells_avail(self):
self.flags(scheduler_retries=7, group='cells')
- host_sched_kwargs = {'request_spec': self.request_spec}
+ host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {}}
call_info = {'num_tries': 0, 'errored_uuids': []}
@@ -177,7 +207,8 @@ class CellsSchedulerTestCase(test.TestCase):
def test_run_instance_on_random_exception(self):
self.flags(scheduler_retries=7, group='cells')
- host_sched_kwargs = {'request_spec': self.request_spec}
+ host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {}}
call_info = {'num_tries': 0,
'errored_uuids1': [],
@@ -206,3 +237,148 @@ class CellsSchedulerTestCase(test.TestCase):
self.assertEqual(1, call_info['num_tries'])
self.assertEqual(self.instance_uuids, call_info['errored_uuids1'])
self.assertEqual(self.instance_uuids, call_info['errored_uuids2'])
+
+ def test_cells_filter_args_correct(self):
+ # Re-init our fakes with some filters.
+ our_path = 'nova.tests.cells.test_cells_scheduler'
+ cls_names = [our_path + '.' + 'FakeFilterClass1',
+ our_path + '.' + 'FakeFilterClass2']
+ self.flags(scheduler_filter_classes=cls_names, group='cells')
+ self._init_cells_scheduler()
+
+ # Make sure there's no child cells so that we will be
+ # selected. Makes stubbing easier.
+ self.state_manager.child_cells = {}
+
+ call_info = {}
+
+ def fake_create_instances_here(ctxt, request_spec):
+ call_info['ctxt'] = ctxt
+ call_info['request_spec'] = request_spec
+
+ def fake_rpc_run_instance(ctxt, **host_sched_kwargs):
+ call_info['host_sched_kwargs'] = host_sched_kwargs
+
+ def fake_get_filtered_objs(filter_classes, cells, filt_properties):
+ call_info['filt_classes'] = filter_classes
+ call_info['filt_cells'] = cells
+ call_info['filt_props'] = filt_properties
+ return cells
+
+ self.stubs.Set(self.scheduler, '_create_instances_here',
+ fake_create_instances_here)
+ self.stubs.Set(self.scheduler.scheduler_rpcapi,
+ 'run_instance', fake_rpc_run_instance)
+ filter_handler = self.scheduler.filter_handler
+ self.stubs.Set(filter_handler, 'get_filtered_objects',
+ fake_get_filtered_objs)
+
+ host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {},
+ 'other': 'stuff'}
+
+ self.msg_runner.schedule_run_instance(self.ctxt,
+ self.my_cell_state, host_sched_kwargs)
+ # Our cell was selected.
+ self.assertEqual(self.ctxt, call_info['ctxt'])
+ self.assertEqual(self.request_spec, call_info['request_spec'])
+ self.assertEqual(host_sched_kwargs, call_info['host_sched_kwargs'])
+ # Filter args are correct
+ expected_filt_props = {'context': self.ctxt,
+ 'scheduler': self.scheduler,
+ 'routing_path': self.my_cell_state.name,
+ 'host_sched_kwargs': host_sched_kwargs,
+ 'request_spec': self.request_spec}
+ self.assertEqual(expected_filt_props, call_info['filt_props'])
+ self.assertEqual([FakeFilterClass1, FakeFilterClass2],
+ call_info['filt_classes'])
+ self.assertEqual([self.my_cell_state], call_info['filt_cells'])
+
+ def test_cells_filter_returning_none(self):
+ # Re-init our fakes with some filters.
+ our_path = 'nova.tests.cells.test_cells_scheduler'
+ cls_names = [our_path + '.' + 'FakeFilterClass1',
+ our_path + '.' + 'FakeFilterClass2']
+ self.flags(scheduler_filter_classes=cls_names, group='cells')
+ self._init_cells_scheduler()
+
+ # Make sure there's no child cells so that we will be
+ # selected. Makes stubbing easier.
+ self.state_manager.child_cells = {}
+
+ call_info = {'scheduled': False}
+
+ def fake_create_instances_here(ctxt, request_spec):
+ # Should not be called
+ call_info['scheduled'] = True
+
+ def fake_get_filtered_objs(filter_classes, cells, filt_properties):
+ # Should cause scheduling to be skipped. Means that the
+ # filter did it.
+ return None
+
+ self.stubs.Set(self.scheduler, '_create_instances_here',
+ fake_create_instances_here)
+ filter_handler = self.scheduler.filter_handler
+ self.stubs.Set(filter_handler, 'get_filtered_objects',
+ fake_get_filtered_objs)
+
+ self.msg_runner.schedule_run_instance(self.ctxt,
+ self.my_cell_state, {})
+ self.assertFalse(call_info['scheduled'])
+
+ def test_cells_weight_args_correct(self):
+ # Re-init our fakes with some filters.
+ our_path = 'nova.tests.cells.test_cells_scheduler'
+ cls_names = [our_path + '.' + 'FakeWeightClass1',
+ our_path + '.' + 'FakeWeightClass2']
+ self.flags(scheduler_weight_classes=cls_names, group='cells')
+ self._init_cells_scheduler()
+
+ # Make sure there's no child cells so that we will be
+ # selected. Makes stubbing easier.
+ self.state_manager.child_cells = {}
+
+ call_info = {}
+
+ def fake_create_instances_here(ctxt, request_spec):
+ call_info['ctxt'] = ctxt
+ call_info['request_spec'] = request_spec
+
+ def fake_rpc_run_instance(ctxt, **host_sched_kwargs):
+ call_info['host_sched_kwargs'] = host_sched_kwargs
+
+ def fake_get_weighed_objs(weight_classes, cells, filt_properties):
+ call_info['weight_classes'] = weight_classes
+ call_info['weight_cells'] = cells
+ call_info['weight_props'] = filt_properties
+ return [weights.WeightedCell(cells[0], 0.0)]
+
+ self.stubs.Set(self.scheduler, '_create_instances_here',
+ fake_create_instances_here)
+ self.stubs.Set(self.scheduler.scheduler_rpcapi,
+ 'run_instance', fake_rpc_run_instance)
+ weight_handler = self.scheduler.weight_handler
+ self.stubs.Set(weight_handler, 'get_weighed_objects',
+ fake_get_weighed_objs)
+
+ host_sched_kwargs = {'request_spec': self.request_spec,
+ 'filter_properties': {},
+ 'other': 'stuff'}
+
+ self.msg_runner.schedule_run_instance(self.ctxt,
+ self.my_cell_state, host_sched_kwargs)
+ # Our cell was selected.
+ self.assertEqual(self.ctxt, call_info['ctxt'])
+ self.assertEqual(self.request_spec, call_info['request_spec'])
+ self.assertEqual(host_sched_kwargs, call_info['host_sched_kwargs'])
+ # Weight args are correct
+ expected_filt_props = {'context': self.ctxt,
+ 'scheduler': self.scheduler,
+ 'routing_path': self.my_cell_state.name,
+ 'host_sched_kwargs': host_sched_kwargs,
+ 'request_spec': self.request_spec}
+ self.assertEqual(expected_filt_props, call_info['weight_props'])
+ self.assertEqual([FakeWeightClass1, FakeWeightClass2],
+ call_info['weight_classes'])
+ self.assertEqual([self.my_cell_state], call_info['weight_cells'])
diff --git a/nova/tests/cells/test_cells_weights.py b/nova/tests/cells/test_cells_weights.py
new file mode 100644
index 000000000..ca01e9939
--- /dev/null
+++ b/nova/tests/cells/test_cells_weights.py
@@ -0,0 +1,165 @@
+# 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.
+"""
+Unit Tests for testing the cells weight algorithms.
+
+Cells with higher weights should be given priority for new builds.
+"""
+
+from nova.cells import state
+from nova.cells import weights
+from nova import test
+
+
+class FakeCellState(state.CellState):
+ def __init__(self, cell_name):
+ super(FakeCellState, self).__init__(cell_name)
+ self.capacities['ram_free'] = {'total_mb': 0,
+ 'units_by_mb': {}}
+ self.db_info = {}
+
+ def _update_ram_free(self, *args):
+ ram_free = self.capacities['ram_free']
+ for ram_size, units in args:
+ ram_free['total_mb'] += units * ram_size
+ ram_free['units_by_mb'][str(ram_size)] = units
+
+
+def _get_fake_cells():
+
+ cell1 = FakeCellState('cell1')
+ cell1._update_ram_free((512, 1), (1024, 4), (2048, 3))
+ cell1.db_info['weight_offset'] = -200.0
+ cell2 = FakeCellState('cell2')
+ cell2._update_ram_free((512, 2), (1024, 3), (2048, 4))
+ cell2.db_info['weight_offset'] = -200.1
+ cell3 = FakeCellState('cell3')
+ cell3._update_ram_free((512, 3), (1024, 2), (2048, 1))
+ cell3.db_info['weight_offset'] = 400.0
+ cell4 = FakeCellState('cell4')
+ cell4._update_ram_free((512, 4), (1024, 1), (2048, 2))
+ cell4.db_info['weight_offset'] = 300.0
+
+ return [cell1, cell2, cell3, cell4]
+
+
+class CellsWeightsTestCase(test.TestCase):
+ """Makes sure the proper weighers are in the directory."""
+
+ def test_all_weighers(self):
+ weighers = weights.all_weighers()
+ # Check at least a couple that we expect are there
+ self.assertTrue(len(weighers) >= 2)
+ class_names = [cls.__name__ for cls in weighers]
+ self.assertIn('WeightOffsetWeigher', class_names)
+ self.assert_('RamByInstanceTypeWeigher', class_names)
+
+
+class _WeigherTestClass(test.TestCase):
+ """Base class for testing individual weigher plugins."""
+ weigher_cls_name = None
+
+ def setUp(self):
+ super(_WeigherTestClass, self).setUp()
+ self.weight_handler = weights.CellWeightHandler()
+ self.weight_classes = self.weight_handler.get_matching_classes(
+ [self.weigher_cls_name])
+
+ def _get_weighed_cells(self, cells, weight_properties):
+ return self.weight_handler.get_weighed_objects(self.weight_classes,
+ cells, weight_properties)
+
+
+class RAMByInstanceTypeWeigherTestClass(_WeigherTestClass):
+
+ weigher_cls_name = ('nova.cells.weights.ram_by_instance_type.'
+ 'RamByInstanceTypeWeigher')
+
+ def test_default_spreading(self):
+ """Test that cells with more ram available return a higher weight."""
+ cells = _get_fake_cells()
+ # Simulate building a new 512MB instance.
+ instance_type = {'memory_mb': 512}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[3], cells[2], cells[1], cells[0]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+ # Simulate building a new 1024MB instance.
+ instance_type = {'memory_mb': 1024}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[0], cells[1], cells[2], cells[3]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+ # Simulate building a new 2048MB instance.
+ instance_type = {'memory_mb': 2048}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[1], cells[0], cells[3], cells[2]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+ def test_negative_multiplier(self):
+ """Test that cells with less ram available return a higher weight."""
+ self.flags(ram_weight_multiplier=-1.0, group='cells')
+ cells = _get_fake_cells()
+ # Simulate building a new 512MB instance.
+ instance_type = {'memory_mb': 512}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[0], cells[1], cells[2], cells[3]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+ # Simulate building a new 1024MB instance.
+ instance_type = {'memory_mb': 1024}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[3], cells[2], cells[1], cells[0]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+ # Simulate building a new 2048MB instance.
+ instance_type = {'memory_mb': 2048}
+ weight_properties = {'request_spec': {'instance_type': instance_type}}
+ weighed_cells = self._get_weighed_cells(cells, weight_properties)
+ self.assertEqual(len(weighed_cells), 4)
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ expected_cells = [cells[2], cells[3], cells[0], cells[1]]
+ self.assertEqual(expected_cells, resulting_cells)
+
+
+class WeightOffsetWeigherTestClass(_WeigherTestClass):
+ """Test the RAMWeigher class."""
+ weigher_cls_name = 'nova.cells.weights.weight_offset.WeightOffsetWeigher'
+
+ def test_weight_offset(self):
+ """Test that cells with higher weight_offsets return higher
+ weights.
+ """
+ cells = _get_fake_cells()
+ weighed_cells = self._get_weighed_cells(cells, {})
+ self.assertEqual(len(weighed_cells), 4)
+ expected_cells = [cells[2], cells[3], cells[0], cells[1]]
+ resulting_cells = [weighed_cell.obj for weighed_cell in weighed_cells]
+ self.assertEqual(expected_cells, resulting_cells)
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index b30793ac4..b09944878 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -19,6 +19,8 @@ policy_data = """
{
"admin_api": "role:admin",
+ "cells_scheduler_filter:TargetCellFilter": "is_admin:True",
+
"context_is_admin": "role:admin or role:administrator",
"compute:create": "",
"compute:create:attach_network": "",
diff --git a/nova/tests/test_filters.py b/nova/tests/test_filters.py
index 3940ce0c3..c06b50fde 100644
--- a/nova/tests/test_filters.py
+++ b/nova/tests/test_filters.py
@@ -123,3 +123,36 @@ class FiltersTestCase(test.TestCase):
filter_objs_initial,
filter_properties)
self.assertEqual(filter_objs_last, result)
+
+ def test_get_filtered_objects_none_response(self):
+ filter_objs_initial = ['initial', 'filter1', 'objects1']
+ filter_properties = 'fake_filter_properties'
+
+ def _fake_base_loader_init(*args, **kwargs):
+ pass
+
+ self.stubs.Set(loadables.BaseLoader, '__init__',
+ _fake_base_loader_init)
+
+ filt1_mock = self.mox.CreateMock(Filter1)
+ filt2_mock = self.mox.CreateMock(Filter2)
+
+ self.mox.StubOutWithMock(sys.modules[__name__], 'Filter1',
+ use_mock_anything=True)
+ self.mox.StubOutWithMock(filt1_mock, 'filter_all')
+ # Shouldn't be called.
+ self.mox.StubOutWithMock(sys.modules[__name__], 'Filter2',
+ use_mock_anything=True)
+ self.mox.StubOutWithMock(filt2_mock, 'filter_all')
+
+ Filter1().AndReturn(filt1_mock)
+ filt1_mock.filter_all(filter_objs_initial,
+ filter_properties).AndReturn(None)
+ self.mox.ReplayAll()
+
+ filter_handler = filters.BaseFilterHandler(filters.BaseFilter)
+ filter_classes = [Filter1, Filter2]
+ result = filter_handler.get_filtered_objects(filter_classes,
+ filter_objs_initial,
+ filter_properties)
+ self.assertEqual(None, result)