Merge "Cells: Add filtering and weight support"
This commit is contained in:
commit
a3e6133628
@ -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": "",
|
||||
|
62
nova/cells/filters/__init__.py
Normal file
62
nova/cells/filters/__init__.py
Normal 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()
|
68
nova/cells/filters/target_cell.py
Normal file
68
nova/cells/filters/target_cell.py
Normal 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.
|
@ -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.
|
||||
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'])
|
||||
# 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,
|
||||
# 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."""
|
||||
|
43
nova/cells/weights/__init__.py
Normal file
43
nova/cells/weights/__init__.py
Normal 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()
|
54
nova/cells/weights/ram_by_instance_type.py
Normal file
54
nova/cells/weights/ram_by_instance_type.py
Normal 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)
|
33
nova/cells/weights/weight_offset.py
Normal file
33
nova/cells/weights/weight_offset.py
Normal 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)
|
@ -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
|
||||
|
121
nova/tests/cells/test_cells_filters.py
Normal file
121
nova/tests/cells/test_cells_filters.py
Normal 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)
|
@ -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'])
|
||||
|
165
nova/tests/cells/test_cells_weights.py
Normal file
165
nova/tests/cells/test_cells_weights.py
Normal 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)
|
@ -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": "",
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user