diff --git a/nova_solverscheduler/scheduler/solvers/costs/vcpu_cost.py b/nova_solverscheduler/scheduler/solvers/costs/vcpu_cost.py new file mode 100644 index 0000000..fa0c5d5 --- /dev/null +++ b/nova_solverscheduler/scheduler/solvers/costs/vcpu_cost.py @@ -0,0 +1,88 @@ +# Copyright (c) 2015 Cisco Systems, Inc. +# 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. + +""" +VCPU Cost. Calculate instance placement costs by hosts' vCPU usage. + +The default is to spread instances across all hosts evenly. If you prefer +stacking, you can set the 'vcpu_cost_multiplier' option to a negative +number and the cost has the opposite effect of the default. +""" + +from oslo_config import cfg +from oslo_log import log as logging + +from nova.i18n import _LW +from nova_solverscheduler.scheduler.solvers import costs as solver_costs +from nova_solverscheduler.scheduler.solvers.costs import utils + +vcpu_cost_opts = [ + cfg.FloatOpt('vcpu_cost_multiplier', + default=1.0, + help='Multiplier used for vcpu costs. Negative ' + 'numbers mean to stack vs spread.'), +] + +CONF = cfg.CONF +CONF.register_opts(vcpu_cost_opts, group='solver_scheduler') + +LOG = logging.getLogger(__name__) + + +class VcpuCost(solver_costs.BaseLinearCost): + + def cost_multiplier(self): + return CONF.solver_scheduler.vcpu_cost_multiplier + + def get_extended_cost_matrix(self, hosts, filter_properties): + num_hosts = len(hosts) + num_instances = filter_properties.get('num_instances') + + instance_type = filter_properties.get('instance_type') or {} + requested_vcpus = instance_type.get('vcpus', 0) + if requested_vcpus <= 0: + LOG.warn(_LW("Requested instances\' vCPU number is 0 or invalid, " + "default value (0) is used.")) + + remaining_vcpus_list = [] + for i in xrange(num_hosts): + vcpus_total = hosts[i].vcpus_total + vcpus_used = hosts[i].vcpus_used + if not vcpus_total: + LOG.warn(_LW("vCPUs of %(host)s not set; assuming CPU " + "collection broken."), {'host': hosts[i]}) + vcpus_total = 0 + remaining_vcpus = vcpus_total - vcpus_used + remaining_vcpus_list.append(remaining_vcpus) + + extended_cost_matrix = [[0 for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + + if requested_vcpus == 0: + extended_cost_matrix = [ + [(-remaining_vcpus_list[i]) + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + else: + # we use int approximation here to avoid scaling problems after + # normalization, in the case that the free vcpus in all hosts are + # of very small values + extended_cost_matrix = [ + [-int(remaining_vcpus_list[i] / requested_vcpus) + j + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + extended_cost_matrix = utils.normalize_cost_matrix( + extended_cost_matrix) + return extended_cost_matrix diff --git a/nova_solverscheduler/tests/scheduler/solvers/costs/test_vcpu_cost.py b/nova_solverscheduler/tests/scheduler/solvers/costs/test_vcpu_cost.py new file mode 100644 index 0000000..b2f6cca --- /dev/null +++ b/nova_solverscheduler/tests/scheduler/solvers/costs/test_vcpu_cost.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015 Cisco Systems, Inc. +# 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. + +"""Test case for solver scheduler vCPU cost.""" + +from nova import context +from nova import test +from nova_solverscheduler.scheduler.solvers import costs +from nova_solverscheduler.scheduler.solvers.costs import vcpu_cost +from nova_solverscheduler.tests.scheduler import solver_scheduler_fakes \ + as fakes + + +class TestVcpuCost(test.NoDBTestCase): + def setUp(self): + super(TestVcpuCost, self).setUp() + self.context = context.RequestContext('fake_usr', 'fake_proj') + self.cost_handler = costs.CostHandler() + self.cost_classes = self.cost_handler.get_matching_classes( + ['nova_solverscheduler.scheduler.solvers.costs.vcpu_cost.VcpuCost'] + ) + + def _get_all_hosts(self): + host1 = fakes.FakeSolverSchedulerHostState('host1', 'node1', + {'vcpus_total': 32, 'vcpus_used': 12}) + host2 = fakes.FakeSolverSchedulerHostState('host2', 'node2', + {'vcpus_total': 16, 'vcpus_used': 6}) + host3 = fakes.FakeSolverSchedulerHostState('host3', 'node3', + {'vcpus_total': 8, 'vcpus_used': 3}) + host4 = fakes.FakeSolverSchedulerHostState('host4', 'node4', + {'vcpus_total': 0, 'vcpus_used': 0}) + return [host1, host2, host3, host4] + + def test_vcpu_cost_multiplier_1(self): + self.flags(vcpu_cost_multiplier=0.5, group='solver_scheduler') + self.assertEqual(0.5, vcpu_cost.VcpuCost().cost_multiplier()) + + def test_vcpu_cost_multiplier_2(self): + self.flags(vcpu_cost_multiplier=(-2), group='solver_scheduler') + self.assertEqual((-2), vcpu_cost.VcpuCost().cost_multiplier()) + + def test_get_extended_cost_matrix(self): + fake_hosts = self._get_all_hosts() + fake_filter_properties = { + 'context': self.context, + 'num_instances': 3, + 'instance_type': {'vcpus': 5}, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)]} + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [-1.0, -0.75, -0.5, -0.25], + [-0.5, -0.25, 0.0, 0.25], + [-0.25, 0.0, 0.25, 0.5], + [0.0, 0.25, 0.5, 0.75]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + expected_x_cost_mat = [[round(val, 4) for val in row] + for row in expected_x_cost_mat] + x_cost_mat = [[round(val, 4) for val in row] for row in x_cost_mat] + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_bad_vcpu_req(self): + fake_hosts = self._get_all_hosts() + fake_filter_properties = { + 'context': self.context, + 'num_instances': 3, + 'instance_type': {'vcpus': 0}, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)]} + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [-1.0, -1.0, -1.0, -1.0], + [-0.5, -0.5, -0.5, -0.5], + [-0.25, -0.25, -0.25, -0.25], + [0.0, 0.0, 0.0, 0.0]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + expected_x_cost_mat = [[round(val, 4) for val in row] + for row in expected_x_cost_mat] + x_cost_mat = [[round(val, 4) for val in row] for row in x_cost_mat] + self.assertEqual(expected_x_cost_mat, x_cost_mat)