diff options
21 files changed, 455 insertions, 70 deletions
diff --git a/doc/source/admin/scheduling.rst b/doc/source/admin/scheduling.rst index 9071c92ac9..353514ab55 100644 --- a/doc/source/admin/scheduling.rst +++ b/doc/source/admin/scheduling.rst @@ -1049,6 +1049,37 @@ Otherwise, it will fall back to the more than one value is found for a host in aggregate metadata, the minimum value will be used. +``HypervisorVersionWeigher`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 28.0.0 (Bobcat) + +Weigh hosts by their relative hypervisor version reported by the virt driver. + +While the hypervisor_version filed for all virt drivers is an int, +each nova virt driver uses a different algorithm to convert the hypervisor-specific +version sequence into an int. As such the values are not directly comparable between +hosts with different hypervisors. + +For example, the ironic virt driver uses the ironic API micro-version as the hypervisor +version for a given node. The libvirt driver uses the libvirt version +i.e. Libvirt `7.1.123` becomes `700100123` vs Ironic `1.82` becomes `1` +Hyper-V `6.3` becomes `6003`. + +If you have a mixed virt driver deployment in the ironic vs non-ironic +case nothing special needs to be done. ironic nodes are scheduled using custom +resource classes so ironic flavors will never match non-ironic compute nodes. + +If a deployment has multiple non-ironic virt drivers it is recommended to use aggregates +to group hosts by virt driver. While this is not strictly required, it is +desirable to avoid bias towards one virt driver. +see :ref:`filtering_hosts_by_isolating_aggregates` and :ref:`AggregateImagePropertiesIsolation` +for more information. + +The default behavior of the HypervisorVersionWeigher is to select newer hosts. +If you prefer to invert the behavior set the +:oslo.config:option:`filter_scheduler.hypervisor_version_weight_multiplier` option +to a negative number and the weighing has the opposite effect of the default. Utilization-aware scheduling ---------------------------- diff --git a/doc/source/reference/isolate-aggregates.rst b/doc/source/reference/isolate-aggregates.rst index f5487df912..7b493f4db9 100644 --- a/doc/source/reference/isolate-aggregates.rst +++ b/doc/source/reference/isolate-aggregates.rst @@ -13,6 +13,8 @@ License for the specific language governing permissions and limitations under the License. +.. _filtering_hosts_by_isolating_aggregates: + Filtering hosts by isolating aggregates ======================================= diff --git a/nova/conf/scheduler.py b/nova/conf/scheduler.py index c75bd07c5b..c7aa2ad76d 100644 --- a/nova/conf/scheduler.py +++ b/nova/conf/scheduler.py @@ -464,6 +464,49 @@ Possible values: * An integer or float value, where the value corresponds to the multipler ratio for this weigher. """), + cfg.FloatOpt("hypervisor_version_weight_multiplier", + default=1.0, + help=""" +Hypervisor Version weight multiplier ratio. + +The multiplier is used for weighting hosts based on the reported +hypervisor version. +Negative numbers indicate preferring older hosts, +the default is to prefer newer hosts to aid with upgrades. + +Possible values: + +* An integer or float value, where the value corresponds to the multiplier + ratio for this weigher. + +Example: + +* Strongly prefer older hosts + + .. code-block:: ini + + [filter_scheduler] + hypervisor_version_weight_multiplier=-1000 + + +* Moderately prefer new hosts + + .. code-block:: ini + + [filter_scheduler] + hypervisor_version_weight_multiplier=2.5 + +* Disable weigher influence + + .. code-block:: ini + + [filter_scheduler] + hypervisor_version_weight_multiplier=0 + +Related options: + +* ``[filter_scheduler] weight_classes`` +"""), cfg.FloatOpt("io_ops_weight_multiplier", default=-1.0, help=""" diff --git a/nova/scheduler/weights/hypervisor_version.py b/nova/scheduler/weights/hypervisor_version.py new file mode 100644 index 0000000000..0cd7b0a824 --- /dev/null +++ b/nova/scheduler/weights/hypervisor_version.py @@ -0,0 +1,39 @@ +# 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. + +""" +Hypervisor Version Weigher. Weigh hosts by their relative hypervior version. + +The default is to select newer hosts. If you prefer +to invert the behavior set the 'hypervisor_version_weight_multiplier' option +to a negative number and the weighing has the opposite effect of the default. +""" + +import nova.conf +from nova.scheduler import utils +from nova.scheduler import weights + +CONF = nova.conf.CONF + + +class HypervisorVersionWeigher(weights.BaseHostWeigher): + + def weight_multiplier(self, host_state): + """Override the weight multiplier.""" + return utils.get_weight_multiplier( + host_state, 'hypervisor_version_weight_multiplier', + CONF.filter_scheduler.hypervisor_version_weight_multiplier) + + def _weigh_object(self, host_state, weight_properties): + """Higher weights win. We want newer hosts by default.""" + # convert None to 0 + return host_state.hypervisor_version or 0 diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py index 5fd893e7dc..abfc3ecc6c 100644 --- a/nova/tests/fixtures/nova.py +++ b/nova/tests/fixtures/nova.py @@ -1318,6 +1318,77 @@ class PrivsepFixture(fixtures.Fixture): nova.privsep.sys_admin_pctxt, 'client_mode', False)) +class CGroupsFixture(fixtures.Fixture): + """Mocks checks made for available subsystems on the host's control group. + + The fixture mocks all calls made on the host to verify the capabilities + provided by its kernel. Through this, one can simulate the underlying + system hosts work on top of and have tests react to expected outcomes from + such. + + Use sample: + >>> cgroups = self.useFixture(CGroupsFixture()) + >>> cgroups = self.useFixture(CGroupsFixture(version=2)) + >>> cgroups = self.useFixture(CGroupsFixture()) + ... cgroups.version = 2 + + :attr version: Arranges mocks to simulate the host interact with nova + following the given version of cgroups. + Available values are: + - 0: All checks related to cgroups will return False. + - 1: Checks related to cgroups v1 will return True. + - 2: Checks related to cgroups v2 will return True. + Defaults to 1. + """ + + def __init__(self, version=1): + self._cpuv1 = None + self._cpuv2 = None + + self._version = version + + @property + def version(self): + return self._version + + @version.setter + def version(self, value): + self._version = value + self._update_mocks() + + def setUp(self): + super().setUp() + self._cpuv1 = self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.host.Host._has_cgroupsv1_cpu_controller')).mock + self._cpuv2 = self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.host.Host._has_cgroupsv2_cpu_controller')).mock + self._update_mocks() + + def _update_mocks(self): + if not self._cpuv1: + return + + if not self._cpuv2: + return + + if self.version == 0: + self._cpuv1.return_value = False + self._cpuv2.return_value = False + return + + if self.version == 1: + self._cpuv1.return_value = True + self._cpuv2.return_value = False + return + + if self.version == 2: + self._cpuv1.return_value = False + self._cpuv2.return_value = True + return + + raise ValueError(f"Unknown cgroups version: '{self.version}'.") + + class NoopQuotaDriverFixture(fixtures.Fixture): """A fixture to run tests using the NoopQuotaDriver. diff --git a/nova/tests/functional/libvirt/base.py b/nova/tests/functional/libvirt/base.py index 7b6ee10631..1ee46a3217 100644 --- a/nova/tests/functional/libvirt/base.py +++ b/nova/tests/functional/libvirt/base.py @@ -43,6 +43,7 @@ class ServersTestBase(integrated_helpers._IntegratedTestBase): super(ServersTestBase, self).setUp() self.useFixture(nova_fixtures.LibvirtImageBackendFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) self.libvirt = self.useFixture(nova_fixtures.LibvirtFixture()) self.useFixture(nova_fixtures.OSBrickFixture()) diff --git a/nova/tests/functional/libvirt/test_evacuate.py b/nova/tests/functional/libvirt/test_evacuate.py index 92d7ffba29..0e89a3cdb6 100644 --- a/nova/tests/functional/libvirt/test_evacuate.py +++ b/nova/tests/functional/libvirt/test_evacuate.py @@ -429,6 +429,7 @@ class _LibvirtEvacuateTest(integrated_helpers.InstanceHelperMixin): self.useFixture(nova_fixtures.NeutronFixture(self)) self.useFixture(nova_fixtures.GlanceFixture(self)) self.useFixture(func_fixtures.PlacementFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) fake_network.set_stub_network_methods(self) api_fixture = self.useFixture( diff --git a/nova/tests/functional/libvirt/test_vpmem.py b/nova/tests/functional/libvirt/test_vpmem.py index 1200f80357..cb524fe8b6 100644 --- a/nova/tests/functional/libvirt/test_vpmem.py +++ b/nova/tests/functional/libvirt/test_vpmem.py @@ -77,6 +77,7 @@ class VPMEMTestBase(integrated_helpers.LibvirtProviderUsageBaseTestCase): 'nova.privsep.libvirt.get_pmem_namespaces', return_value=self.fake_pmem_namespaces)) self.useFixture(nova_fixtures.LibvirtImageBackendFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) self.useFixture(fixtures.MockPatch( 'nova.virt.libvirt.LibvirtDriver._get_local_gb_info', return_value={'total': 128, diff --git a/nova/tests/functional/regressions/test_bug_1595962.py b/nova/tests/functional/regressions/test_bug_1595962.py index 94421a81f9..9232eea335 100644 --- a/nova/tests/functional/regressions/test_bug_1595962.py +++ b/nova/tests/functional/regressions/test_bug_1595962.py @@ -47,6 +47,7 @@ class TestSerialConsoleLiveMigrate(test.TestCase): 'nova.virt.libvirt.guest.libvirt', fakelibvirt)) self.useFixture(nova_fixtures.LibvirtFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) self.admin_api = api_fixture.admin_api self.api = api_fixture.api diff --git a/nova/tests/functional/regressions/test_bug_1995153.py b/nova/tests/functional/regressions/test_bug_1995153.py index c897156d99..f4e61d06df 100644 --- a/nova/tests/functional/regressions/test_bug_1995153.py +++ b/nova/tests/functional/regressions/test_bug_1995153.py @@ -68,16 +68,17 @@ class Bug1995153RegressionTest( side_effect=host_pass_mock)).mock def test_socket_policy_bug_1995153(self): - """The numa_usage_from_instance_numa() method in hardware.py saves the - host NUMAToplogy object with NUMACells that have no `socket` set. This - was an omission in the original implementation of the `socket` PCI NUMA - affinity policy. The consequence is that any code path that calls into - numa_usage_from_instance_numa() will clobber the host NUMA topology in - the database with a socket-less version. Booting an instance with NUMA - toplogy will do that, for example. If then a second instance is booted - with the `socket` PCI NUMA affinity policy, it will read the - socket-less host NUMATopology from the database, and error out with a - NotImplementedError. This is bug 1995153. + """Previously, the numa_usage_from_instance_numa() method in + hardware.py saved the host NUMAToplogy object with NUMACells that have + no `socket` set. This was an omission in the original implementation of + the `socket` PCI NUMA affinity policy. The consequence was that any + code path that called into numa_usage_from_instance_numa() would + clobber the host NUMA topology in the database with a socket-less + version. Booting an instance with NUMA toplogy would do that, for + example. If then a second instance was booted with the `socket` PCI + NUMA affinity policy, it would read the socket-less host NUMATopology + from the database, and error out with a NotImplementedError. This was + bug 1995153. Demonstrate that this is fixed. """ host_info = fakelibvirt.HostInfo( cpu_nodes=2, cpu_sockets=1, cpu_cores=2, cpu_threads=2, @@ -92,18 +93,15 @@ class Bug1995153RegressionTest( 'pci_passthrough:alias': '%s:1' % self.ALIAS_NAME, 'hw:pci_numa_affinity_policy': 'socket' } - # Boot a first instance with a guest NUMA topology to run the buggy - # code in numa_usage_from_instance_numa() and save the socket-less host - # NUMATopology to the database. + # Boot a first instance with a guest NUMA topology to run the + # numa_usage_from_instance_numa() and update the host NUMATopology in + # the database. self._create_server( flavor_id=self._create_flavor( extra_spec={'hw:cpu_policy': 'dedicated'})) - # FIXME(artom) Attempt to boot an instance with the `socket` PCI NUMA - # affinity policy and observe the fireworks. + # Boot an instance with the `socket` PCI NUMA affinity policy and + # assert that it boots correctly now. flavor_id = self._create_flavor(extra_spec=extra_spec) - server = self._create_server(flavor_id=flavor_id, - expected_state='ERROR') - self.assertIn('fault', server) - self.assertIn('NotImplementedError', server['fault']['message']) + self._create_server(flavor_id=flavor_id) self.assertTrue(self.mock_filter.called) diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index bb9b726912..36bcd368dc 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -5672,6 +5672,7 @@ class ComputeTestCase(BaseTestCase, pagesize=2048, cpu_usage=2, memory_usage=0, + socket=0, pinned_cpus=set([1, 2]), siblings=[set([1]), set([2])], mempages=[objects.NUMAPagesTopology( @@ -5687,6 +5688,7 @@ class ComputeTestCase(BaseTestCase, pagesize=2048, memory_usage=0, cpu_usage=0, + socket=0, siblings=[set([3]), set([4])], mempages=[objects.NUMAPagesTopology( size_kb=2048, total=256, used=0)]) diff --git a/nova/tests/unit/compute/test_resource_tracker.py b/nova/tests/unit/compute/test_resource_tracker.py index b6770cb5b8..919dcb8334 100644 --- a/nova/tests/unit/compute/test_resource_tracker.py +++ b/nova/tests/unit/compute/test_resource_tracker.py @@ -181,6 +181,7 @@ _NUMA_HOST_TOPOLOGIES = { memory=_2MB, cpu_usage=0, memory_usage=0, + socket=0, mempages=[_NUMA_PAGE_TOPOLOGIES['2mb*1024']], siblings=[set([1]), set([2])], pinned_cpus=set()), @@ -191,6 +192,7 @@ _NUMA_HOST_TOPOLOGIES = { memory=_2MB, cpu_usage=0, memory_usage=0, + socket=0, mempages=[_NUMA_PAGE_TOPOLOGIES['2mb*1024']], siblings=[set([3]), set([4])], pinned_cpus=set())]), diff --git a/nova/tests/unit/scheduler/fakes.py b/nova/tests/unit/scheduler/fakes.py index 658c82c20e..f5dcf87e4a 100644 --- a/nova/tests/unit/scheduler/fakes.py +++ b/nova/tests/unit/scheduler/fakes.py @@ -34,6 +34,7 @@ NUMA_TOPOLOGY = objects.NUMATopology(cells=[ memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=16, total=387184, used=0), @@ -46,6 +47,7 @@ NUMA_TOPOLOGY = objects.NUMATopology(cells=[ memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=1548736, used=0), diff --git a/nova/tests/unit/scheduler/weights/test_weights_hypervisor_version.py b/nova/tests/unit/scheduler/weights/test_weights_hypervisor_version.py new file mode 100644 index 0000000000..c6e4abd4cd --- /dev/null +++ b/nova/tests/unit/scheduler/weights/test_weights_hypervisor_version.py @@ -0,0 +1,97 @@ +# 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 hypervisor version weights. +""" + +from nova.scheduler import weights +from nova.scheduler.weights import hypervisor_version +from nova import test +from nova.tests.unit.scheduler import fakes + + +class HypervisorVersionWeigherTestCase(test.NoDBTestCase): + def setUp(self): + super().setUp() + self.weight_handler = weights.HostWeightHandler() + self.weighers = [hypervisor_version.HypervisorVersionWeigher()] + + def _get_weighed_host(self, hosts, weight_properties=None): + if weight_properties is None: + weight_properties = {} + return self.weight_handler.get_weighed_objects(self.weighers, + hosts, weight_properties)[0] + + def _get_all_hosts(self): + host_values = [ + ('host1', 'node1', {'hypervisor_version': 1}), + ('host2', 'node2', {'hypervisor_version': 200}), + ('host3', 'node3', {'hypervisor_version': 100}), + ('host4', 'node4', {'hypervisor_version': 1000}), + ] + return [fakes.FakeHostState(host, node, values) + for host, node, values in host_values] + + def test_multiplier_default(self): + hostinfo_list = self._get_all_hosts() + weighed_host = self._get_weighed_host(hostinfo_list) + self.assertEqual(1.0, weighed_host.weight) + self.assertEqual('host4', weighed_host.obj.host) + + def test_multiplier_default_full_ordering(self): + hostinfo_list = self._get_all_hosts() + weighed_hosts = self.weight_handler.get_weighed_objects( + self.weighers, hostinfo_list, {} + ) + expected_hosts = [fakes.FakeHostState(host, node, values) + for host, node, values in [ + ('host4', 'node4', {'hypervisor_version': 1000}), + ('host2', 'node2', {'hypervisor_version': 200}), + ('host3', 'node3', {'hypervisor_version': 100}), + ('host1', 'node1', {'hypervisor_version': 1}), + ]] + for actual, expected in zip( + weighed_hosts, + expected_hosts + ): + self.assertEqual(actual.obj.host, expected.host) + + def test_multiplier_none(self): + multi = 0.0 + self.flags( + hypervisor_version_weight_multiplier=multi, + group='filter_scheduler' + ) + hostinfo_list = self._get_all_hosts() + weighed_host = self._get_weighed_host(hostinfo_list) + self.assertEqual(multi, weighed_host.weight) + + def test_multiplier_positive(self): + multi = 2.0 + self.flags( + hypervisor_version_weight_multiplier=multi, + group='filter_scheduler' + ) + hostinfo_list = self._get_all_hosts() + weighed_host = self._get_weighed_host(hostinfo_list) + self.assertEqual(1.0 * multi, weighed_host.weight) + self.assertEqual('host4', weighed_host.obj.host) + + def test_multiplier_negative(self): + multi = -1.0 + self.flags( + hypervisor_version_weight_multiplier=multi, + group='filter_scheduler' + ) + hostinfo_list = self._get_all_hosts() + weighed_host = self._get_weighed_host(hostinfo_list) + self.assertEqual('host1', weighed_host.obj.host) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 2b58c7df8b..66dbf795d8 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -740,6 +740,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, imagebackend.Image._get_driver_format) self.libvirt = self.useFixture(nova_fixtures.LibvirtFixture()) + self.cgroups = self.useFixture(nova_fixtures.CGroupsFixture()) # ensure tests perform the same on all host architectures; this is # already done by the fakelibvirt fixture but we want to change the @@ -3093,9 +3094,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, 'fake-flavor', 'fake-image-meta').obj_to_primitive()) @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_fits(self, is_able): + def test_get_guest_config_numa_host_instance_fits(self): self.flags(cpu_shared_set=None, cpu_dedicated_set=None, group='compute') instance_ref = objects.Instance(**self.test_instance) @@ -3133,9 +3132,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) @mock.patch('nova.privsep.utils.supports_direct_io', new=mock.Mock(return_value=True)) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_no_fit(self, is_able): + def test_get_guest_config_numa_host_instance_no_fit(self): instance_ref = objects.Instance(**self.test_instance) image_meta = objects.ImageMeta.from_dict(self.test_image_meta) flavor = objects.Flavor(memory_mb=4096, vcpus=4, root_gb=496, @@ -3563,10 +3560,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, host_topology, inst_topology, numa_tune) @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_pci_no_numa_info( - self, is_able): + def test_get_guest_config_numa_host_instance_pci_no_numa_info(self): self.flags(cpu_shared_set='3', cpu_dedicated_set=None, group='compute') @@ -3620,10 +3614,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) @mock.patch('nova.privsep.utils.supports_direct_io', new=mock.Mock(return_value=True)) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_2pci_no_fit( - self, is_able): + def test_get_guest_config_numa_host_instance_2pci_no_fit(self): self.flags(cpu_shared_set='3', cpu_dedicated_set=None, group='compute') instance_ref = objects.Instance(**self.test_instance) @@ -3740,10 +3731,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, None) @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_fit_w_cpu_pinset( - self, is_able): + def test_get_guest_config_numa_host_instance_fit_w_cpu_pinset(self): self.flags(cpu_shared_set='2-3', cpu_dedicated_set=None, group='compute') @@ -3782,10 +3770,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.cpu.numa) @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_non_numa_host_instance_topo( - self, is_able): + def test_get_guest_config_non_numa_host_instance_topo(self): instance_topology = objects.InstanceNUMATopology(cells=[ objects.InstanceNUMACell( id=0, cpuset=set([0]), pcpuset=set(), memory=1024), @@ -3833,10 +3818,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, numa_cfg_cell.memory) @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_topo( - self, is_able): + def test_get_guest_config_numa_host_instance_topo(self): self.flags(cpu_shared_set='0-5', cpu_dedicated_set=None, group='compute') @@ -7310,9 +7292,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, [], image_meta, disk_info) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_with_cpu_quota(self, is_able): + def test_get_guest_config_with_cpu_quota(self): self.flags(virt_type='kvm', group='libvirt') drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) @@ -7648,9 +7628,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.flags(images_type='rbd', group='libvirt') self._test_get_guest_config_disk_cachemodes('rbd') - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_with_bogus_cpu_quota(self, is_able): + def test_get_guest_config_with_bogus_cpu_quota(self): self.flags(virt_type='kvm', group='libvirt') drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) @@ -7668,9 +7646,10 @@ class LibvirtConnTestCase(test.NoDBTestCase, drvr._get_guest_config, instance_ref, [], image_meta, disk_info) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=False) - def test_get_update_guest_cputune(self, is_able): + def test_get_update_guest_cputune(self): + # No CPU controller on the host + self.cgroups.version = 0 + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) instance_ref = objects.Instance(**self.test_instance) instance_ref.flavor.extra_specs = {'quota:cpu_shares': '10000', @@ -22348,6 +22327,7 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): self.flags(sysinfo_serial="none", group="libvirt") self.flags(instances_path=self.useFixture(fixtures.TempDir()).path) self.useFixture(nova_fixtures.LibvirtFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) os_vif.initialize() self.drvr = libvirt_driver.LibvirtDriver( diff --git a/nova/tests/unit/virt/libvirt/test_host.py b/nova/tests/unit/virt/libvirt/test_host.py index 631b10d81a..a76dc83105 100644 --- a/nova/tests/unit/virt/libvirt/test_host.py +++ b/nova/tests/unit/virt/libvirt/test_host.py @@ -1619,25 +1619,59 @@ Active: 8381604 kB self.host.compare_cpu("cpuxml") mock_compareCPU.assert_called_once_with("cpuxml", 0) - def test_is_cpu_control_policy_capable_ok(self): + def test_is_cpu_control_policy_capable_via_neither(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=0)) + self.assertFalse(self.host.is_cpu_control_policy_capable()) + + def test_is_cpu_control_policy_capable_via_cgroupsv1(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=1)) + self.assertTrue(self.host.is_cpu_control_policy_capable()) + + def test_is_cpu_control_policy_capable_via_cgroupsv2(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=2)) + self.assertTrue(self.host.is_cpu_control_policy_capable()) + + def test_has_cgroupsv1_cpu_controller_ok(self): m = mock.mock_open( - read_data="""cg /cgroup/cpu,cpuacct cg opt1,cpu,opt3 0 0 -cg /cgroup/memory cg opt1,opt2 0 0 -""") - with mock.patch('builtins.open', m, create=True): - self.assertTrue(self.host.is_cpu_control_policy_capable()) + read_data=( + "cg /cgroup/cpu,cpuacct cg opt1,cpu,opt3 0 0" + "cg /cgroup/memory cg opt1,opt2 0 0" + ) + ) + with mock.patch("builtins.open", m, create=True): + self.assertTrue(self.host._has_cgroupsv1_cpu_controller()) - def test_is_cpu_control_policy_capable_ko(self): + def test_has_cgroupsv1_cpu_controller_ko(self): m = mock.mock_open( - read_data="""cg /cgroup/cpu,cpuacct cg opt1,opt2,opt3 0 0 -cg /cgroup/memory cg opt1,opt2 0 0 -""") - with mock.patch('builtins.open', m, create=True): - self.assertFalse(self.host.is_cpu_control_policy_capable()) + read_data=( + "cg /cgroup/cpu,cpuacct cg opt1,opt2,opt3 0 0" + "cg /cgroup/memory cg opt1,opt2 0 0" + ) + ) + with mock.patch("builtins.open", m, create=True): + self.assertFalse(self.host._has_cgroupsv1_cpu_controller()) - @mock.patch('builtins.open', side_effect=IOError) - def test_is_cpu_control_policy_capable_ioerror(self, mock_open): - self.assertFalse(self.host.is_cpu_control_policy_capable()) + @mock.patch("builtins.open", side_effect=IOError) + def test_has_cgroupsv1_cpu_controller_ioerror(self, _): + self.assertFalse(self.host._has_cgroupsv1_cpu_controller()) + + def test_has_cgroupsv2_cpu_controller_ok(self): + m = mock.mock_open( + read_data="cpuset cpu io memory hugetlb pids rdma misc" + ) + with mock.patch("builtins.open", m, create=True): + self.assertTrue(self.host._has_cgroupsv2_cpu_controller()) + + def test_has_cgroupsv2_cpu_controller_ko(self): + m = mock.mock_open( + read_data="memory pids" + ) + with mock.patch("builtins.open", m, create=True): + self.assertFalse(self.host._has_cgroupsv2_cpu_controller()) + + @mock.patch("builtins.open", side_effect=IOError) + def test_has_cgroupsv2_cpu_controller_ioerror(self, _): + self.assertFalse(self.host._has_cgroupsv2_cpu_controller()) def test_get_canonical_machine_type(self): # this test relies on configuration from the FakeLibvirtFixture diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index 753ee41550..ab51a3e26c 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -2023,6 +2023,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=256, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=32768, used=0), @@ -2036,6 +2037,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=256, cpu_usage=0, memory_usage=0, + socket=1, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=32768, used=64), @@ -2049,6 +2051,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=2, cpu_usage=0, memory_usage=0, + socket=2, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=16)], @@ -2130,6 +2133,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=160, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=32768, used=32), @@ -2170,6 +2174,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=1024, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], @@ -2181,6 +2186,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], @@ -2192,6 +2198,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], @@ -2258,6 +2265,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=1024, cpu_usage=2, memory_usage=512, + socket=0, mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], siblings=[set([0]), set([1]), set([2]), set([3])], @@ -2269,6 +2277,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=512, cpu_usage=1, memory_usage=512, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], @@ -2280,6 +2289,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=256, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[ objects.NUMAPagesTopology(size_kb=4, total=512, used=0)], @@ -2330,6 +2340,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[objects.NUMAPagesTopology( size_kb=2048, total=512, used=128, @@ -2342,6 +2353,7 @@ class NUMATopologyTest(test.NoDBTestCase): memory=512, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), mempages=[objects.NUMAPagesTopology( size_kb=1048576, total=5, used=2, @@ -2606,6 +2618,7 @@ class VirtNUMAHostTopologyTestCase(test.NoDBTestCase): memory=2048, cpu_usage=2, memory_usage=2048, + socket=0, pinned_cpus=set(), mempages=[objects.NUMAPagesTopology( size_kb=4, total=524288, used=0)], @@ -2616,6 +2629,7 @@ class VirtNUMAHostTopologyTestCase(test.NoDBTestCase): memory=2048, cpu_usage=2, memory_usage=2048, + socket=0, pinned_cpus=set(), mempages=[objects.NUMAPagesTopology( size_kb=4, total=524288, used=0)], @@ -4162,6 +4176,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0]), set([1]), set([2]), set([3])], mempages=[objects.NUMAPagesTopology( @@ -4191,6 +4206,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set([0, 1, 3]), mempages=[objects.NUMAPagesTopology( size_kb=4, total=524288, used=0)], @@ -4220,6 +4236,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0]), set([1]), set([2]), set([3])], mempages=[objects.NUMAPagesTopology( @@ -4248,6 +4265,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0, 2]), set([1, 3])], mempages=[objects.NUMAPagesTopology( @@ -4274,6 +4292,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set([0, 1, 2, 3]), siblings=[set([0, 2]), set([1, 3])], mempages=[objects.NUMAPagesTopology( @@ -4300,6 +4319,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0]), set([1]), set([2]), set([3])], mempages=[objects.NUMAPagesTopology( @@ -4326,6 +4346,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set([0, 1, 2, 3]), siblings=[set([0]), set([1]), set([2]), set([3])], mempages=[objects.NUMAPagesTopology( @@ -4355,6 +4376,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set([2]), siblings=[set([0, 4]), set([1, 5]), set([2, 6]), set([3, 7])], mempages=[objects.NUMAPagesTopology( @@ -4385,6 +4407,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=2, memory_usage=0, + socket=0, pinned_cpus=set([2, 6, 7]), siblings=[set([0, 4]), set([1, 5]), set([2, 6]), set([3, 7])], mempages=[objects.NUMAPagesTopology( @@ -4417,6 +4440,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): cpu_usage=2, memory_usage=0, pinned_cpus=set(), + socket=0, siblings=[{cpu} for cpu in range(8)], mempages=[objects.NUMAPagesTopology( size_kb=4, total=524288, used=0)] @@ -4450,6 +4474,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=2, memory_usage=0, + socket=0, pinned_cpus=set([0, 1, 2, 3]), siblings=[{cpu} for cpu in range(8)], mempages=[objects.NUMAPagesTopology( @@ -4492,6 +4517,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=2, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0, 5]), set([1, 6]), set([2, 7]), set([3, 8]), set([4, 9])], @@ -4531,6 +4557,7 @@ class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase): memory=4096, cpu_usage=2, memory_usage=0, + socket=0, pinned_cpus=set([0, 1, 2, 5, 6, 7]), siblings=[set([0, 5]), set([1, 6]), set([2, 7]), set([3, 8]), set([4, 9])], @@ -4766,6 +4793,7 @@ class EmulatorThreadsTestCase(test.NoDBTestCase): memory=2048, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([0]), set([1])], mempages=[objects.NUMAPagesTopology( @@ -4777,6 +4805,7 @@ class EmulatorThreadsTestCase(test.NoDBTestCase): memory=2048, cpu_usage=0, memory_usage=0, + socket=0, pinned_cpus=set(), siblings=[set([2]), set([3])], mempages=[objects.NUMAPagesTopology( diff --git a/nova/tests/unit/virt/test_virt_drivers.py b/nova/tests/unit/virt/test_virt_drivers.py index 58fa3d4c27..ed9f1e3822 100644 --- a/nova/tests/unit/virt/test_virt_drivers.py +++ b/nova/tests/unit/virt/test_virt_drivers.py @@ -832,6 +832,7 @@ class LibvirtConnTestCase(_VirtDriverTestCase, test.TestCase): # This is needed for the live migration tests which spawn off the # operation for monitoring. self.useFixture(nova_fixtures.SpawnIsSynchronousFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) # When destroying an instance, os-vif will try to execute some commands # which hang tests so let's just stub out the unplug call to os-vif # since we don't care about it. diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index c8f8bb2481..292536735a 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -2583,6 +2583,7 @@ def numa_usage_from_instance_numa(host_topology, instance_topology, cpuset=host_cell.cpuset, pcpuset=host_cell.pcpuset, memory=host_cell.memory, + socket=host_cell.socket, cpu_usage=0, memory_usage=0, mempages=host_cell.mempages, diff --git a/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index 9658a5791d..1ae86d9f47 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -1643,15 +1643,44 @@ class Host(object): CONFIG_CGROUP_SCHED may be disabled in some kernel configs to improve scheduler latency. """ + return self._has_cgroupsv1_cpu_controller() or \ + self._has_cgroupsv2_cpu_controller() + + def _has_cgroupsv1_cpu_controller(self): + LOG.debug(f"Searching host: '{self.get_hostname()}' " + "for CPU controller through CGroups V1...") try: with open("/proc/self/mounts", "r") as fd: for line in fd.readlines(): # mount options and split options bits = line.split()[3].split(",") if "cpu" in bits: + LOG.debug("CPU controller found on host.") + return True + LOG.debug("CPU controller missing on host.") + return False + except IOError as ex: + LOG.debug(f"Search failed due to: '{ex}'. " + "Maybe the host is not running under CGroups V1. " + "Deemed host to be missing controller by this approach.") + return False + + def _has_cgroupsv2_cpu_controller(self): + LOG.debug(f"Searching host: '{self.get_hostname()}' " + "for CPU controller through CGroups V2...") + try: + with open("/sys/fs/cgroup/cgroup.controllers", "r") as fd: + for line in fd.readlines(): + bits = line.split() + if "cpu" in bits: + LOG.debug("CPU controller found on host.") return True + LOG.debug("CPU controller missing on host.") return False - except IOError: + except IOError as ex: + LOG.debug(f"Search failed due to: '{ex}'. " + "Maybe the host is not running under CGroups V2. " + "Deemed host to be missing controller by this approach.") return False def get_canonical_machine_type(self, arch, machine) -> str: diff --git a/releasenotes/notes/hypervisor-version-weigher-d0bba77e720edafe.yaml b/releasenotes/notes/hypervisor-version-weigher-d0bba77e720edafe.yaml new file mode 100644 index 0000000000..31f2c70926 --- /dev/null +++ b/releasenotes/notes/hypervisor-version-weigher-d0bba77e720edafe.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + A new hypervisor version weigher has been added to prefer selecting hosts + with newer hypervisors installed. For the libvirt driver, this is the version + of libvirt on the compute node not the version of qemu. As with all + weighers this is enabled by default and its behavior can be modified using + the new ``hypervisor_version_weight_multiplier`` config option in the + ``filter_scheduler`` section. +upgrade: + - | + A new hypervisor version weigher has been added that will prefer selecting + hosts with a newer hypervisor installed. This can help simplify rolling + upgrades by preferring the already upgraded hosts when moving workloads around + using live or cold migration. To restore the old behavior either remove + the weigher from the list of enabled weighers or set + ``[filter_scheduler] hypervisor_version_weight_multiplier=0``. The default + value of the hypervisor_version_weight_multiplier is 1 so only a mild + preference is given to new hosts, higher values will make the effect + more pronounced and negative values will prefer older hosts. |