diff --git a/nova_solverscheduler/scheduler/solvers/constraints/rack_affinity_constraint.py b/nova_solverscheduler/scheduler/solvers/constraints/rack_affinity_constraint.py new file mode 100644 index 0000000..99ac16a --- /dev/null +++ b/nova_solverscheduler/scheduler/solvers/constraints/rack_affinity_constraint.py @@ -0,0 +1,130 @@ +# 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. + +import six + +from oslo_log import log as logging + +from nova.compute import api as compute +from nova.i18n import _ +from nova_solverscheduler.scheduler.solvers import constraints +from nova_solverscheduler.scheduler.solvers import utils as solver_utils + +LOG = logging.getLogger(__name__) + + +class SameRackConstraint(constraints.BaseLinearConstraint): + """Place instances in the same racks as those of specified instances. + If the specified instances are in hosts without rack config, then place + instances in the same hosts as those of specified instances. + """ + + def __init__(self): + super(SameRackConstraint, self).__init__() + self.compute_api = compute.API() + + def get_constraint_matrix(self, hosts, filter_properties): + num_hosts = len(hosts) + num_instances = filter_properties.get('num_instances') + + constraint_matrix = [[True for j in xrange(num_instances)] + for i in xrange(num_hosts)] + + scheduler_hints = filter_properties.get('scheduler_hints') or {} + affinity_uuids = scheduler_hints.get('same_rack', []) + + if not affinity_uuids: + return constraint_matrix + + if isinstance(affinity_uuids, six.string_types): + affinity_uuids = [affinity_uuids] + + host_racks_map = solver_utils.get_host_racks_map(hosts) + + affinity_racks = set([]) + affinity_hosts = set([]) + + for i in xrange(num_hosts): + host_name = hosts[i].host + host_racks = host_racks_map.get(host_name, set([])) + if solver_utils.instance_uuids_overlap(hosts[i], affinity_uuids): + affinity_racks = affinity_racks.union(host_racks) + affinity_hosts.add(host_name) + + for i in xrange(num_hosts): + host_name = hosts[i].host + host_racks = host_racks_map.get(host_name, set([])) + if host_name in affinity_hosts: + LOG.debug(_("%(host)s passed same-rack check."), + {'host': host_name}) + continue + elif (len(host_racks) == 0) or any([rack not in affinity_racks + for rack in host_racks]): + constraint_matrix[i] = [False for j in xrange(num_instances)] + else: + LOG.debug(_("%(host)s passed same-rack check."), + {'host': host_name}) + + return constraint_matrix + + +class DifferentRackConstraint(constraints.BaseLinearConstraint): + """Place instances in different racks as those of specified instances. + If the specified instances are in hosts without rack config, then place + instances in different hosts as those of specified instances. + """ + + def __init__(self): + super(DifferentRackConstraint, self).__init__() + self.compute_api = compute.API() + + def get_constraint_matrix(self, hosts, filter_properties): + num_hosts = len(hosts) + num_instances = filter_properties.get('num_instances') + + constraint_matrix = [[True for j in xrange(num_instances)] + for i in xrange(num_hosts)] + + scheduler_hints = filter_properties.get('scheduler_hints') or {} + affinity_uuids = scheduler_hints.get('different_rack', []) + + if not affinity_uuids: + return constraint_matrix + + if isinstance(affinity_uuids, six.string_types): + affinity_uuids = [affinity_uuids] + + host_racks_map = solver_utils.get_host_racks_map(hosts) + + affinity_racks = set([]) + affinity_hosts = set([]) + + for i in xrange(num_hosts): + host_name = hosts[i].host + host_racks = host_racks_map.get(host_name, set([])) + if solver_utils.instance_uuids_overlap(hosts[i], affinity_uuids): + affinity_racks = affinity_racks.union(host_racks) + affinity_hosts.add(host_name) + + for i in xrange(num_hosts): + host_name = hosts[i].host + host_racks = host_racks_map.get(host_name, set([])) + if any([rack in affinity_racks for rack in host_racks]) or ( + host_name in affinity_hosts): + constraint_matrix[i] = [False for j in xrange(num_instances)] + LOG.debug(_("%(host)s didnot pass different-rack check."), + {'host': host_name}) + + return constraint_matrix diff --git a/nova_solverscheduler/scheduler/solvers/utils.py b/nova_solverscheduler/scheduler/solvers/utils.py index 7a0ac69..0e924da 100644 --- a/nova_solverscheduler/scheduler/solvers/utils.py +++ b/nova_solverscheduler/scheduler/solvers/utils.py @@ -86,3 +86,25 @@ def get_host_racks_config(): str(e)) return host_racks_map + + +def get_host_racks_map(hosts): + """Return a dict where keys are host names and values are names of racks + belonging to each host. Hosts without rack config will not show up in the + result. By default this checks host aggregate for a metadata key 'rack', + if no such metadata key is found, it will check an external config file. + """ + host_racks_map = {} + + for host_state in hosts: + host_name = host_state.host + host_racks = aggregate_values_from_key(host_state, 'rack') + if host_racks: + host_racks_map.setdefault(host_name, set()) + host_racks_map[host_name] = host_racks_map[host_name].union( + host_racks) + + if not host_racks_map: + host_racks_map = get_host_racks_config() + + return host_racks_map diff --git a/nova_solverscheduler/tests/scheduler/solvers/constraints/test_rack_affinity_constraint.py b/nova_solverscheduler/tests/scheduler/solvers/constraints/test_rack_affinity_constraint.py new file mode 100644 index 0000000..59d4f39 --- /dev/null +++ b/nova_solverscheduler/tests/scheduler/solvers/constraints/test_rack_affinity_constraint.py @@ -0,0 +1,465 @@ +# 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. + +import mock + +from nova import context +from nova import objects +from nova import test +from nova_solverscheduler.scheduler.solvers.constraints \ + import rack_affinity_constraint +from nova_solverscheduler.tests.scheduler import solver_scheduler_fakes \ + as fakes + + +@mock.patch('nova_solverscheduler.scheduler.solvers.utils.get_host_racks_map') +class TestSameRackConstraint(test.NoDBTestCase): + USES_DB = True + + def setUp(self): + super(TestSameRackConstraint, self).setUp() + self.constraint_cls = rack_affinity_constraint.SameRackConstraint + self.context = context.RequestContext('fake', 'fake') + self.fake_hosts = [fakes.FakeSolverSchedulerHostState( + 'host%s' % i, 'node1', {}) for i in xrange(1, 7)] + + def test_same_rack_one_inst(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [True, True], + [True, True], + [False, False], + [False, False], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_same_rack_multi_inst(self, racks_mock): + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + self.fake_hosts[0].instances = {instance1_uuid: instance1} + self.fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': [instance1_uuid, instance2_uuid]} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [True, True], + [True, True], + [True, True], + [True, True], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_same_rack_with_cross_rack_host(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host1 + self.fake_hosts[0].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1', 'rack2']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [True, True], + [True, True], + [True, True], + [True, True], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_same_rack_incomplete_rack_config(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [False, False], + [True, True], + [False, False], + [False, False], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_same_rack_incomplete_rack_config2(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host3 + self.fake_hosts[2].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [False, False], + [False, False], + [True, True], + [True, True], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_same_rack_no_rack_config(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'same_rack': instance_uuid} + } + + racks_mock.return_value = {} + + expected_cons_mat = [ + [False, False], + [True, True], + [False, False], + [False, False], + [False, False], + [False, False] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + +@mock.patch('nova_solverscheduler.scheduler.solvers.utils.get_host_racks_map') +class TestDifferentRackConstraint(test.NoDBTestCase): + USES_DB = True + + def setUp(self): + super(TestDifferentRackConstraint, self).setUp() + self.constraint_cls = rack_affinity_constraint.DifferentRackConstraint + self.context = context.RequestContext('fake', 'fake') + self.fake_hosts = [fakes.FakeSolverSchedulerHostState( + 'host%s' % i, 'node1', {}) for i in xrange(1, 7)] + + def test_different_rack_one_inst(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [False, False], + [False, False], + [True, True], + [True, True], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_different_rack_multi_inst(self, racks_mock): + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + self.fake_hosts[0].instances = {instance1_uuid: instance1} + self.fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': + [instance1_uuid, instance2_uuid]} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [False, False], + [False, False], + [False, False], + [False, False], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_different_rack_with_cross_rack_host(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host1 + self.fake_hosts[0].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1', 'rack2']), + 'host2': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host5': set(['rack3']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [False, False], + [False, False], + [False, False], + [False, False], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_different_rack_incomplete_rack_config(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [True, True], + [False, False], + [True, True], + [True, True], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_different_rack_incomplete_rack_config2(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host3 + self.fake_hosts[2].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': instance_uuid} + } + + racks_mock.return_value = { + 'host1': set(['rack1']), + 'host3': set(['rack2']), + 'host4': set(['rack2']), + 'host6': set(['rack3']) + } + + expected_cons_mat = [ + [True, True], + [True, True], + [False, False], + [False, False], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) + + def test_different_rack_no_rack_config(self, racks_mock): + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + self.fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context, + 'project_id': 'fake', + 'num_instances': 2, + 'scheduler_hints': {'different_rack': instance_uuid} + } + + racks_mock.return_value = {} + + expected_cons_mat = [ + [True, True], + [False, False], + [True, True], + [True, True], + [True, True], + [True, True] + ] + + cons_mat = self.constraint_cls().get_constraint_matrix( + self.fake_hosts, fake_filter_properties) + + self.assertEqual(expected_cons_mat, cons_mat) diff --git a/nova_solverscheduler/tests/scheduler/solvers/test_utils.py b/nova_solverscheduler/tests/scheduler/solvers/test_utils.py index e126b68..7cc987e 100644 --- a/nova_solverscheduler/tests/scheduler/solvers/test_utils.py +++ b/nova_solverscheduler/tests/scheduler/solvers/test_utils.py @@ -13,16 +13,41 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import os.path import tempfile from oslo_config import cfg +from nova import objects from nova import test from nova_solverscheduler.scheduler.solvers import utils +from nova_solverscheduler.tests.scheduler import solver_scheduler_fakes \ + as fakes CONF = cfg.CONF +_AGGREGATE_FIXTURES = [ + objects.Aggregate( + id=1, + name='aggr1', + hosts=['fake-host'], + metadata={'k1': '1', 'k2': '2'}, + ), + objects.Aggregate( + id=2, + name='bar', + hosts=['fake-host'], + metadata={'k1': '3', 'k2': '4'}, + ), + objects.Aggregate( + id=3, + name='bar', + hosts=['fake-host'], + metadata={'k1': '6,7', 'k2': '8, 9'}, + ), +] + class TestRackConfigLoader(test.NoDBTestCase): """Test case for rack config file loading.""" @@ -67,3 +92,64 @@ k=bla def tearDown(self): self.config.close() super(TestRackConfigLoader, self).tearDown() + + +class TestGetHostRacksMap(test.NoDBTestCase): + def setUp(self): + super(TestGetHostRacksMap, self).setUp() + self.fake_aggregates = [ + objects.Aggregate( + id=1, + name='aggr1', + hosts=['host1', 'host2'], + metadata={'rack': 'rack1', 'foo': 'bar'}, + ), + objects.Aggregate( + id=2, + name='aggr2', + hosts=['host2'], + metadata={'rack': 'rack2'}, + ), + objects.Aggregate( + id=3, + name='aggr3', + hosts=['host3'], + metadata={'foo': 'bar'}, + ), + ] + + def test_get_host_racks_map_from_aggregate(self): + host1 = fakes.FakeSolverSchedulerHostState('host1', 'node1', + {'aggregates': self.fake_aggregates[0:1]}) + host2 = fakes.FakeSolverSchedulerHostState('host2', 'node2', + {'aggregates': self.fake_aggregates[0:2]}) + host3 = fakes.FakeSolverSchedulerHostState('host3', 'node3', + {'aggregates': self.fake_aggregates[2:3]}) + host4 = fakes.FakeSolverSchedulerHostState('host4', 'node4', + {'aggregates': []}) + hosts = [host1, host2, host3, host4] + + result = utils.get_host_racks_map(hosts) + expected_result = { + 'host1': set(['rack1']), + 'host2': set(['rack1', 'rack2']) + } + + self.assertEqual(expected_result, result) + + @mock.patch('nova_solverscheduler.scheduler.solvers.utils.' + 'get_host_racks_config') + def test_get_host_racks_map_no_aggregate_key(self, getconfig_mock): + host1 = fakes.FakeSolverSchedulerHostState('host1', 'node1', {}) + host2 = fakes.FakeSolverSchedulerHostState('host2', 'node2', {}) + host3 = fakes.FakeSolverSchedulerHostState('host3', 'node3', + {'aggregates': self.fake_aggregates[2:3]}) + host4 = fakes.FakeSolverSchedulerHostState('host4', 'node4', {}) + hosts = [host1, host2, host3, host4] + + expected_result = {'host1': set('rack1'), 'host2': set('rack1')} + getconfig_mock.return_value = expected_result + + result = utils.get_host_racks_map(hosts) + + self.assertEqual(expected_result, result)