Merge "Cells: Add filtering and weight support"

This commit is contained in:
Jenkins 2013-05-28 20:38:05 +00:00 committed by Gerrit Code Review
commit a3e6133628
13 changed files with 848 additions and 33 deletions

View File

@ -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": "",

View File

@ -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()

View File

@ -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.

View File

@ -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
cells = self._get_possible_cells()
if not cells:
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})
{'routing_path': 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)
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()
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."""

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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'])

View File

@ -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)

View File

@ -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": "",

View File

@ -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)