diff --git a/etc/nova/policy.json b/etc/nova/policy.json index d6524b6a28ab..b09f5f473a6c 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 000000000000..f9b6d7a1565c --- /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:" where + 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 000000000000..ab5adb1e8276 --- /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 18389dbc501a..c95498fa0911 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 - 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.""" diff --git a/nova/cells/weights/__init__.py b/nova/cells/weights/__init__.py new file mode 100644 index 000000000000..202a6a31a89c --- /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 000000000000..1a1d164d2a1a --- /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 000000000000..14348ee20345 --- /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 18e3a7d66682..4b7f9ff102e8 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 000000000000..e11e6c640d85 --- /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 c9e626385a19..c8f90619e8f2 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 000000000000..ca01e99396b1 --- /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 b30793ac40fb..b09944878074 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 3940ce0c33d1..c06b50fde957 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)