Adds LeastCostScheduler which uses a series of cost functions and associated weights to determine which host to provision to.
Left for future work: * Handle scheduling of many instances (currently assumes n=1) * Handle scheduling of arbitrary resources (currently weigh_hosts only handles instances) * Add more cost functions (currently just noop and fill-first) * Simulator so we can determine sensible values for cost-function-weights NOTE: This patch depends on Sandy's dist-scheduler-2a patch.
This commit is contained in:
		| @@ -41,6 +41,7 @@ import json | ||||
| from nova import exception | ||||
| from nova import flags | ||||
| from nova import log as logging | ||||
| from nova.scheduler import zone_aware_scheduler | ||||
| from nova import utils | ||||
| from nova.scheduler import zone_aware_scheduler | ||||
|  | ||||
|   | ||||
							
								
								
									
										156
									
								
								nova/scheduler/least_cost.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								nova/scheduler/least_cost.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| # 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. | ||||
| """ | ||||
| Least Cost Scheduler is a mechanism for choosing which host machines to | ||||
| provision a set of resources to. The input of the least-cost-scheduler is a | ||||
| set of objective-functions, called the 'cost-functions', a weight for each | ||||
| cost-function, and a list of candidate hosts (gathered via FilterHosts). | ||||
|  | ||||
| The cost-function and weights are tabulated, and the host with the least cost | ||||
| is then selected for provisioning. | ||||
| """ | ||||
|  | ||||
| import collections | ||||
|  | ||||
| from nova import flags | ||||
| from nova import log as logging | ||||
| from nova.scheduler import zone_aware_scheduler | ||||
| from nova import utils | ||||
|  | ||||
| LOG = logging.getLogger('nova.scheduler.least_cost') | ||||
|  | ||||
| FLAGS = flags.FLAGS | ||||
| flags.DEFINE_list('least_cost_scheduler_cost_functions', | ||||
|                   ['nova.scheduler.least_cost.noop_cost_fn'], | ||||
|                   'Which cost functions the LeastCostScheduler should use.') | ||||
|  | ||||
|  | ||||
| # TODO(sirp): Once we have enough of these rules, we can break them out into a | ||||
| # cost_functions.py file (perhaps in a least_cost_scheduler directory) | ||||
| flags.DEFINE_integer('noop_cost_fn_weight', 1, | ||||
|                      'How much weight to give the noop cost function') | ||||
|  | ||||
|  | ||||
| def noop_cost_fn(host): | ||||
|     """Return a pre-weight cost of 1 for each host""" | ||||
|     return 1 | ||||
|  | ||||
|  | ||||
| flags.DEFINE_integer('fill_first_cost_fn_weight', 1, | ||||
|                      'How much weight to give the fill-first cost function') | ||||
|  | ||||
|  | ||||
| def fill_first_cost_fn(host): | ||||
|     """Prefer hosts that have less ram available, filter_hosts will exclude | ||||
|     hosts that don't have enough ram""" | ||||
|     hostname, caps = host | ||||
|     free_mem = caps['compute']['host_memory_free'] | ||||
|     return free_mem | ||||
|  | ||||
|  | ||||
| class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler): | ||||
|     def get_cost_fns(self): | ||||
|         """Returns a list of tuples containing weights and cost functions to | ||||
|         use for weighing hosts | ||||
|         """ | ||||
|         cost_fns = [] | ||||
|         for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions: | ||||
|  | ||||
|             try: | ||||
|                 # NOTE(sirp): import_class is somewhat misnamed since it can | ||||
|                 # any callable from a module | ||||
|                 cost_fn = utils.import_class(cost_fn_str) | ||||
|             except exception.ClassNotFound: | ||||
|                 raise exception.SchedulerCostFunctionNotFound( | ||||
|                     cost_fn_str=cost_fn_str) | ||||
|  | ||||
|             try: | ||||
|                 weight = getattr(FLAGS, "%s_weight" % cost_fn.__name__) | ||||
|             except AttributeError: | ||||
|                 raise exception.SchedulerWeightFlagNotFound( | ||||
|                     flag_name=flag_name) | ||||
|  | ||||
|             cost_fns.append((weight, cost_fn)) | ||||
|  | ||||
|         return cost_fns | ||||
|  | ||||
|     def weigh_hosts(self, num, request_spec, hosts): | ||||
|         """Returns a list of dictionaries of form: | ||||
|             [ {weight: weight, hostname: hostname} ]""" | ||||
|  | ||||
|         # FIXME(sirp): weigh_hosts should handle more than just instances | ||||
|         hostnames = [hostname for hostname, caps in hosts] | ||||
|  | ||||
|         cost_fns = self.get_cost_fns() | ||||
|         costs = weighted_sum(domain=hosts, weighted_fns=cost_fns) | ||||
|  | ||||
|         weighted = [] | ||||
|         weight_log = [] | ||||
|         for cost, hostname in zip(costs, hostnames): | ||||
|             weight_log.append("%s: %s" % (hostname, "%.2f" % cost)) | ||||
|             weight_dict = dict(weight=cost, hostname=hostname) | ||||
|             weighted.append(weight_dict) | ||||
|  | ||||
|         LOG.debug(_("Weighted Costs => %s") % weight_log) | ||||
|         return weighted | ||||
|  | ||||
|  | ||||
| def normalize_list(L): | ||||
|     """Normalize an array of numbers such that each element satisfies: | ||||
|         0 <= e <= 1""" | ||||
|     if not L: | ||||
|         return L | ||||
|     max_ = max(L) | ||||
|     if max_ > 0: | ||||
|         return [(float(e) / max_) for e in L] | ||||
|     return L | ||||
|  | ||||
|  | ||||
| def weighted_sum(domain, weighted_fns, normalize=True): | ||||
|     """Use the weighted-sum method to compute a score for an array of objects. | ||||
|     Normalize the results of the objective-functions so that the weights are | ||||
|     meaningful regardless of objective-function's range. | ||||
|  | ||||
|     domain - input to be scored | ||||
|     weighted_fns - list of weights and functions like: | ||||
|         [(weight, objective-functions)] | ||||
|  | ||||
|     Returns an unsorted of scores. To pair with hosts do: zip(scores, hosts) | ||||
|     """ | ||||
|     # Table of form: | ||||
|     #   { domain1: [score1, score2, ..., scoreM] | ||||
|     #     ... | ||||
|     #     domainN: [score1, score2, ..., scoreM] } | ||||
|     score_table = collections.defaultdict(list) | ||||
|     for weight, fn in weighted_fns: | ||||
|         scores = [fn(elem) for elem in domain] | ||||
|  | ||||
|         if normalize: | ||||
|             norm_scores = normalize_list(scores) | ||||
|         else: | ||||
|             norm_scores = scores | ||||
|  | ||||
|         for idx, score in enumerate(norm_scores): | ||||
|             weighted_score = score * weight | ||||
|             score_table[idx].append(weighted_score) | ||||
|  | ||||
|     # Sum rows in table to compute score for each element in domain | ||||
|     domain_scores = [] | ||||
|     for idx in sorted(score_table): | ||||
|         elem_score = sum(score_table[idx]) | ||||
|         elem = domain[idx] | ||||
|         domain_scores.append(elem_score) | ||||
|  | ||||
|     return domain_scores | ||||
| @@ -116,6 +116,9 @@ class ZoneAwareScheduler(driver.Scheduler): | ||||
|         # Filter local hosts based on requirements ... | ||||
|         host_list = self.filter_hosts(num_instances, request_spec) | ||||
|  | ||||
|         # TODO(sirp): weigh_hosts should also be a function of 'topic' or | ||||
|         # resources, so that we can apply different objective functions to it | ||||
|  | ||||
|         # then weigh the selected hosts. | ||||
|         # weighted = [{weight=weight, name=hostname}, ...] | ||||
|         weighted = self.weigh_hosts(num_instances, request_spec, host_list) | ||||
| @@ -141,10 +144,14 @@ class ZoneAwareScheduler(driver.Scheduler): | ||||
|         """Derived classes must override this method and return | ||||
|            a list of hosts in [(hostname, capability_dict)] format. | ||||
|         """ | ||||
|         raise NotImplemented() | ||||
|         # NOTE(sirp): The default logic is the equivalent to AllHostsFilter | ||||
|         service_states = self.zone_manager.service_states | ||||
|         return [(host, services) | ||||
|                 for host, services in service_states.iteritems()] | ||||
|  | ||||
|     def weigh_hosts(self, num, request_spec, hosts): | ||||
|         """Derived classes must override this method and return | ||||
|         a lists of hosts in [{weight, hostname}] format. | ||||
|         """Derived classes may override this to provide more sophisticated | ||||
|         scheduling objectives | ||||
|         """ | ||||
|         raise NotImplemented() | ||||
|         # NOTE(sirp): The default logic is the same as the NoopCostFunction | ||||
|         return [dict(weight=1, hostname=host) for host, caps in hosts] | ||||
|   | ||||
							
								
								
									
										0
									
								
								nova/tests/scheduler/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								nova/tests/scheduler/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										206
									
								
								nova/tests/scheduler/test_host_filter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								nova/tests/scheduler/test_host_filter.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| # 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 Scheduler Host Filters. | ||||
| """ | ||||
|  | ||||
| import json | ||||
|  | ||||
| from nova import exception | ||||
| from nova import flags | ||||
| from nova import test | ||||
| from nova.scheduler import host_filter | ||||
|  | ||||
| FLAGS = flags.FLAGS | ||||
|  | ||||
|  | ||||
| class FakeZoneManager: | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class HostFilterTestCase(test.TestCase): | ||||
|     """Test case for host filters.""" | ||||
|  | ||||
|     def _host_caps(self, multiplier): | ||||
|         # Returns host capabilities in the following way: | ||||
|         # host1 = memory:free 10 (100max) | ||||
|         #         disk:available 100 (1000max) | ||||
|         # hostN = memory:free 10 + 10N | ||||
|         #         disk:available 100 + 100N | ||||
|         # in other words: hostN has more resources than host0 | ||||
|         # which means ... don't go above 10 hosts. | ||||
|         return {'host_name-description': 'XenServer %s' % multiplier, | ||||
|                 'host_hostname': 'xs-%s' % multiplier, | ||||
|                 'host_memory_total': 100, | ||||
|                 'host_memory_overhead': 10, | ||||
|                 'host_memory_free': 10 + multiplier * 10, | ||||
|                 'host_memory_free-computed': 10 + multiplier * 10, | ||||
|                 'host_other-config': {}, | ||||
|                 'host_ip_address': '192.168.1.%d' % (100 + multiplier), | ||||
|                 'host_cpu_info': {}, | ||||
|                 'disk_available': 100 + multiplier * 100, | ||||
|                 'disk_total': 1000, | ||||
|                 'disk_used': 0, | ||||
|                 'host_uuid': 'xxx-%d' % multiplier, | ||||
|                 'host_name-label': 'xs-%s' % multiplier} | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.old_flag = FLAGS.default_host_filter | ||||
|         FLAGS.default_host_filter = \ | ||||
|                             'nova.scheduler.host_filter.AllHostsFilter' | ||||
|         self.instance_type = dict(name='tiny', | ||||
|                 memory_mb=50, | ||||
|                 vcpus=10, | ||||
|                 local_gb=500, | ||||
|                 flavorid=1, | ||||
|                 swap=500, | ||||
|                 rxtx_quota=30000, | ||||
|                 rxtx_cap=200) | ||||
|  | ||||
|         self.zone_manager = FakeZoneManager() | ||||
|         states = {} | ||||
|         for x in xrange(10): | ||||
|             states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)} | ||||
|         self.zone_manager.service_states = states | ||||
|  | ||||
|     def tearDown(self): | ||||
|         FLAGS.default_host_filter = self.old_flag | ||||
|  | ||||
|     def test_choose_filter(self): | ||||
|         # Test default filter ... | ||||
|         hf = host_filter.choose_host_filter() | ||||
|         self.assertEquals(hf._full_name(), | ||||
|                         'nova.scheduler.host_filter.AllHostsFilter') | ||||
|         # Test valid filter ... | ||||
|         hf = host_filter.choose_host_filter( | ||||
|                         'nova.scheduler.host_filter.InstanceTypeFilter') | ||||
|         self.assertEquals(hf._full_name(), | ||||
|                         'nova.scheduler.host_filter.InstanceTypeFilter') | ||||
|         # Test invalid filter ... | ||||
|         try: | ||||
|             host_filter.choose_host_filter('does not exist') | ||||
|             self.fail("Should not find host filter.") | ||||
|         except exception.SchedulerHostFilterNotFound: | ||||
|             pass | ||||
|  | ||||
|     def test_all_host_filter(self): | ||||
|         hf = host_filter.AllHostsFilter() | ||||
|         cooked = hf.instance_type_to_filter(self.instance_type) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|         self.assertEquals(10, len(hosts)) | ||||
|         for host, capabilities in hosts: | ||||
|             self.assertTrue(host.startswith('host')) | ||||
|  | ||||
|     def test_instance_type_filter(self): | ||||
|         hf = host_filter.InstanceTypeFilter() | ||||
|         # filter all hosts that can support 50 ram and 500 disk | ||||
|         name, cooked = hf.instance_type_to_filter(self.instance_type) | ||||
|         self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', | ||||
|                           name) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|         self.assertEquals(6, len(hosts)) | ||||
|         just_hosts = [host for host, caps in hosts] | ||||
|         just_hosts.sort() | ||||
|         self.assertEquals('host05', just_hosts[0]) | ||||
|         self.assertEquals('host10', just_hosts[5]) | ||||
|  | ||||
|     def test_json_filter(self): | ||||
|         hf = host_filter.JsonFilter() | ||||
|         # filter all hosts that can support 50 ram and 500 disk | ||||
|         name, cooked = hf.instance_type_to_filter(self.instance_type) | ||||
|         self.assertEquals('nova.scheduler.host_filter.JsonFilter', name) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|         self.assertEquals(6, len(hosts)) | ||||
|         just_hosts = [host for host, caps in hosts] | ||||
|         just_hosts.sort() | ||||
|         self.assertEquals('host05', just_hosts[0]) | ||||
|         self.assertEquals('host10', just_hosts[5]) | ||||
|  | ||||
|         # Try some custom queries | ||||
|  | ||||
|         raw = ['or', | ||||
|                    ['and', | ||||
|                        ['<', '$compute.host_memory_free', 30], | ||||
|                        ['<', '$compute.disk_available', 300] | ||||
|                    ], | ||||
|                    ['and', | ||||
|                        ['>', '$compute.host_memory_free', 70], | ||||
|                        ['>', '$compute.disk_available', 700] | ||||
|                    ] | ||||
|               ] | ||||
|         cooked = json.dumps(raw) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|  | ||||
|         self.assertEquals(5, len(hosts)) | ||||
|         just_hosts = [host for host, caps in hosts] | ||||
|         just_hosts.sort() | ||||
|         for index, host in zip([1, 2, 8, 9, 10], just_hosts): | ||||
|             self.assertEquals('host%02d' % index, host) | ||||
|  | ||||
|         raw = ['not', | ||||
|                   ['=', '$compute.host_memory_free', 30], | ||||
|               ] | ||||
|         cooked = json.dumps(raw) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|  | ||||
|         self.assertEquals(9, len(hosts)) | ||||
|         just_hosts = [host for host, caps in hosts] | ||||
|         just_hosts.sort() | ||||
|         for index, host in zip([1, 2, 4, 5, 6, 7, 8, 9, 10], just_hosts): | ||||
|             self.assertEquals('host%02d' % index, host) | ||||
|  | ||||
|         raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] | ||||
|         cooked = json.dumps(raw) | ||||
|         hosts = hf.filter_hosts(self.zone_manager, cooked) | ||||
|  | ||||
|         self.assertEquals(5, len(hosts)) | ||||
|         just_hosts = [host for host, caps in hosts] | ||||
|         just_hosts.sort() | ||||
|         for index, host in zip([2, 4, 6, 8, 10], just_hosts): | ||||
|             self.assertEquals('host%02d' % index, host) | ||||
|  | ||||
|         # Try some bogus input ... | ||||
|         raw = ['unknown command', ] | ||||
|         cooked = json.dumps(raw) | ||||
|         try: | ||||
|             hf.filter_hosts(self.zone_manager, cooked) | ||||
|             self.fail("Should give KeyError") | ||||
|         except KeyError, e: | ||||
|             pass | ||||
|  | ||||
|         self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps([]))) | ||||
|         self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps({}))) | ||||
|         self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps( | ||||
|                 ['not', True, False, True, False] | ||||
|             ))) | ||||
|  | ||||
|         try: | ||||
|             hf.filter_hosts(self.zone_manager, json.dumps( | ||||
|                 'not', True, False, True, False | ||||
|             )) | ||||
|             self.fail("Should give KeyError") | ||||
|         except KeyError, e: | ||||
|             pass | ||||
|  | ||||
|         self.assertFalse(hf.filter_hosts(self.zone_manager, | ||||
|                 json.dumps(['=', '$foo', 100]))) | ||||
|         self.assertFalse(hf.filter_hosts(self.zone_manager, | ||||
|                 json.dumps(['=', '$.....', 100]))) | ||||
|         self.assertFalse(hf.filter_hosts(self.zone_manager, | ||||
|                 json.dumps( | ||||
|             ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]))) | ||||
|  | ||||
|         self.assertFalse(hf.filter_hosts(self.zone_manager, | ||||
|                 json.dumps(['=', {}, ['>', '$missing....foo']]))) | ||||
							
								
								
									
										144
									
								
								nova/tests/scheduler/test_least_cost_scheduler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								nova/tests/scheduler/test_least_cost_scheduler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| # 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 Least Cost Scheduler | ||||
| """ | ||||
|  | ||||
| from nova import flags | ||||
| from nova import test | ||||
| from nova.scheduler import least_cost | ||||
| from nova.tests.scheduler import test_zone_aware_scheduler | ||||
|  | ||||
| MB = 1024 * 1024 | ||||
| FLAGS = flags.FLAGS | ||||
|  | ||||
|  | ||||
| class FakeHost(object): | ||||
|     def __init__(self, host_id, free_ram, io): | ||||
|         self.id = host_id | ||||
|         self.free_ram = free_ram | ||||
|         self.io = io | ||||
|  | ||||
|  | ||||
| class WeightedSumTestCase(test.TestCase): | ||||
|     def test_empty_domain(self): | ||||
|         domain = [] | ||||
|         weighted_fns = [] | ||||
|         result = least_cost.weighted_sum(domain, weighted_fns) | ||||
|         expected = [] | ||||
|         self.assertEqual(expected, result) | ||||
|  | ||||
|     def test_basic_costing(self): | ||||
|         hosts = [ | ||||
|             FakeHost(1, 512 * MB, 100), | ||||
|             FakeHost(2, 256 * MB, 400), | ||||
|             FakeHost(3, 512 * MB, 100) | ||||
|         ] | ||||
|  | ||||
|         weighted_fns = [ | ||||
|             (1, lambda h: h.free_ram),  # Fill-first, free_ram is a *cost* | ||||
|             (2, lambda h: h.io),  # Avoid high I/O | ||||
|         ] | ||||
|  | ||||
|         costs = least_cost.weighted_sum( | ||||
|             domain=hosts, weighted_fns=weighted_fns) | ||||
|  | ||||
|         # Each 256 MB unit of free-ram contributes 0.5 points by way of: | ||||
|         #   cost = weight * (score/max_score) = 1 * (256/512) = 0.5 | ||||
|         # Each 100 iops of IO adds 0.5 points by way of: | ||||
|         #   cost = 2 * (100/400) = 2 * 0.25 = 0.5 | ||||
|         expected = [1.5, 2.5, 1.5] | ||||
|         self.assertEqual(expected, costs) | ||||
|  | ||||
|  | ||||
| class LeastCostSchedulerTestCase(test.TestCase): | ||||
|     def setUp(self): | ||||
|         super(LeastCostSchedulerTestCase, self).setUp() | ||||
|  | ||||
|         class FakeZoneManager: | ||||
|             pass | ||||
|  | ||||
|         zone_manager = FakeZoneManager() | ||||
|  | ||||
|         states = test_zone_aware_scheduler.fake_zone_manager_service_states( | ||||
|             num_hosts=10) | ||||
|         zone_manager.service_states = states | ||||
|  | ||||
|         self.sched = least_cost.LeastCostScheduler() | ||||
|         self.sched.zone_manager = zone_manager | ||||
|  | ||||
|     def tearDown(self): | ||||
|         super(LeastCostSchedulerTestCase, self).tearDown() | ||||
|  | ||||
|     def assertWeights(self, expected, num, request_spec, hosts): | ||||
|         weighted = self.sched.weigh_hosts(num, request_spec, hosts) | ||||
|         self.assertDictListMatch(weighted, expected, approx_equal=True) | ||||
|  | ||||
|     def test_no_hosts(self): | ||||
|         num = 1 | ||||
|         request_spec = {} | ||||
|         hosts = [] | ||||
|  | ||||
|         expected = [] | ||||
|         self.assertWeights(expected, num, request_spec, hosts) | ||||
|  | ||||
|     def test_noop_cost_fn(self): | ||||
|         FLAGS.least_cost_scheduler_cost_functions = [ | ||||
|             'nova.scheduler.least_cost.noop_cost_fn' | ||||
|         ] | ||||
|         FLAGS.noop_cost_fn_weight = 1 | ||||
|  | ||||
|         num = 1 | ||||
|         request_spec = {} | ||||
|         hosts = self.sched.filter_hosts(num, request_spec) | ||||
|  | ||||
|         expected = [dict(weight=1, hostname=hostname) | ||||
|                     for hostname, caps in hosts] | ||||
|         self.assertWeights(expected, num, request_spec, hosts) | ||||
|  | ||||
|     def test_cost_fn_weights(self): | ||||
|         FLAGS.least_cost_scheduler_cost_functions = [ | ||||
|             'nova.scheduler.least_cost.noop_cost_fn' | ||||
|         ] | ||||
|         FLAGS.noop_cost_fn_weight = 2 | ||||
|  | ||||
|         num = 1 | ||||
|         request_spec = {} | ||||
|         hosts = self.sched.filter_hosts(num, request_spec) | ||||
|  | ||||
|         expected = [dict(weight=2, hostname=hostname) | ||||
|                     for hostname, caps in hosts] | ||||
|         self.assertWeights(expected, num, request_spec, hosts) | ||||
|  | ||||
|     def test_fill_first_cost_fn(self): | ||||
|         FLAGS.least_cost_scheduler_cost_functions = [ | ||||
|             'nova.scheduler.least_cost.fill_first_cost_fn' | ||||
|         ] | ||||
|         FLAGS.fill_first_cost_fn_weight = 1 | ||||
|  | ||||
|         num = 1 | ||||
|         request_spec = {} | ||||
|         hosts = self.sched.filter_hosts(num, request_spec) | ||||
|  | ||||
|         expected = [] | ||||
|         for idx, (hostname, caps) in enumerate(hosts): | ||||
|             # Costs are normalized so over 10 hosts, each host with increasing | ||||
|             # free ram will cost 1/N more. Since the lowest cost host has some | ||||
|             # free ram, we add in the 1/N for the base_cost | ||||
|             weight = 0.1 + (0.1 * idx) | ||||
|             weight_dict = dict(weight=weight, hostname=hostname) | ||||
|             expected.append(weight_dict) | ||||
|  | ||||
|         self.assertWeights(expected, num, request_spec, hosts) | ||||
| @@ -22,6 +22,37 @@ from nova.scheduler import zone_aware_scheduler | ||||
| from nova.scheduler import zone_manager | ||||
| 
 | ||||
| 
 | ||||
| def _host_caps(multiplier): | ||||
|     # Returns host capabilities in the following way: | ||||
|     # host1 = memory:free 10 (100max) | ||||
|     #         disk:available 100 (1000max) | ||||
|     # hostN = memory:free 10 + 10N | ||||
|     #         disk:available 100 + 100N | ||||
|     # in other words: hostN has more resources than host0 | ||||
|     # which means ... don't go above 10 hosts. | ||||
|     return {'host_name-description': 'XenServer %s' % multiplier, | ||||
|             'host_hostname': 'xs-%s' % multiplier, | ||||
|             'host_memory_total': 100, | ||||
|             'host_memory_overhead': 10, | ||||
|             'host_memory_free': 10 + multiplier * 10, | ||||
|             'host_memory_free-computed': 10 + multiplier * 10, | ||||
|             'host_other-config': {}, | ||||
|             'host_ip_address': '192.168.1.%d' % (100 + multiplier), | ||||
|             'host_cpu_info': {}, | ||||
|             'disk_available': 100 + multiplier * 100, | ||||
|             'disk_total': 1000, | ||||
|             'disk_used': 0, | ||||
|             'host_uuid': 'xxx-%d' % multiplier, | ||||
|             'host_name-label': 'xs-%s' % multiplier} | ||||
| 
 | ||||
| 
 | ||||
| def fake_zone_manager_service_states(num_hosts): | ||||
|     states = {} | ||||
|     for x in xrange(num_hosts): | ||||
|         states['host%02d' % (x + 1)] = {'compute': _host_caps(x)} | ||||
|     return states | ||||
| 
 | ||||
| 
 | ||||
| class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler): | ||||
|     def filter_hosts(self, num, specs): | ||||
|         # NOTE(sirp): this is returning [(hostname, services)] | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Rick Harris
					Rick Harris