diff --git a/doc/source/user/filter-scheduler.rst b/doc/source/user/filter-scheduler.rst index 35cef69a0..8e4ef6222 100644 --- a/doc/source/user/filter-scheduler.rst +++ b/doc/source/user/filter-scheduler.rst @@ -29,6 +29,8 @@ There are many standard filter classes which may be used to host the instance are passed. * LabelFilter - filters hosts based on whether host has the CLI specified labels. +* ComputeFilter - filters hosts that are operational and enabled. In general, + you should always enable this filter. Configuring Filters ------------------- @@ -45,7 +47,7 @@ The default values for these settings in zun.conf are: :: --filter_scheduler.available_filters=zun.scheduler.filters.all_filters - --filter_scheduler.enabled_filters=RamFilter,CPUFilter + --filter_scheduler.enabled_filters=RamFilter,CPUFilter,ComputeFilter With this configuration, all filters in ``zun.scheduler.filters`` would be available, and by default the RamFilter and CPUFilter would be diff --git a/setup.cfg b/setup.cfg index 10fbf8c50..08803806a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,7 +72,7 @@ zun.database.migration_backend = zun.scheduler.driver = chance_scheduler = zun.scheduler.chance_scheduler:ChanceScheduler - fake_scheduler = zun.tests.unit.scheduler.fake_scheduler:FakeScheduler + fake_scheduler = zun.tests.unit.scheduler.fakes:FakeScheduler filter_scheduler = zun.scheduler.filter_scheduler:FilterScheduler zun.image.driver = diff --git a/zun/conf/scheduler.py b/zun/conf/scheduler.py index 297f21116..48bfc3b93 100644 --- a/zun/conf/scheduler.py +++ b/zun/conf/scheduler.py @@ -64,7 +64,8 @@ Related options: cfg.ListOpt("enabled_filters", default=[ "CPUFilter", - "RamFilter" + "RamFilter", + "ComputeFilter" ], help=""" Filters that the scheduler will use. diff --git a/zun/scheduler/filter_scheduler.py b/zun/scheduler/filter_scheduler.py index e0597e00a..6bc5dc473 100644 --- a/zun/scheduler/filter_scheduler.py +++ b/zun/scheduler/filter_scheduler.py @@ -21,7 +21,6 @@ from zun.common import exception from zun.common.i18n import _ import zun.conf from zun import objects -from zun.pci import stats as pci_stats from zun.scheduler import driver from zun.scheduler import filters from zun.scheduler.host_state import HostState @@ -44,10 +43,11 @@ class FilterScheduler(driver.Scheduler): def _schedule(self, context, container, extra_spec): """Picks a host according to filters.""" - hosts = self.hosts_up(context) + services = self._get_services_by_host(context) nodes = objects.ComputeNode.list(context) + hosts = services.keys() nodes = [node for node in nodes if node.hostname in hosts] - host_states = self.get_all_host_state(nodes) + host_states = self.get_all_host_state(nodes, services) hosts = self.filter_handler.get_filtered_objects(self.enabled_filters, host_states, container, @@ -102,17 +102,18 @@ class FilterScheduler(driver.Scheduler): def _load_filters(self): return CONF.scheduler.enabled_filters - def get_all_host_state(self, nodes): + def _get_services_by_host(self, context): + """Get a dict of services indexed by hostname""" + return {service.host: service + for service in objects.ZunService.list_by_binary( + context, + 'zun-compute')} + + def get_all_host_state(self, nodes, services): host_states = [] for node in nodes: host_state = HostState(node.hostname) - host_state.mem_total = node.mem_total - host_state.mem_used = node.mem_used - host_state.cpus = node.cpus - host_state.cpu_used = node.cpu_used - host_state.numa_topology = node.numa_topology - host_state.labels = node.labels - host_state.pci_stats = pci_stats.PciDeviceStats( - stats=node.pci_device_pools) + host_state.update(compute_node=node, + service=services.get(node.hostname)) host_states.append(host_state) return host_states diff --git a/zun/scheduler/filters/compute_filter.py b/zun/scheduler/filters/compute_filter.py new file mode 100644 index 000000000..1c25df3a1 --- /dev/null +++ b/zun/scheduler/filters/compute_filter.py @@ -0,0 +1,47 @@ +# 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.log import logging + +from zun.api import servicegroup +from zun.scheduler import filters + +LOG = logging.getLogger(__name__) + + +class ComputeFilter(filters.BaseHostFilter): + """Filter on active Compute nodes""" + + def __init__(self): + self.servicegroup_api = servicegroup.ServiceGroup() + super(ComputeFilter, self).__init__() + + # Host state does not change within a request + run_filter_once_per_request = True + + def host_passes(self, host_state, container, extra_spec): + """Returns True for only active compute nodes""" + service = host_state.service + if service.disabled: + LOG.debug('%(host_state)s is disabled, reason: %(reason)s', + {'host_state': host_state, + 'reason': service.disabled_reason or 'Unknow'}) + return False + else: + if not self.servicegroup_api.service_is_up(service): + LOG.warning('%(host_state)s has not been heard from in ' + 'a while', {'host_state': host_state}) + return False + return True diff --git a/zun/scheduler/host_state.py b/zun/scheduler/host_state.py index b6c20be09..e895743e6 100644 --- a/zun/scheduler/host_state.py +++ b/zun/scheduler/host_state.py @@ -10,6 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log.log import logging + +from zun.common import utils +from zun.pci import stats as pci_stats + +LOG = logging.getLogger(__name__) + class HostState(object): """Mutable and immutable information tracked for a host. @@ -28,6 +35,34 @@ class HostState(object): self.cpus = 0 self.cpu_used = 0 self.numa_topology = None + self.labels = None + self.pci_stats = None # Resource oversubscription values for the compute host: self.limits = {} + + def update(self, compute_node=None, service=None): + """Update information about a host""" + @utils.synchronized((self.hostname, compute_node)) + def _locked_update(self, compute_node, service): + if compute_node is not None: + LOG.debug('Update host state from compute node: %s', + compute_node) + self._update_from_compute_node(compute_node) + if service is not None: + LOG.debug('Update host state with service: %s', service) + self.service = service + + return _locked_update(self, compute_node, service) + + def _update_from_compute_node(self, compute_node): + """Update information about a host from a Compute object""" + self.mem_total = compute_node.mem_total + self.mem_free = compute_node.mem_free + self.mem_used = compute_node.mem_used + self.cpus = compute_node.cpus + self.cpu_used = compute_node.cpu_used + self.numa_topology = compute_node.numa_topology + self.labels = compute_node.labels + self.pci_stats = pci_stats.PciDeviceStats( + stats=compute_node.pci_device_pools) diff --git a/zun/tests/unit/scheduler/fake_scheduler.py b/zun/tests/unit/scheduler/fakes.py similarity index 60% rename from zun/tests/unit/scheduler/fake_scheduler.py rename to zun/tests/unit/scheduler/fakes.py index 443fc1662..f7ff9714c 100644 --- a/zun/tests/unit/scheduler/fake_scheduler.py +++ b/zun/tests/unit/scheduler/fakes.py @@ -11,9 +11,26 @@ # under the License. from zun.scheduler import driver +from zun.scheduler import host_state class FakeScheduler(driver.Scheduler): def select_destinations(self, context, containers): return [] + + +class FakeHostState(host_state.HostState): + def __init__(self, host, attribute_dict=None): + super(FakeHostState, self).__init__(host) + if attribute_dict: + for (key, val) in attribute_dict.items(): + setattr(self, key, val) + + +class FakeService(object): + + def __init__(self, name, host, disabled=False): + self.name = name + self.host = host + self.disabled = disabled diff --git a/zun/tests/unit/scheduler/filters/test_compute_filter.py b/zun/tests/unit/scheduler/filters/test_compute_filter.py new file mode 100644 index 000000000..6d8ec0625 --- /dev/null +++ b/zun/tests/unit/scheduler/filters/test_compute_filter.py @@ -0,0 +1,68 @@ +# 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 oslo_utils import timeutils + +from zun.common import context +from zun import objects +from zun.scheduler.filters import compute_filter +from zun.tests import base +from zun.tests.unit.scheduler import fakes + + +@mock.patch('zun.api.servicegroup.ServiceGroup.service_is_up') +class TestComputeFilter(base.TestCase): + + def setUp(self): + super(TestComputeFilter, self).setUp() + self.context = context.RequestContext('fake_user', 'fake_project') + + def test_compute_filter_manual_disable(self, service_up_mock): + filt_cls = compute_filter.ComputeFilter() + container = objects.Container(self.context) + extra_spec = {} + service = objects.ZunService(self.context) + service.disabled = True + service.disabled_reason = 'This is a reason!' + host = fakes.FakeHostState('host1', + {'service': service}) + self.assertFalse(filt_cls.host_passes(host, container, + extra_spec)) + self.assertFalse(service_up_mock.called) + + def test_compute_filter_sgapi_passes(self, service_up_mock): + filt_cls = compute_filter.ComputeFilter() + container = objects.Container(self.context) + service = objects.ZunService(self.context) + service.disabled = False + extra_spec = {} + host = fakes.FakeHostState('host2', + {'service': service}) + service_up_mock.return_value = True + self.assertTrue(filt_cls.host_passes(host, container, + extra_spec)) + service_up_mock.assert_called_once_with(service) + + def test_compute_filter_sgapi_fails(self, service_up_mock): + filts_cls = compute_filter.ComputeFilter() + container = objects.Container(self.context) + service = objects.ZunService(self.context) + service.disabled = False + service.updated_at = timeutils.utcnow() + extra_spec = {} + host = fakes.FakeHostState('host3', + {'service': service}) + service_up_mock.return_value = False + self.assertFalse(filts_cls.host_passes(host, container, + extra_spec)) + service_up_mock.assert_called_once_with(service) diff --git a/zun/tests/unit/scheduler/filters/test_cpu_filter.py b/zun/tests/unit/scheduler/filters/test_cpu_filter.py index cf5f8e356..f02ebfa4c 100644 --- a/zun/tests/unit/scheduler/filters/test_cpu_filter.py +++ b/zun/tests/unit/scheduler/filters/test_cpu_filter.py @@ -13,8 +13,8 @@ from zun.common import context from zun import objects from zun.scheduler.filters import cpu_filter -from zun.scheduler.host_state import HostState from zun.tests import base +from zun.tests.unit.scheduler import fakes class TestCPUFilter(base.TestCase): @@ -27,7 +27,7 @@ class TestCPUFilter(base.TestCase): self.filt_cls = cpu_filter.CPUFilter() container = objects.Container(self.context) container.cpu = 5.0 - host = HostState('testhost') + host = fakes.FakeHostState('testhost') host.cpus = 8 host.cpu_used = 0.0 extra_spec = {} @@ -37,7 +37,7 @@ class TestCPUFilter(base.TestCase): self.filt_cls = cpu_filter.CPUFilter() container = objects.Container(self.context) container.cpu = 8.0 - host = HostState('testhost') + host = fakes.FakeHostState('testhost') host.cpus = 5 host.cpu_used = 2.0 extra_spec = {} diff --git a/zun/tests/unit/scheduler/filters/test_ram_filter.py b/zun/tests/unit/scheduler/filters/test_ram_filter.py index 5b28e7e73..add926bee 100644 --- a/zun/tests/unit/scheduler/filters/test_ram_filter.py +++ b/zun/tests/unit/scheduler/filters/test_ram_filter.py @@ -13,8 +13,8 @@ from zun.common import context from zun import objects from zun.scheduler.filters import ram_filter -from zun.scheduler.host_state import HostState from zun.tests import base +from zun.tests.unit.scheduler import fakes class TestRamFilter(base.TestCase): @@ -27,7 +27,7 @@ class TestRamFilter(base.TestCase): self.filt_cls = ram_filter.RamFilter() container = objects.Container(self.context) container.memory = '1024M' - host = HostState('testhost') + host = fakes.FakeHostState('testhost') host.mem_total = 1024 * 128 host.mem_used = 1024 extra_spec = {} @@ -37,7 +37,7 @@ class TestRamFilter(base.TestCase): self.filt_cls = ram_filter.RamFilter() container = objects.Container(self.context) container.memory = '4096M' - host = HostState('testhost') + host = fakes.FakeHostState('testhost') host.mem_total = 1024 * 128 host.mem_used = 1024 * 127 extra_spec = {} diff --git a/zun/tests/unit/scheduler/test_client.py b/zun/tests/unit/scheduler/test_client.py index a2d7dfa38..223cf107d 100644 --- a/zun/tests/unit/scheduler/test_client.py +++ b/zun/tests/unit/scheduler/test_client.py @@ -17,7 +17,7 @@ from oslo_config import cfg from zun.scheduler import client as scheduler_client from zun.scheduler import filter_scheduler from zun.tests import base -from zun.tests.unit.scheduler import fake_scheduler +from zun.tests.unit.scheduler import fakes CONF = cfg.CONF @@ -37,7 +37,7 @@ class SchedulerClientTestCase(base.TestCase): def test_init_using_custom_schedulerdriver(self): CONF.set_override('driver', 'fake_scheduler', group='scheduler') driver = self.client_cls().driver - self.assertIsInstance(driver, fake_scheduler.FakeScheduler) + self.assertIsInstance(driver, fakes.FakeScheduler) @mock.patch('zun.scheduler.filter_scheduler.FilterScheduler' '.select_destinations') diff --git a/zun/tests/unit/scheduler/test_filter_scheduler.py b/zun/tests/unit/scheduler/test_filter_scheduler.py index fe8a2fe12..b2612ee7c 100644 --- a/zun/tests/unit/scheduler/test_filter_scheduler.py +++ b/zun/tests/unit/scheduler/test_filter_scheduler.py @@ -12,19 +12,14 @@ import mock +from zun.api import servicegroup from zun.common import context from zun.common import exception from zun import objects from zun.scheduler import filter_scheduler from zun.tests import base from zun.tests.unit.db import utils - - -class FakeService(object): - - def __init__(self, name, host): - self.name = name - self.host = host +from zun.tests.unit.scheduler.fakes import FakeService class FilterSchedulerTestCase(base.TestCase): @@ -37,11 +32,13 @@ class FilterSchedulerTestCase(base.TestCase): self.context = context.RequestContext('fake_user', 'fake_project') self.driver = self.driver_cls() + @mock.patch.object(servicegroup.ServiceGroup, 'service_is_up') @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_compute_list): + mock_list_by_binary, mock_compute_list, + mock_service_is_up): all_services = [FakeService('service1', 'host1'), FakeService('service2', 'host2'), FakeService('service3', 'host3'), @@ -60,6 +57,7 @@ class FilterSchedulerTestCase(base.TestCase): node1.cpu_used = 0.0 node1.mem_total = 1024 * 128 node1.mem_used = 1024 * 4 + node1.mem_free = 1024 * 124 node1.hostname = 'host1' node1.numa_topology = None node1.labels = {} @@ -69,6 +67,7 @@ class FilterSchedulerTestCase(base.TestCase): node2.cpu_used = 0.0 node2.mem_total = 1024 * 128 node2.mem_used = 1024 * 4 + node2.mem_free = 1024 * 124 node2.hostname = 'host2' node2.numa_topology = None node2.labels = {} @@ -78,6 +77,7 @@ class FilterSchedulerTestCase(base.TestCase): node3.cpu_used = 0.0 node3.mem_total = 1024 * 128 node3.mem_used = 1024 * 4 + node3.mem_free = 1024 * 124 node3.hostname = 'host3' node3.numa_topology = None node3.labels = {} @@ -87,6 +87,7 @@ class FilterSchedulerTestCase(base.TestCase): node4.cpu_used = 0.0 node4.mem_total = 1024 * 128 node4.mem_used = 1024 * 4 + node4.mem_free = 1024 * 124 node4.hostname = 'host4' node4.numa_topology = None node4.labels = {} @@ -97,6 +98,7 @@ class FilterSchedulerTestCase(base.TestCase): def side_effect(hosts): return hosts[2] mock_random_choice.side_effect = side_effect + mock_service_is_up.return_value = True extra_spec = {} dests = self.driver.select_destinations(self.context, containers, extra_spec) diff --git a/zun/tests/unit/scheduler/test_scheduler.py b/zun/tests/unit/scheduler/test_scheduler.py index 7457a36bf..ef8288b16 100644 --- a/zun/tests/unit/scheduler/test_scheduler.py +++ b/zun/tests/unit/scheduler/test_scheduler.py @@ -17,13 +17,13 @@ import mock from zun import objects from zun.tests import base -from zun.tests.unit.scheduler import fake_scheduler +from zun.tests.unit.scheduler import fakes class SchedulerTestCase(base.TestCase): """Test case for base scheduler driver class.""" - driver_cls = fake_scheduler.FakeScheduler + driver_cls = fakes.FakeScheduler def setUp(self): super(SchedulerTestCase, self).setUp()