diff --git a/zun/conf/scheduler.py b/zun/conf/scheduler.py index 8d48102a6..98f6a3367 100644 --- a/zun/conf/scheduler.py +++ b/zun/conf/scheduler.py @@ -64,6 +64,7 @@ Related options: cfg.ListOpt("enabled_filters", default=[ "NoopFilter", + "CPUFilter" ], help=""" Filters that the scheduler will use. diff --git a/zun/scheduler/filter_scheduler.py b/zun/scheduler/filter_scheduler.py index f8f5a7abf..ea444c32b 100644 --- a/zun/scheduler/filter_scheduler.py +++ b/zun/scheduler/filter_scheduler.py @@ -20,6 +20,7 @@ import random from zun.common import exception from zun.common.i18n import _ import zun.conf +from zun import objects from zun.scheduler import driver from zun.scheduler import filters @@ -42,21 +43,23 @@ class FilterScheduler(driver.Scheduler): def _schedule(self, context, container): """Picks a host according to filters.""" hosts = self.hosts_up(context) - hosts = self.filter_handler.get_filtered_objects(self.enabled_filters, - hosts, + nodes = objects.ComputeNode.list(context) + nodes = [node for node in nodes if node.hostname in hosts] + nodes = self.filter_handler.get_filtered_objects(self.enabled_filters, + nodes, container) - if not hosts: + if not nodes: msg = _("Is the appropriate service running?") raise exception.NoValidHost(reason=msg) - return random.choice(hosts) + return random.choice(nodes) def select_destinations(self, context, containers): """Selects destinations by filters.""" dests = [] for container in containers: - host = self._schedule(context, container) - host_state = dict(host=host, nodename=None, limits=None) + node = self._schedule(context, container) + host_state = dict(host=node.hostname, nodename=None, limits=None) dests.append(host_state) if len(dests) < 1: diff --git a/zun/scheduler/filters/cpu_filter.py b/zun/scheduler/filters/cpu_filter.py new file mode 100644 index 000000000..51431fa2d --- /dev/null +++ b/zun/scheduler/filters/cpu_filter.py @@ -0,0 +1,38 @@ +# Copyright (c) 2017 OpenStack Foundation +# 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. + +from oslo_log import log as logging + +from zun.scheduler import filters + +LOG = logging.getLogger(__name__) + + +class CPUFilter(filters.BaseHostFilter): + """Filter the containers by cpu request""" + + run_filter_once_per_request = True + + def host_passes(self, host_state, container): + cpu_free = host_state.cpus - host_state.cpu_used + if cpu_free < container.cpu: + LOG.debug("%(host_state)s does not have %(container_vcpus).2f " + "usable vcpus, it only has %(free_vcpus).2f usable " + "vcpus", + {'host_state': host_state, + 'container_vcpus': container.cpu, + 'free_vcpus': cpu_free}) + return False + return True diff --git a/zun/tests/unit/scheduler/filters/__init__.py b/zun/tests/unit/scheduler/filters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/tests/unit/scheduler/filters/test_cpu_filter.py b/zun/tests/unit/scheduler/filters/test_cpu_filter.py new file mode 100644 index 000000000..be4874aa9 --- /dev/null +++ b/zun/tests/unit/scheduler/filters/test_cpu_filter.py @@ -0,0 +1,41 @@ +# 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. + +from zun.common import context +from zun import objects +from zun.scheduler.filters import cpu_filter +from zun.tests import base + + +class TestCPUFilter(base.TestCase): + + def setUp(self): + super(TestCPUFilter, self).setUp() + self.context = context.RequestContext('fake_user', 'fake_project') + + def test_cpu_filter_pass(self): + self.filt_cls = cpu_filter.CPUFilter() + container = objects.Container(self.context) + container.cpu = 5.0 + host = objects.ComputeNode(self.context) + host.cpus = 8 + host.cpu_used = 0.0 + self.assertTrue(self.filt_cls.host_passes(host, container)) + + def test_cpu_filter_fail(self): + self.filt_cls = cpu_filter.CPUFilter() + container = objects.Container(self.context) + container.cpu = 8.0 + host = objects.ComputeNode(self.context) + host.cpus = 5 + host.cpu_used = 2.0 + self.assertFalse(self.filt_cls.host_passes(host, container)) diff --git a/zun/tests/unit/scheduler/test_filter_scheduler.py b/zun/tests/unit/scheduler/test_filter_scheduler.py index d283c1cc2..a60f43fdd 100644 --- a/zun/tests/unit/scheduler/test_filter_scheduler.py +++ b/zun/tests/unit/scheduler/test_filter_scheduler.py @@ -12,6 +12,7 @@ import mock +from zun.common import context from zun.common import exception from zun import objects from zun.scheduler import filter_scheduler @@ -33,27 +34,46 @@ class FilterSchedulerTestCase(base.TestCase): def setUp(self): super(FilterSchedulerTestCase, self).setUp() + self.context = context.RequestContext('fake_user', 'fake_project') self.driver = self.driver_cls() + @mock.patch.object(objects.ComputeNode, 'list') @mock.patch.object(objects.ZunService, 'list_by_binary') @mock.patch('random.choice') def test_select_destinations(self, mock_random_choice, - mock_list_by_binary): + mock_list_by_binary, mock_compute_list): all_services = [FakeService('service1', 'host1'), FakeService('service2', 'host2'), FakeService('service3', 'host3'), FakeService('service4', 'host4')] - all_hosts = ['host1', 'host2', 'host3', 'host4'] def _return_services(*args, **kwargs): return all_services - mock_random_choice.side_effect = ['host3'] self.driver.servicegroup_api.service_is_up = mock.Mock( return_value=True) mock_list_by_binary.side_effect = _return_services test_container = utils.get_test_container() containers = [objects.Container(self.context, **test_container)] + node1 = objects.ComputeNode(self.context) + node1.cpus = 48 + node1.cpu_used = 0.0 + node1.hostname = 'host1' + node2 = objects.ComputeNode(self.context) + node2.cpus = 48 + node2.cpu_used = 0.0 + node2.hostname = 'host2' + node3 = objects.ComputeNode(self.context) + node3.cpus = 48 + node3.cpu_used = 0.0 + node3.hostname = 'host3' + node4 = objects.ComputeNode(self.context) + node4.cpus = 48 + node4.cpu_used = 0.0 + node4.hostname = 'host4' + nodes = [node1, node2, node3, node4] + mock_compute_list.return_value = nodes + mock_random_choice.side_effect = [node3] dests = self.driver.select_destinations(self.context, containers) self.assertEqual(1, len(dests)) @@ -61,13 +81,15 @@ class FilterSchedulerTestCase(base.TestCase): self.assertEqual('host3', host) self.assertIsNone(node) - calls = [mock.call(all_hosts)] + calls = [mock.call(nodes)] self.assertEqual(calls, mock_random_choice.call_args_list) + @mock.patch.object(objects.ComputeNode, 'list') @mock.patch.object(objects.ZunService, 'list_by_binary') @mock.patch('random.choice') def test_select_destinations_no_valid_host(self, mock_random_choice, - mock_list_by_binary): + mock_list_by_binary, + mock_compute_list): def _return_services(*args, **kwargs): return []