From 4f3c5411d92d488b78f06212912fc7ae6d225b79 Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Mon, 17 Jun 2013 10:36:46 -0400 Subject: New select_destinations scheduler call select_destinations returns a list of (host, node) tuples that satisfy the request_spec and filter_properties passed to it. This will allow the conductor to get a list of destinations for workflows such as creating or resizing an instance and then handle the orchestration itself. part of bp query-scheduler Change-Id: I1a42cea64dbad67562d7efe8d759e6efb5ec8121 --- nova/scheduler/chance.py | 11 ++++++ nova/scheduler/driver.py | 9 +++++ nova/scheduler/filter_scheduler.py | 16 ++++++++ nova/scheduler/manager.py | 13 ++++++- nova/scheduler/rpcapi.py | 7 ++++ nova/scheduler/utils.py | 3 ++ nova/tests/conductor/test_conductor.py | 3 +- nova/tests/scheduler/test_chance_scheduler.py | 45 ++++++++++++++++++++++ nova/tests/scheduler/test_filter_scheduler.py | 55 +++++++++++++++++++++++++++ nova/tests/scheduler/test_rpcapi.py | 6 +++ nova/tests/scheduler/test_scheduler.py | 4 ++ 11 files changed, 170 insertions(+), 2 deletions(-) diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index f0b1701e0..41a69d21e 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -72,6 +72,17 @@ class ChanceScheduler(driver.Scheduler): raise exception.NoValidHost(reason="") return hosts + def select_destinations(self, context, request_spec, filter_properties): + """Selects random destinations.""" + num_instances = request_spec['num_instances'] + # NOTE(alaski): Returns a list of tuples for compatibility with + # filter_scheduler + dests = [(self._schedule(context, CONF.compute_topic, request_spec, + filter_properties), None) for i in range(num_instances)] + if len(dests) < num_instances: + raise exception.NoValidHost(reason='') + return dests + def schedule_run_instance(self, context, request_spec, admin_password, injected_files, requested_networks, is_first_time, diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index c4265285f..765aede10 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -158,6 +158,15 @@ class Scheduler(object): msg = _("Driver must implement schedule_run_instance") raise NotImplementedError(msg) + def select_destinations(self, context, request_spec, filter_properties): + """Must override select_destinations method. + + :return: A list of (host, node) tuples that satisifies the request_spec + and filter_properties. + """ + msg = _("Driver must implement select_destinations") + raise NotImplementedError(msg) + def select_hosts(self, context, request_spec, filter_properties): """Must override select_hosts method for scheduler to work.""" msg = _("Driver must implement select_hosts") diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py index 4e5662e65..103aec940 100644 --- a/nova/scheduler/filter_scheduler.py +++ b/nova/scheduler/filter_scheduler.py @@ -161,6 +161,22 @@ class FilterScheduler(driver.Scheduler): raise exception.NoValidHost(reason="") return hosts + def select_destinations(self, context, request_spec, filter_properties): + """Selects a filtered set of hosts and nodes.""" + num_instances = request_spec['num_instances'] + instance_uuids = request_spec.get('instance_uuids') + selected_hosts = self._schedule(context, request_spec, + filter_properties, instance_uuids) + + # Couldn't fulfill the request_spec + if len(selected_hosts) < num_instances: + raise exception.NoValidHost(reason='') + + dests = [] + for host in selected_hosts: + dests.append((host.obj.host, host.obj.nodename)) + return dests + def _provision_resource(self, context, weighed_host, request_spec, filter_properties, requested_networks, injected_files, admin_password, is_first_time, instance_uuid=None): diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 2d63ee970..bd99808f4 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -60,7 +60,7 @@ QUOTAS = quota.QUOTAS class SchedulerManager(manager.Manager): """Chooses a host to run instances on.""" - RPC_API_VERSION = '2.6' + RPC_API_VERSION = '2.7' def __init__(self, scheduler_driver=None, *args, **kwargs): if not scheduler_driver: @@ -324,3 +324,14 @@ class SchedulerManager(manager.Manager): hosts = self.driver.select_hosts(context, request_spec, filter_properties) return jsonutils.to_primitive(hosts) + + @rpc_common.client_exceptions(exception.NoValidHost) + def select_destinations(self, context, request_spec, filter_properties): + """Returns destinations(s) best suited for this request_spec and + filter_properties. + + The result should be a list of (host, nodename) tuples. + """ + dests = self.driver.select_destinations(context, request_spec, + filter_properties) + return jsonutils.to_primitive(dests) diff --git a/nova/scheduler/rpcapi.py b/nova/scheduler/rpcapi.py index 369363bb4..9cd7a28b6 100644 --- a/nova/scheduler/rpcapi.py +++ b/nova/scheduler/rpcapi.py @@ -67,6 +67,8 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy): ... Grizzly supports message version 2.6. So, any changes to existing methods in 2.x after that point should be done such that they can handle the version_cap being set to 2.6. + + 2.7 - Add select_destinations() ''' # @@ -90,6 +92,11 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy): default_version=self.BASE_RPC_API_VERSION, version_cap=version_cap) + def select_destinations(self, ctxt, request_spec, filter_properties): + return self.call(ctxt, self.make_msg('select_destinations', + request_spec=request_spec, filter_properties=filter_properties), + version='2.7') + def run_instance(self, ctxt, request_spec, admin_password, injected_files, requested_networks, is_first_time, filter_properties): diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index b707f424c..aa6cff149 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -36,5 +36,8 @@ def build_request_spec(ctxt, image, instances): 'image': image, 'instance_properties': instance, 'instance_type': instance_type, + 'num_instances': len(instances), + # NOTE(alaski): This should be removed as logic moves from the + # scheduler to conductor. Provides backwards compatibility now. 'instance_uuids': [inst['uuid'] for inst in instances]} return jsonutils.to_primitive(request_spec) diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py index 609c2164c..9d8725f66 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -1251,7 +1251,8 @@ class _BaseTaskTestCase(object): 'instance_type': expected_instance_type, 'instance_uuids': ['fakeuuid', 'fakeuuid2'], 'block_device_mapping': 'block_device_mapping', - 'security_group': 'security_groups'}, + 'security_group': 'security_groups', + 'num_instances': 2}, admin_password='admin_password', injected_files='injected_files', requested_networks='requested_networks', is_first_time=True, diff --git a/nova/tests/scheduler/test_chance_scheduler.py b/nova/tests/scheduler/test_chance_scheduler.py index ba1701e93..4e52464ec 100644 --- a/nova/tests/scheduler/test_chance_scheduler.py +++ b/nova/tests/scheduler/test_chance_scheduler.py @@ -197,3 +197,48 @@ class ChanceSchedulerTestCase(test_scheduler.SchedulerTestCase): self.stubs.Set(self.driver, '_schedule', _return_no_host) self.assertRaises(exception.NoValidHost, self.driver.select_hosts, self.context, {}, {}) + + def test_select_destinations(self): + ctxt = context.RequestContext('fake', 'fake', False) + ctxt_elevated = 'fake-context-elevated' + request_spec = {'num_instances': 2} + + self.mox.StubOutWithMock(ctxt, 'elevated') + self.mox.StubOutWithMock(self.driver, 'hosts_up') + self.mox.StubOutWithMock(random, 'choice') + + hosts_full = ['host1', 'host2', 'host3', 'host4'] + + ctxt.elevated().AndReturn(ctxt_elevated) + self.driver.hosts_up(ctxt_elevated, 'compute').AndReturn(hosts_full) + random.choice(hosts_full).AndReturn('host3') + + ctxt.elevated().AndReturn(ctxt_elevated) + self.driver.hosts_up(ctxt_elevated, 'compute').AndReturn(hosts_full) + random.choice(hosts_full).AndReturn('host2') + + self.mox.ReplayAll() + dests = self.driver.select_destinations(ctxt, request_spec, {}) + self.assertEquals(2, len(dests)) + (host, node) = dests[0] + self.assertEquals('host3', host) + self.assertEquals(None, node) + (host, node) = dests[1] + self.assertEquals('host2', host) + self.assertEquals(None, node) + + def test_select_destinations_no_valid_host(self): + + def _return_no_host(*args, **kwargs): + return [] + + self.mox.StubOutWithMock(self.driver, 'hosts_up') + self.driver.hosts_up(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn([1, 2]) + self.stubs.Set(self.driver, '_filter_hosts', _return_no_host) + self.mox.ReplayAll() + + request_spec = {'num_instances': 1} + self.assertRaises(exception.NoValidHost, + self.driver.select_destinations, self.context, + request_spec, {}) diff --git a/nova/tests/scheduler/test_filter_scheduler.py b/nova/tests/scheduler/test_filter_scheduler.py index ac2e73ec7..1aeab0408 100644 --- a/nova/tests/scheduler/test_filter_scheduler.py +++ b/nova/tests/scheduler/test_filter_scheduler.py @@ -609,3 +609,58 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): self.stubs.Set(self.driver, '_schedule', _return_no_host) self.assertRaises(exception.NoValidHost, self.driver.select_hosts, self.context, {}, {}) + + def test_select_destinations(self): + """select_destinations is basically a wrapper around _schedule(). + + Similar to the _schedule tests, this just does a happy path test to + ensure there is nothing glaringly wrong. + """ + + self.next_weight = 1.0 + + selected_hosts = [] + selected_nodes = [] + + def _fake_weigh_objects(_self, functions, hosts, options): + self.next_weight += 2.0 + host_state = hosts[0] + selected_hosts.append(host_state.host) + selected_nodes.append(host_state.nodename) + return [weights.WeighedHost(host_state, self.next_weight)] + + sched = fakes.FakeFilterScheduler() + fake_context = context.RequestContext('user', 'project', + is_admin=True) + + self.stubs.Set(sched.host_manager, 'get_filtered_hosts', + fake_get_filtered_hosts) + self.stubs.Set(weights.HostWeightHandler, + 'get_weighed_objects', _fake_weigh_objects) + fakes.mox_host_manager_db_calls(self.mox, fake_context) + + request_spec = {'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, + 'os_type': 'Linux'}, + 'num_instances': 1} + self.mox.ReplayAll() + dests = sched.select_destinations(fake_context, request_spec, {}) + (host, node) = dests[0] + self.assertEquals(host, selected_hosts[0]) + self.assertEquals(node, selected_nodes[0]) + + def test_select_destinations_no_valid_host(self): + + def _return_no_host(*args, **kwargs): + return [] + + self.stubs.Set(self.driver, '_schedule', _return_no_host) + self.assertRaises(exception.NoValidHost, + self.driver.select_destinations, self.context, + {'num_instances': 1}, {}) diff --git a/nova/tests/scheduler/test_rpcapi.py b/nova/tests/scheduler/test_rpcapi.py index cecc55f20..7d61ecf97 100644 --- a/nova/tests/scheduler/test_rpcapi.py +++ b/nova/tests/scheduler/test_rpcapi.py @@ -86,3 +86,9 @@ class SchedulerRpcAPITestCase(test.NoDBTestCase): request_spec='fake_request_spec', filter_properties='fake_prop', version='2.6') + + def test_select_destinations(self): + self._test_scheduler_api('select_destinations', rpc_method='call', + request_spec='fake_request_spec', + filter_properties='fake_prop', + version='2.7') diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 149c6d3c8..e2778a94b 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -527,6 +527,10 @@ class SchedulerDriverBaseTestCase(SchedulerTestCase): self.assertRaises(NotImplementedError, self.driver.select_hosts, self.context, {}, {}) + def test_unimplemented_select_destinations(self): + self.assertRaises(NotImplementedError, + self.driver.select_destinations, self.context, {}, {}) + class SchedulerDriverModuleTestCase(test.NoDBTestCase): """Test case for scheduler driver module methods.""" -- cgit