diff --git a/doc/source/admin/configuration/schedulers.rst b/doc/source/admin/configuration/schedulers.rst index eea264fd98f7..40ab84679195 100644 --- a/doc/source/admin/configuration/schedulers.rst +++ b/doc/source/admin/configuration/schedulers.rst @@ -274,6 +274,15 @@ extra specs key as the key to be matched if no namespace is present; this action is highly discouraged because it conflicts with :ref:`AggregateInstanceExtraSpecsFilter` filter when you enable both filters. +Some virt drivers support reporting CPU traits to the Placement service. With that +feature available, you should consider using traits in flavors instead of +ComputeCapabilitiesFilter, because traits provide consistent naming for CPU +features in some virt drivers and querying traits is efficient. For more detail, please see +`Support Matrix `_, +:ref:`Required traits `, +:ref:`Forbidden traits ` and +`Report CPU features to the Placement service `_. + .. _ComputeFilter: ComputeFilter diff --git a/doc/source/user/flavors.rst b/doc/source/user/flavors.rst index 1e91e2a1a2f5..77c4ed190942 100644 --- a/doc/source/user/flavors.rst +++ b/doc/source/user/flavors.rst @@ -650,6 +650,8 @@ Secure Boot - ``disabled`` or ``optional``: (default) Disable Secure Boot for instances running with this flavor. +.. _extra-specs-required-traits: + Required traits Added in the 17.0.0 Queens release. @@ -677,6 +679,8 @@ Required traits Traits can be managed using the `osc-placement plugin`_. +.. _extra-specs-forbidden-traits: + Forbidden traits Added in the 18.0.0 Rocky release. diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index bfa4ba7a7fe9..e8bc8783959f 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -1527,3 +1527,25 @@ driver.ironic=missing driver.libvirt-vz-vm=missing driver.libvirt-vz-ct=missing driver.powervm=missing + +[operation.report-cpu-traits] +title=Report CPU traits +status=optional +notes=The report CPU traits feature in OpenStack allows a Nova node to report + its CPU traits according to CPU mode configuration. This gives users the ability + to boot instances based on desired CPU traits. +cli= +driver.xenserver=missing +driver.libvirt-kvm-x86=complete +driver.libvirt-kvm-aarch64=unknown +driver.libvirt-kvm-ppc64=complete +driver.libvirt-kvm-s390x=missing +driver.libvirt-qemu-x86=complete +driver.libvirt-lxc=missing +driver.libvirt-xen=missing +driver.vmware=missing +driver.hyperv=missing +driver.ironic=missing +driver.libvirt-vz-vm=missing +driver.libvirt-vz-ct=missing +driver.powervm=missing diff --git a/nova/compute/provider_tree.py b/nova/compute/provider_tree.py index 6a234367c67d..c9932a36469d 100644 --- a/nova/compute/provider_tree.py +++ b/nova/compute/provider_tree.py @@ -558,6 +558,8 @@ class ProviderTree(object): to be affected. :param traits: String names of traits to be added. """ + if not traits: + return with self.lock: provider = self._find_with_lock(name_or_uuid) final_traits = provider.traits | set(traits) @@ -570,6 +572,8 @@ class ProviderTree(object): to be affected. :param traits: String names of traits to be removed. """ + if not traits: + return with self.lock: provider = self._find_with_lock(name_or_uuid) final_traits = provider.traits - set(traits) diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 86e83c08fe4c..b5f251d116f8 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -421,6 +421,9 @@ class ProviderUsageBaseTestCase(test.TestCase, InstanceHelperMixin): return self.placement_api.get( '/resource_providers', version='1.14').body['resource_providers'] + def _create_trait(self, trait): + return self.placement_api.put('/traits/%s' % trait, {}, version='1.6') + def _get_provider_traits(self, provider_uuid): return self.placement_api.get( '/resource_providers/%s/traits' % provider_uuid, diff --git a/nova/tests/functional/libvirt/test_report_cpu_traits.py b/nova/tests/functional/libvirt/test_report_cpu_traits.py new file mode 100644 index 000000000000..42958cb3ee55 --- /dev/null +++ b/nova/tests/functional/libvirt/test_report_cpu_traits.py @@ -0,0 +1,62 @@ +# Copyright (c) 2018 Intel, 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 fixtures + +from nova import conf +from nova.tests.functional import integrated_helpers +from nova.tests.unit.virt.libvirt import fakelibvirt + +CONF = conf.CONF + + +class LibvirtReportTraitsTests(integrated_helpers.ProviderUsageBaseTestCase): + compute_driver = 'libvirt.LibvirtDriver' + + def setUp(self): + super(LibvirtReportTraitsTests, self).setUp() + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.libvirt.driver.libvirt', + fakelibvirt)) + self.useFixture(fixtures.MonkeyPatch('nova.virt.libvirt.host.libvirt', + fakelibvirt)) + self.useFixture( + fixtures.MockPatch( + 'nova.virt.libvirt.driver.LibvirtDriver.init_host')) + + self.useFixture( + fixtures.MockPatch('nova.virt.libvirt.utils.get_fs_info')) + + self.assertEqual([], self._get_all_providers()) + self.compute = self._start_compute(CONF.host) + nodename = self.compute.manager._get_nodename(None) + self.host_uuid = self._get_provider_uuid_by_host(nodename) + + def test_report_cpu_traits(self): + # Test CPU traits reported on initial node startup, these specific + # trait values are coming from fakelibvirt's baselineCPU result. + self.assertItemsEqual(['HW_CPU_X86_VMX', 'HW_CPU_X86_AESNI'], + self._get_provider_traits(self.host_uuid)) + + self._create_trait('CUSTOM_TRAITS') + new_traits = ['CUSTOM_TRAITS', 'HW_CPU_X86_AVX'] + self._set_provider_traits(self.host_uuid, new_traits) + self._run_periodics() + # HW_CPU_X86_AVX is filtered out because nova-compute owns CPU traits + # and it's not in the baseline for the host. + self.assertItemsEqual( + ['HW_CPU_X86_VMX', 'HW_CPU_X86_AESNI', 'CUSTOM_TRAITS'], + self._get_provider_traits(self.host_uuid) + ) diff --git a/nova/tests/unit/compute/test_provider_tree.py b/nova/tests/unit/compute/test_provider_tree.py index 8e75ac4411c8..a0aae53b2c9e 100644 --- a/nova/tests/unit/compute/test_provider_tree.py +++ b/nova/tests/unit/compute/test_provider_tree.py @@ -562,6 +562,12 @@ class TestProviderTree(test.NoDBTestCase): cn = self.compute_node1 pt = self._pt_with_cns() self.assertEqual(set([]), pt.data(cn.uuid).traits) + # Test adding with no trait provided for a bogus provider + pt.add_traits('bogus-uuid') + self.assertEqual( + set([]), + pt.data(cn.uuid).traits + ) # Add a couple of traits pt.add_traits(cn.uuid, "HW_GPU_API_DIRECT3D_V7_0", "HW_NIC_OFFLOAD_SG") self.assertEqual( @@ -570,6 +576,12 @@ class TestProviderTree(test.NoDBTestCase): # set() behavior: add a trait that's already there, and one that's not. # The unrelated one is unaffected. pt.add_traits(cn.uuid, "HW_GPU_API_DIRECT3D_V7_0", "HW_CPU_X86_AVX") + self.assertEqual( + set(["HW_GPU_API_DIRECT3D_V7_0", "HW_NIC_OFFLOAD_SG", + "HW_CPU_X86_AVX"]), + pt.data(cn.uuid).traits) + # Test removing with no trait provided for a bogus provider + pt.remove_traits('bogus-uuid') self.assertEqual( set(["HW_GPU_API_DIRECT3D_V7_0", "HW_NIC_OFFLOAD_SG", "HW_CPU_X86_AVX"]), diff --git a/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py b/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py index 55c44164da86..47535e03bf0f 100644 --- a/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py +++ b/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py @@ -25,6 +25,7 @@ disk_backing_files = {} disk_type = "qcow2" RESIZE_SNAPSHOT_NAME = libvirt_utils.RESIZE_SNAPSHOT_NAME +CPU_TRAITS_MAPPING = libvirt_utils.CPU_TRAITS_MAPPING def create_image(disk_format, path, size): @@ -172,3 +173,7 @@ def get_arch(image_meta): def version_to_string(version): return libvirt_utils.version_to_string(version) + + +def cpu_features_to_traits(features): + return libvirt_utils.cpu_features_to_traits(features) diff --git a/nova/tests/unit/virt/libvirt/fakelibvirt.py b/nova/tests/unit/virt/libvirt/fakelibvirt.py index 938f9a4c674f..a3940e4a5ab3 100644 --- a/nova/tests/unit/virt/libvirt/fakelibvirt.py +++ b/nova/tests/unit/virt/libvirt/fakelibvirt.py @@ -115,6 +115,7 @@ VIR_FROM_RPC = 345 VIR_FROM_NODEDEV = 666 VIR_ERR_INVALID_ARG = 8 VIR_ERR_NO_SUPPORT = 3 +VIR_ERR_XML_ERROR = 27 VIR_ERR_XML_DETAIL = 350 VIR_ERR_NO_DOMAIN = 420 VIR_ERR_OPERATION_FAILED = 510 diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index e482e02914e0..b0c788344e45 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -351,6 +351,98 @@ _fake_cpu_info = { eph_default_ext = utils.get_hash_str(nova.privsep.fs._DEFAULT_FILE_SYSTEM)[:7] +_fake_qemu64_cpu_feature = """ + + qemu64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +_fake_broadwell_cpu_feature = """ + + Broadwell-noTSX + Intel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + def eph_name(size): return ('ephemeral_%(size)s_%(ext)s' % @@ -17770,6 +17862,7 @@ class TestUpdateProviderTree(test.NoDBTestCase): vcpus = 24 memory_mb = 1024 disk_gb = 200 + cpu_traits = {t: False for t in libvirt_utils.CPU_TRAITS_MAPPING.values()} def setUp(self): super(TestUpdateProviderTree, self).setUp() @@ -17801,6 +17894,8 @@ class TestUpdateProviderTree(test.NoDBTestCase): return pt self.pt = _pt_with_cn_rp_and_shared_rp() + self.cpu_traits['HW_CPU_X86_AVX512F'] = True + self.cpu_traits['HW_CPU_X86_BMI'] = True def _get_inventory(self): return { @@ -17824,6 +17919,8 @@ class TestUpdateProviderTree(test.NoDBTestCase): }, } + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_cpu_traits', + new=mock.Mock(return_value=cpu_traits)) @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_vgpu_total') @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_local_gb_info', return_value={'total': disk_gb}) @@ -17841,6 +17938,8 @@ class TestUpdateProviderTree(test.NoDBTestCase): self._test_update_provider_tree() self.assertEqual(self._get_inventory(), (self.pt.data(self.cn_rp.uuid)).inventory) + self.assertEqual(set(['HW_CPU_X86_AVX512F', 'HW_CPU_X86_BMI']), + self.pt.data(self.cn_rp.uuid).traits) def test_update_provider_tree_with_vgpus(self): self._test_update_provider_tree(total_vgpus=8) @@ -17902,8 +18001,23 @@ class TestUpdateProviderTree(test.NoDBTestCase): self.assertEqual(self._get_inventory(), (self.pt.data(self.cn_rp.uuid)).inventory) + def test_update_provider_tree_with_cpu_traits(self): + # These two traits should be unset when update_provider_tree is called + self.pt.add_traits(self.cn_rp.uuid, 'HW_CPU_X86_VMX', 'HW_CPU_X86_XOP') + self._test_update_provider_tree() + self.assertEqual(set(['HW_CPU_X86_AVX512F', 'HW_CPU_X86_BMI']), + self.pt.data(self.cn_rp.uuid).traits) -class LibvirtDriverTestCase(test.NoDBTestCase): + +class TraitsComparisonMixin(object): + + def assertTraitsEqual(self, expected, actual): + exp = {t: t in expected + for t in libvirt_utils.CPU_TRAITS_MAPPING.values()} + self.assertEqual(exp, actual) + + +class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin): """Test for nova.virt.libvirt.libvirt_driver.LibvirtDriver.""" def setUp(self): super(LibvirtDriverTestCase, self).setUp() @@ -20469,6 +20583,254 @@ class LibvirtDriverTestCase(test.NoDBTestCase): self.assertRaises(test.TestingException, self._test_detach_mediated_devices, exc) + def test_cpu_traits_with_passthrough_mode(self): + """Test getting CPU traits when cpu_mmode is 'host-passthrough', traits + are calculated from fakelibvirt's baseline CPU features. + """ + self.flags(cpu_mode='host-passthrough', group='libvirt') + self.assertTraitsEqual(['HW_CPU_X86_AESNI', 'HW_CPU_X86_VMX'], + self.drvr._get_cpu_traits()) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + def test_cpu_traits_with_mode_none(self, mock_baseline): + """Test getting CPU traits when cpu_mode is 'none', traits are + calculated from _fake_qemu64_cpu_features. + """ + self.flags(cpu_mode='none', group='libvirt') + mock_baseline.return_value = _fake_qemu64_cpu_feature + self.assertTraitsEqual(['HW_CPU_X86_SSE', 'HW_CPU_X86_SVM', + 'HW_CPU_X86_MMX', 'HW_CPU_X86_SSE2'], + self.drvr._get_cpu_traits()) + + mock_baseline.assert_called_with([u''' + x86_64 + qemu64 + Intel + + +'''], 1) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + def test_cpu_traits_with_mode_custom(self, mock_baseline): + """Test getting CPU traits when cpu_mode is 'custom' and cpu_model is + 'Broadwell-noTSX', traits are calculated from + _fake_broadwell_cpu_features. + """ + self.flags(cpu_mode='custom', + cpu_model='Broadwell-noTSX', + group='libvirt') + mock_baseline.return_value = _fake_broadwell_cpu_feature + + self.assertTraitsEqual( + [ + 'HW_CPU_X86_BMI2', + 'HW_CPU_X86_AVX2', + 'HW_CPU_X86_BMI', + 'HW_CPU_X86_AVX', + 'HW_CPU_X86_AESNI', + 'HW_CPU_X86_SSE42', + 'HW_CPU_X86_SSE41', + 'HW_CPU_X86_FMA3', + 'HW_CPU_X86_SSSE3', + 'HW_CPU_X86_CLMUL', + 'HW_CPU_X86_SSE2', + 'HW_CPU_X86_SSE', + 'HW_CPU_X86_MMX' + ], self.drvr._get_cpu_traits() + ) + mock_baseline.assert_called_with([u''' + x86_64 + Broadwell-noTSX + Intel + + +'''], 1) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + def test_cpu_traits_with_no_baseline_support(self, mock_baseline): + """Test getting CPU traits when baseline call is not supported.""" + self.flags(cpu_mode='none', group='libvirt') + not_supported_exc = fakelibvirt.make_libvirtError( + fakelibvirt.libvirtError, + 'this function is not supported by the connection driver', + error_code=fakelibvirt.VIR_ERR_NO_SUPPORT) + mock_baseline.side_effect = not_supported_exc + self.assertTraitsEqual([], self.drvr._get_cpu_traits()) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.getCapabilities') + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + def test_cpu_traits_on_s390x(self, mock_baseline, mock_cap): + """Test getting CPU traits on s390x, baseline call is not supported on + the platform. + """ + self.flags(cpu_mode='none', group='libvirt') + + mock_cap.return_value = """ + + + cef19ce0-0ca2-11df-855d-b19fbce37686 + + s390x + + + + + + + """ + + not_supported_exc = fakelibvirt.make_libvirtError( + fakelibvirt.libvirtError, + 'this function is not supported by the connection driver: cannot' + ' compute baseline CPU', + error_code=fakelibvirt.VIR_ERR_NO_SUPPORT) + missing_model_exc = fakelibvirt.make_libvirtError( + fakelibvirt.libvirtError, + 'XML error: Missing CPU model name', + error_code=fakelibvirt.VIR_ERR_XML_ERROR) + + # model the libvirt behavior on s390x + def mocked_baseline(cpu_xml, *args): + xml = cpu_xml[0] + if "" in xml: + raise not_supported_exc + else: + raise missing_model_exc + mock_baseline.side_effect = mocked_baseline + + self.assertTraitsEqual([], self.drvr._get_cpu_traits()) + + def test_cpu_traits_with_invalid_virt_type(self): + """Test getting CPU traits when using a virt_type that doesn't support + the feature, only kvm and qemu supports reporting CPU traits. + """ + self.flags(cpu_mode='custom', + cpu_model='IvyBridge', + virt_type='lxc', + group='libvirt' + ) + self.assertRaises(exception.Invalid, self.drvr._get_cpu_traits) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.getCapabilities') + @mock.patch('nova.virt.libvirt.utils.cpu_features_to_traits') + def test_cpu_traits_with_mode_passthrough_and_extra_flags( + self, mock_to_traits, mock_cap): + """Test if extra flags are accounted when cpu_mode is set to + host-passthrough. + """ + self.flags(cpu_mode='host-passthrough', + cpu_model_extra_flags='PCID', + group='libvirt') + mock_cap.return_value = """ + + + cef19ce0-0ca2-11df-855d-b19fbce37686 + + IvyBridge + + + + + + + + """ + self.drvr._get_cpu_traits() + self.assertItemsEqual(['pcid', 'erms'], mock_to_traits.call_args[0][0]) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + @mock.patch('nova.virt.libvirt.utils.cpu_features_to_traits') + def test_cpu_traits_with_mode_custom_and_extra_flags(self, mock_to_traits, + mock_baseline): + """Test if extra flags are accounted when cpu_mode is set to custom. + """ + self.flags(cpu_mode='custom', + cpu_model='IvyBridge', + cpu_model_extra_flags='PCID', + group='libvirt') + + mock_baseline.return_value = """ + + IvyBridge + Intel + + + + """ + self.drvr._get_cpu_traits() + mock_baseline.assert_called_with([u''' + x86_64 + IvyBridge + Intel + + + +'''], 1) + self.assertItemsEqual(['pcid', 'erms'], mock_to_traits.call_args[0][0]) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + @mock.patch('nova.virt.libvirt.utils.cpu_features_to_traits') + def test_cpu_traits_with_mode_not_set_and_extra_flags(self, mock_to_traits, + mock_baseline): + """Test if extra flags are accounted when cpu_mode is not set.""" + self.flags(cpu_mode=None, + cpu_model_extra_flags='PCID', + virt_type='kvm', + group='libvirt' + ) + mock_baseline.return_value = """ + + IvyBridge + Intel + + + """ + self.drvr._get_cpu_traits() + self.assertItemsEqual(['pcid', 'erms'], mock_to_traits.call_args[0][0]) + + def test_cpu_traits_with_mode_none_and_invalid_virt_type(self): + """Test case that cpu mode is none and virt_type is neither kvm nor + qemu. + """ + self.flags(cpu_mode='none', + virt_type='lxc', + group='libvirt') + self.assertIsNone(self.drvr._get_cpu_traits()) + + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.getCapabilities') + @mock.patch('nova.virt.libvirt.host.libvirt.Connection.baselineCPU') + def test_cpu_traits_with_mode_none_on_power(self, mock_baseline, mock_cap): + """Test case that cpu mode is none on Power machines.""" + self.flags(cpu_mode='none', virt_type='kvm', group='libvirt') + mock_cap.return_value = ''' + + + 1f71d34a-7c89-45cf-95ce-3df20fc6b936 + + POWER8 + IBM + ppc64le + + + + + + ''' + mock_baseline.return_value = ''' + + POWER8 + IBM + + ''' + self.drvr._get_cpu_traits() + mock_baseline.assert_called_with([u''' + ppc64le + POWER8 + IBM + + +'''], 1) + class LibvirtVolumeUsageTestCase(test.NoDBTestCase): """Test for LibvirtDriver.get_all_volume_usage.""" diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 539bce70b488..b0e14f002f46 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -43,6 +43,7 @@ import time import uuid from castellan import key_manager +from copy import deepcopy import eventlet from eventlet import greenthread from eventlet import tpool @@ -6379,8 +6380,8 @@ class LibvirtDriver(driver.ComputeDriver): self.firewall_driver.refresh_instance_security_rules(instance) def update_provider_tree(self, provider_tree, nodename): - """Update a ProviderTree object with current resource provider and - inventory information. + """Update a ProviderTree object with current resource provider, + inventory information and CPU traits. :param nova.compute.provider_tree.ProviderTree provider_tree: A nova.compute.provider_tree.ProviderTree object representing all @@ -6453,6 +6454,16 @@ class LibvirtDriver(driver.ComputeDriver): provider_tree.update_inventory(nodename, result) + traits = self._get_cpu_traits() + if traits is not None: + # _get_cpu_traits returns a dict of trait names mapped to boolean + # values. Add traits equal to True to provider tree, remove + # those False traits from provider tree. + traits_to_add = [t for t in traits if traits[t]] + traits_to_remove = set(traits) - set(traits_to_add) + provider_tree.add_traits(nodename, *traits_to_add) + provider_tree.remove_traits(nodename, *traits_to_remove) + def get_available_resource(self, nodename): """Retrieve resource information. @@ -8911,3 +8922,70 @@ class LibvirtDriver(driver.ComputeDriver): nova.privsep.fs.FS_FORMAT_EXT3, nova.privsep.fs.FS_FORMAT_EXT4, nova.privsep.fs.FS_FORMAT_XFS] + + def _get_cpu_traits(self): + """Get CPU traits of VMs based on guest CPU model config: + 1. if mode is 'host-model' or 'host-passthrough', use host's + CPU features. + 2. if mode is None, choose a default CPU model based on CPU + architecture. + 3. if mode is 'custom', use cpu_model to generate CPU features. + The code also accounts for cpu_model_extra_flags configuration when + cpu_mode is 'host-model', 'host-passthrough' or 'custom', this + ensures user specified CPU feature flags to be included. + :return: A dict of trait names mapped to boolean values or None. + """ + cpu = self._get_guest_cpu_model_config() + if not cpu: + LOG.info('The current libvirt hypervisor %(virt_type)s ' + 'does not support reporting CPU traits.', + {'virt_type': CONF.libvirt.virt_type}) + return + + caps = deepcopy(self._host.get_capabilities()) + if cpu.mode in ('host-model', 'host-passthrough'): + # Account for features in cpu_model_extra_flags conf + host_features = [f.name for f in + caps.host.cpu.features | cpu.features] + return libvirt_utils.cpu_features_to_traits(host_features) + + # Choose a default CPU model when cpu_mode is not specified + if cpu.mode is None: + caps.host.cpu.model = libvirt_utils.get_cpu_model_from_arch( + caps.host.cpu.arch) + caps.host.cpu.features = set() + else: + # For custom mode, set model to guest CPU model + caps.host.cpu.model = cpu.model + caps.host.cpu.features = set() + # Account for features in cpu_model_extra_flags conf + for f in cpu.features: + caps.host.cpu.add_feature( + vconfig.LibvirtConfigCPUFeature(name=f.name)) + + xml_str = caps.host.cpu.to_xml() + LOG.info("Libvirt baseline CPU %s", xml_str) + # TODO(lei-zh): baselineCPU is not supported on all platforms. + # There is some work going on in the libvirt community to replace the + # baseline call. Consider using the new apis when they are ready. See + # https://www.redhat.com/archives/libvir-list/2018-May/msg01204.html. + try: + if hasattr(libvirt, 'VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES'): + features = self._host.get_connection().baselineCPU( + [xml_str], + libvirt.VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES) + else: + features = self._host.get_connection().baselineCPU([xml_str]) + except libvirt.libvirtError as ex: + with excutils.save_and_reraise_exception() as ctxt: + error_code = ex.get_error_code() + if error_code == libvirt.VIR_ERR_NO_SUPPORT: + ctxt.reraise = False + LOG.info('URI %(uri)s does not support full set' + ' of host capabilities: %(error)s', + {'uri': self._host._uri, 'error': ex}) + return libvirt_utils.cpu_features_to_traits([]) + + cpu.parse_str(features) + return libvirt_utils.cpu_features_to_traits( + [f.name for f in cpu.features]) diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index d523f7f72ad2..dce771ecddbf 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -42,6 +42,43 @@ LOG = logging.getLogger(__name__) RESIZE_SNAPSHOT_NAME = 'nova-resize' +# Mapping used to convert libvirt cpu features to traits, for more details, see +# https://github.com/libvirt/libvirt/blob/master/src/cpu/cpu_map.xml. +CPU_TRAITS_MAPPING = { + '3dnow': 'HW_CPU_X86_3DNOW', + 'abm': 'HW_CPU_X86_ABM', + 'aes': 'HW_CPU_X86_AESNI', + 'avx': 'HW_CPU_X86_AVX', + 'avx2': 'HW_CPU_X86_AVX2', + 'avx512bw': 'HW_CPU_X86_AVX512BW', + 'avx512cd': 'HW_CPU_X86_AVX512CD', + 'avx512dq': 'HW_CPU_X86_AVX512DQ', + 'avx512er': 'HW_CPU_X86_AVX512ER', + 'avx512f': 'HW_CPU_X86_AVX512F', + 'avx512pf': 'HW_CPU_X86_AVX512PF', + 'avx512vl': 'HW_CPU_X86_AVX512VL', + 'bmi1': 'HW_CPU_X86_BMI', + 'bmi2': 'HW_CPU_X86_BMI2', + 'pclmuldq': 'HW_CPU_X86_CLMUL', + 'f16c': 'HW_CPU_X86_F16C', + 'fma': 'HW_CPU_X86_FMA3', + 'fma4': 'HW_CPU_X86_FMA4', + 'mmx': 'HW_CPU_X86_MMX', + 'mpx': 'HW_CPU_X86_MPX', + 'sha-ni': 'HW_CPU_X86_SHA', + 'sse': 'HW_CPU_X86_SSE', + 'sse2': 'HW_CPU_X86_SSE2', + 'sse3': 'HW_CPU_X86_SSE3', + 'sse4.1': 'HW_CPU_X86_SSE41', + 'sse4.2': 'HW_CPU_X86_SSE42', + 'sse4a': 'HW_CPU_X86_SSE4A', + 'ssse3': 'HW_CPU_X86_SSSE3', + 'svm': 'HW_CPU_X86_SVM', + 'tbm': 'HW_CPU_X86_TBM', + 'vmx': 'HW_CPU_X86_VMX', + 'xop': 'HW_CPU_X86_XOP' +} + def create_image(disk_format, path, size): """Create a disk image @@ -483,3 +520,25 @@ def is_valid_hostname(hostname): def version_to_string(version): """Returns string version based on tuple""" return '.'.join([str(x) for x in version]) + + +def cpu_features_to_traits(features): + """Returns this driver's CPU traits dict where keys are trait names from + CPU_TRAITS_MAPPING, values are boolean indicates whether the trait should + be set in the provider tree. + """ + traits = {trait_name: False for trait_name in CPU_TRAITS_MAPPING.values()} + for f in features: + if f in CPU_TRAITS_MAPPING: + traits[CPU_TRAITS_MAPPING[f]] = True + + return traits + + +def get_cpu_model_from_arch(arch): + mode = 'qemu64' + if arch == obj_fields.Architecture.I686: + mode = 'qemu32' + elif arch == obj_fields.Architecture.PPC64LE: + mode = 'POWER8' + return mode diff --git a/releasenotes/notes/bp-report-cpu-features-aff90db66837de7d.yaml b/releasenotes/notes/bp-report-cpu-features-aff90db66837de7d.yaml new file mode 100644 index 000000000000..317cec144d00 --- /dev/null +++ b/releasenotes/notes/bp-report-cpu-features-aff90db66837de7d.yaml @@ -0,0 +1,7 @@ +--- + +features: + - Add support for reporting CPU traits to Placement + in libvirt driver. For more detail, see + https://specs.openstack.org/openstack/nova-specs/specs/rocky/approved/report-cpu-features-as-traits.html + and https://docs.openstack.org/nova/latest/user/support-matrix.html.