Rename DistributedScheduler as FilterScheduler
Change-Id: I1091609d5997c4ba9c26a3f2426496ff7f1e64fa
This commit is contained in:
@@ -1,260 +0,0 @@
|
|||||||
# Copyright (c) 2011 Openstack, LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
The DistributedScheduler is for creating instances locally.
|
|
||||||
You can customize this scheduler by specifying your own Host Filters and
|
|
||||||
Weighing Functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import operator
|
|
||||||
|
|
||||||
from nova import exception
|
|
||||||
from nova import flags
|
|
||||||
from nova import log as logging
|
|
||||||
from nova.notifier import api as notifier
|
|
||||||
from nova.scheduler import driver
|
|
||||||
from nova.scheduler import least_cost
|
|
||||||
from nova.scheduler import scheduler_options
|
|
||||||
from nova import utils
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DistributedScheduler(driver.Scheduler):
|
|
||||||
"""Scheduler that can be used for filtering and weighing."""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DistributedScheduler, self).__init__(*args, **kwargs)
|
|
||||||
self.cost_function_cache = {}
|
|
||||||
self.options = scheduler_options.SchedulerOptions()
|
|
||||||
|
|
||||||
def schedule(self, context, topic, method, *args, **kwargs):
|
|
||||||
"""The schedule() contract requires we return the one
|
|
||||||
best-suited host for this request.
|
|
||||||
|
|
||||||
NOTE: We're only focused on compute instances right now,
|
|
||||||
so this method will always raise NoValidHost()."""
|
|
||||||
msg = _("No host selection for %s defined.") % topic
|
|
||||||
raise exception.NoValidHost(reason=msg)
|
|
||||||
|
|
||||||
def schedule_run_instance(self, context, request_spec, *args, **kwargs):
|
|
||||||
"""This method is called from nova.compute.api to provision
|
|
||||||
an instance. We first create a build plan (a list of WeightedHosts)
|
|
||||||
and then provision.
|
|
||||||
|
|
||||||
Returns a list of the instances created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
elevated = context.elevated()
|
|
||||||
num_instances = request_spec.get('num_instances', 1)
|
|
||||||
LOG.debug(_("Attempting to build %(num_instances)d instance(s)") %
|
|
||||||
locals())
|
|
||||||
|
|
||||||
payload = dict(request_spec=request_spec)
|
|
||||||
notifier.notify(notifier.publisher_id("scheduler"),
|
|
||||||
'scheduler.run_instance.start', notifier.INFO, payload)
|
|
||||||
|
|
||||||
weighted_hosts = self._schedule(context, "compute", request_spec,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
if not weighted_hosts:
|
|
||||||
raise exception.NoValidHost(reason="")
|
|
||||||
|
|
||||||
# NOTE(comstud): Make sure we do not pass this through. It
|
|
||||||
# contains an instance of RpcContext that cannot be serialized.
|
|
||||||
kwargs.pop('filter_properties', None)
|
|
||||||
|
|
||||||
instances = []
|
|
||||||
for num in xrange(num_instances):
|
|
||||||
if not weighted_hosts:
|
|
||||||
break
|
|
||||||
weighted_host = weighted_hosts.pop(0)
|
|
||||||
|
|
||||||
request_spec['instance_properties']['launch_index'] = num
|
|
||||||
instance = self._provision_resource(elevated, weighted_host,
|
|
||||||
request_spec, kwargs)
|
|
||||||
|
|
||||||
if instance:
|
|
||||||
instances.append(instance)
|
|
||||||
|
|
||||||
notifier.notify(notifier.publisher_id("scheduler"),
|
|
||||||
'scheduler.run_instance.end', notifier.INFO, payload)
|
|
||||||
|
|
||||||
return instances
|
|
||||||
|
|
||||||
def schedule_prep_resize(self, context, request_spec, *args, **kwargs):
|
|
||||||
"""Select a target for resize.
|
|
||||||
|
|
||||||
Selects a target host for the instance, post-resize, and casts
|
|
||||||
the prep_resize operation to it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
hosts = self._schedule(context, 'compute', request_spec,
|
|
||||||
*args, **kwargs)
|
|
||||||
if not hosts:
|
|
||||||
raise exception.NoValidHost(reason="")
|
|
||||||
host = hosts.pop(0)
|
|
||||||
|
|
||||||
# NOTE(comstud): Make sure we do not pass this through. It
|
|
||||||
# contains an instance of RpcContext that cannot be serialized.
|
|
||||||
kwargs.pop('filter_properties', None)
|
|
||||||
|
|
||||||
# Forward off to the host
|
|
||||||
driver.cast_to_compute_host(context, host.host_state.host,
|
|
||||||
'prep_resize', **kwargs)
|
|
||||||
|
|
||||||
def _provision_resource(self, context, weighted_host, request_spec,
|
|
||||||
kwargs):
|
|
||||||
"""Create the requested resource in this Zone."""
|
|
||||||
instance = self.create_instance_db_entry(context, request_spec)
|
|
||||||
|
|
||||||
payload = dict(request_spec=request_spec,
|
|
||||||
weighted_host=weighted_host.to_dict(),
|
|
||||||
instance_id=instance['uuid'])
|
|
||||||
notifier.notify(notifier.publisher_id("scheduler"),
|
|
||||||
'scheduler.run_instance.scheduled', notifier.INFO,
|
|
||||||
payload)
|
|
||||||
|
|
||||||
driver.cast_to_compute_host(context, weighted_host.host_state.host,
|
|
||||||
'run_instance', instance_uuid=instance['uuid'], **kwargs)
|
|
||||||
inst = driver.encode_instance(instance, local=True)
|
|
||||||
# So if another instance is created, create_instance_db_entry will
|
|
||||||
# actually create a new entry, instead of assume it's been created
|
|
||||||
# already
|
|
||||||
del request_spec['instance_properties']['uuid']
|
|
||||||
return inst
|
|
||||||
|
|
||||||
def _get_configuration_options(self):
|
|
||||||
"""Fetch options dictionary. Broken out for testing."""
|
|
||||||
return self.options.get_configuration()
|
|
||||||
|
|
||||||
def populate_filter_properties(self, request_spec, filter_properties):
|
|
||||||
"""Stuff things into filter_properties. Can be overriden in a
|
|
||||||
subclass to add more data.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _schedule(self, context, topic, request_spec, *args, **kwargs):
|
|
||||||
"""Returns a list of hosts that meet the required specs,
|
|
||||||
ordered by their fitness.
|
|
||||||
"""
|
|
||||||
elevated = context.elevated()
|
|
||||||
if topic != "compute":
|
|
||||||
msg = _("Scheduler only understands Compute nodes (for now)")
|
|
||||||
raise NotImplementedError(msg)
|
|
||||||
|
|
||||||
instance_properties = request_spec['instance_properties']
|
|
||||||
instance_type = request_spec.get("instance_type", None)
|
|
||||||
|
|
||||||
cost_functions = self.get_cost_functions()
|
|
||||||
config_options = self._get_configuration_options()
|
|
||||||
|
|
||||||
filter_properties = kwargs.get('filter_properties', {})
|
|
||||||
filter_properties.update({'context': context,
|
|
||||||
'request_spec': request_spec,
|
|
||||||
'config_options': config_options,
|
|
||||||
'instance_type': instance_type})
|
|
||||||
|
|
||||||
self.populate_filter_properties(request_spec,
|
|
||||||
filter_properties)
|
|
||||||
|
|
||||||
# Find our local list of acceptable hosts by repeatedly
|
|
||||||
# filtering and weighing our options. Each time we choose a
|
|
||||||
# host, we virtually consume resources on it so subsequent
|
|
||||||
# selections can adjust accordingly.
|
|
||||||
|
|
||||||
# unfiltered_hosts_dict is {host : ZoneManager.HostInfo()}
|
|
||||||
unfiltered_hosts_dict = self.host_manager.get_all_host_states(
|
|
||||||
elevated, topic)
|
|
||||||
|
|
||||||
# Note: remember, we are using an iterator here. So only
|
|
||||||
# traverse this list once. This can bite you if the hosts
|
|
||||||
# are being scanned in a filter or weighing function.
|
|
||||||
hosts = unfiltered_hosts_dict.itervalues()
|
|
||||||
|
|
||||||
num_instances = request_spec.get('num_instances', 1)
|
|
||||||
selected_hosts = []
|
|
||||||
for num in xrange(num_instances):
|
|
||||||
# Filter local hosts based on requirements ...
|
|
||||||
hosts = self.host_manager.filter_hosts(hosts,
|
|
||||||
filter_properties)
|
|
||||||
if not hosts:
|
|
||||||
# Can't get any more locally.
|
|
||||||
break
|
|
||||||
|
|
||||||
LOG.debug(_("Filtered %(hosts)s") % locals())
|
|
||||||
|
|
||||||
# weighted_host = WeightedHost() ... the best
|
|
||||||
# host for the job.
|
|
||||||
# TODO(comstud): filter_properties will also be used for
|
|
||||||
# weighing and I plan fold weighing into the host manager
|
|
||||||
# in a future patch. I'll address the naming of this
|
|
||||||
# variable at that time.
|
|
||||||
weighted_host = least_cost.weighted_sum(cost_functions,
|
|
||||||
hosts, filter_properties)
|
|
||||||
LOG.debug(_("Weighted %(weighted_host)s") % locals())
|
|
||||||
selected_hosts.append(weighted_host)
|
|
||||||
|
|
||||||
# Now consume the resources so the filter/weights
|
|
||||||
# will change for the next instance.
|
|
||||||
weighted_host.host_state.consume_from_instance(
|
|
||||||
instance_properties)
|
|
||||||
|
|
||||||
selected_hosts.sort(key=operator.attrgetter('weight'))
|
|
||||||
return selected_hosts[:num_instances]
|
|
||||||
|
|
||||||
def get_cost_functions(self, topic=None):
|
|
||||||
"""Returns a list of tuples containing weights and cost functions to
|
|
||||||
use for weighing hosts
|
|
||||||
"""
|
|
||||||
if topic is None:
|
|
||||||
# Schedulers only support compute right now.
|
|
||||||
topic = "compute"
|
|
||||||
if topic in self.cost_function_cache:
|
|
||||||
return self.cost_function_cache[topic]
|
|
||||||
|
|
||||||
cost_fns = []
|
|
||||||
for cost_fn_str in FLAGS.least_cost_functions:
|
|
||||||
if '.' in cost_fn_str:
|
|
||||||
short_name = cost_fn_str.split('.')[-1]
|
|
||||||
else:
|
|
||||||
short_name = cost_fn_str
|
|
||||||
cost_fn_str = "%s.%s.%s" % (
|
|
||||||
__name__, self.__class__.__name__, short_name)
|
|
||||||
if not (short_name.startswith('%s_' % topic) or
|
|
||||||
short_name.startswith('noop')):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# NOTE: import_class is somewhat misnamed since
|
|
||||||
# the weighing function can be any non-class callable
|
|
||||||
# (i.e., no 'self')
|
|
||||||
cost_fn = utils.import_class(cost_fn_str)
|
|
||||||
except exception.ClassNotFound:
|
|
||||||
raise exception.SchedulerCostFunctionNotFound(
|
|
||||||
cost_fn_str=cost_fn_str)
|
|
||||||
|
|
||||||
try:
|
|
||||||
flag_name = "%s_weight" % cost_fn.__name__
|
|
||||||
weight = getattr(FLAGS, flag_name)
|
|
||||||
except AttributeError:
|
|
||||||
raise exception.SchedulerWeightFlagNotFound(
|
|
||||||
flag_name=flag_name)
|
|
||||||
cost_fns.append((weight, cost_fn))
|
|
||||||
|
|
||||||
self.cost_function_cache[topic] = cost_fns
|
|
||||||
return cost_fns
|
|
||||||
@@ -30,7 +30,7 @@ from nova.scheduler import driver
|
|||||||
multi_scheduler_opts = [
|
multi_scheduler_opts = [
|
||||||
cfg.StrOpt('compute_scheduler_driver',
|
cfg.StrOpt('compute_scheduler_driver',
|
||||||
default='nova.scheduler.'
|
default='nova.scheduler.'
|
||||||
'distributed_scheduler.DistributedScheduler',
|
'filter_scheduler.FilterScheduler',
|
||||||
help='Driver to use for scheduling compute calls'),
|
help='Driver to use for scheduling compute calls'),
|
||||||
cfg.StrOpt('volume_scheduler_driver',
|
cfg.StrOpt('volume_scheduler_driver',
|
||||||
default='nova.scheduler.chance.ChanceScheduler',
|
default='nova.scheduler.chance.ChanceScheduler',
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
"""
|
|
||||||
Tests For Distributed Scheduler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mox
|
|
||||||
|
|
||||||
from nova import context
|
|
||||||
from nova import exception
|
|
||||||
from nova.scheduler import least_cost
|
|
||||||
from nova.scheduler import host_manager
|
|
||||||
from nova.scheduler import distributed_scheduler
|
|
||||||
from nova import test
|
|
||||||
from nova.tests.scheduler import fakes
|
|
||||||
from nova.tests.scheduler import test_scheduler
|
|
||||||
|
|
||||||
|
|
||||||
def fake_filter_hosts(hosts, filter_properties):
|
|
||||||
return list(hosts)
|
|
||||||
|
|
||||||
|
|
||||||
class DistributedSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|
||||||
"""Test case for Distributed Scheduler."""
|
|
||||||
|
|
||||||
driver_cls = distributed_scheduler.DistributedScheduler
|
|
||||||
|
|
||||||
def test_run_instance_no_hosts(self):
|
|
||||||
"""
|
|
||||||
Ensure empty hosts & child_zones result in NoValidHosts exception.
|
|
||||||
"""
|
|
||||||
def _fake_empty_call_zone_method(*args, **kwargs):
|
|
||||||
return []
|
|
||||||
|
|
||||||
sched = fakes.FakeDistributedScheduler()
|
|
||||||
|
|
||||||
fake_context = context.RequestContext('user', 'project')
|
|
||||||
request_spec = {'instance_type': {'memory_mb': 1, 'root_gb': 1,
|
|
||||||
'ephemeral_gb': 0},
|
|
||||||
'instance_properties': {'project_id': 1}}
|
|
||||||
self.assertRaises(exception.NoValidHost, sched.schedule_run_instance,
|
|
||||||
fake_context, request_spec)
|
|
||||||
|
|
||||||
def test_run_instance_non_admin(self):
|
|
||||||
"""Test creating an instance locally using run_instance, passing
|
|
||||||
a non-admin context. DB actions should work."""
|
|
||||||
self.was_admin = False
|
|
||||||
|
|
||||||
def fake_get(context, *args, **kwargs):
|
|
||||||
# make sure this is called with admin context, even though
|
|
||||||
# we're using user context below
|
|
||||||
self.was_admin = context.is_admin
|
|
||||||
return {}
|
|
||||||
|
|
||||||
sched = fakes.FakeDistributedScheduler()
|
|
||||||
self.stubs.Set(sched.host_manager, 'get_all_host_states', fake_get)
|
|
||||||
|
|
||||||
fake_context = context.RequestContext('user', 'project')
|
|
||||||
|
|
||||||
request_spec = {'instance_type': {'memory_mb': 1, 'local_gb': 1},
|
|
||||||
'instance_properties': {'project_id': 1}}
|
|
||||||
self.assertRaises(exception.NoValidHost, sched.schedule_run_instance,
|
|
||||||
fake_context, request_spec)
|
|
||||||
self.assertTrue(self.was_admin)
|
|
||||||
|
|
||||||
def test_schedule_bad_topic(self):
|
|
||||||
"""Parameter checking."""
|
|
||||||
sched = fakes.FakeDistributedScheduler()
|
|
||||||
fake_context = context.RequestContext('user', 'project')
|
|
||||||
self.assertRaises(NotImplementedError, sched._schedule, fake_context,
|
|
||||||
"foo", {})
|
|
||||||
|
|
||||||
def test_scheduler_includes_launch_index(self):
|
|
||||||
ctxt = "fake-context"
|
|
||||||
fake_kwargs = {'fake_kwarg1': 'fake_value1',
|
|
||||||
'fake_kwarg2': 'fake_value2'}
|
|
||||||
instance_opts = {'fake_opt1': 'meow'}
|
|
||||||
request_spec = {'num_instances': 2,
|
|
||||||
'instance_properties': instance_opts}
|
|
||||||
instance1 = {'uuid': 'fake-uuid1'}
|
|
||||||
instance2 = {'uuid': 'fake-uuid2'}
|
|
||||||
|
|
||||||
def _has_launch_index(expected_index):
|
|
||||||
"""Return a function that verifies the expected index."""
|
|
||||||
def _check_launch_index(value):
|
|
||||||
if 'instance_properties' in value:
|
|
||||||
if 'launch_index' in value['instance_properties']:
|
|
||||||
index = value['instance_properties']['launch_index']
|
|
||||||
if index == expected_index:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
return _check_launch_index
|
|
||||||
|
|
||||||
class ContextFake(object):
|
|
||||||
def elevated(self):
|
|
||||||
return ctxt
|
|
||||||
context_fake = ContextFake()
|
|
||||||
|
|
||||||
self.mox.StubOutWithMock(self.driver, '_schedule')
|
|
||||||
self.mox.StubOutWithMock(self.driver, '_provision_resource')
|
|
||||||
|
|
||||||
self.driver._schedule(context_fake, 'compute',
|
|
||||||
request_spec, **fake_kwargs
|
|
||||||
).AndReturn(['host1', 'host2'])
|
|
||||||
# instance 1
|
|
||||||
self.driver._provision_resource(
|
|
||||||
ctxt, 'host1',
|
|
||||||
mox.Func(_has_launch_index(0)), fake_kwargs).AndReturn(instance1)
|
|
||||||
# instance 2
|
|
||||||
self.driver._provision_resource(
|
|
||||||
ctxt, 'host2',
|
|
||||||
mox.Func(_has_launch_index(1)), fake_kwargs).AndReturn(instance2)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
self.driver.schedule_run_instance(context_fake, request_spec,
|
|
||||||
**fake_kwargs)
|
|
||||||
|
|
||||||
def test_schedule_happy_day(self):
|
|
||||||
"""Make sure there's nothing glaringly wrong with _schedule()
|
|
||||||
by doing a happy day pass through."""
|
|
||||||
|
|
||||||
self.next_weight = 1.0
|
|
||||||
|
|
||||||
def _fake_weighted_sum(functions, hosts, options):
|
|
||||||
self.next_weight += 2.0
|
|
||||||
host_state = hosts[0]
|
|
||||||
return least_cost.WeightedHost(self.next_weight,
|
|
||||||
host_state=host_state)
|
|
||||||
|
|
||||||
sched = fakes.FakeDistributedScheduler()
|
|
||||||
fake_context = context.RequestContext('user', 'project',
|
|
||||||
is_admin=True)
|
|
||||||
|
|
||||||
self.stubs.Set(sched.host_manager, 'filter_hosts',
|
|
||||||
fake_filter_hosts)
|
|
||||||
self.stubs.Set(least_cost, 'weighted_sum', _fake_weighted_sum)
|
|
||||||
fakes.mox_host_manager_db_calls(self.mox, fake_context)
|
|
||||||
|
|
||||||
request_spec = {'num_instances': 10,
|
|
||||||
'instance_type': {'memory_mb': 512, 'root_gb': 512,
|
|
||||||
'ephemeral_gb': 0,
|
|
||||||
'vcpus': 1},
|
|
||||||
'instance_properties': {'project_id': 1,
|
|
||||||
'root_gb': 512,
|
|
||||||
'memory_mb': 512,
|
|
||||||
'ephemeral_gb': 0,
|
|
||||||
'vcpus': 1}}
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
weighted_hosts = sched._schedule(fake_context, 'compute',
|
|
||||||
request_spec)
|
|
||||||
self.assertEquals(len(weighted_hosts), 10)
|
|
||||||
for weighted_host in weighted_hosts:
|
|
||||||
self.assertTrue(weighted_host.host_state is not None)
|
|
||||||
|
|
||||||
def test_get_cost_functions(self):
|
|
||||||
self.flags(reserved_host_memory_mb=128)
|
|
||||||
fixture = fakes.FakeDistributedScheduler()
|
|
||||||
fns = fixture.get_cost_functions()
|
|
||||||
self.assertEquals(len(fns), 1)
|
|
||||||
weight, fn = fns[0]
|
|
||||||
self.assertEquals(weight, 1.0)
|
|
||||||
hostinfo = host_manager.HostState('host', 'compute')
|
|
||||||
hostinfo.update_from_compute_node(dict(memory_mb=1000,
|
|
||||||
local_gb=0, vcpus=1))
|
|
||||||
self.assertEquals(1000 - 128, fn(hostinfo, {}))
|
|
||||||
Reference in New Issue
Block a user