From 702ec6a84217ac864a0c747617fea9b4a51e2a4e Mon Sep 17 00:00:00 2001 From: Iulia Toader Date: Tue, 8 Nov 2016 06:37:47 -0800 Subject: [PATCH] Hyper-V PCI Passthrough Discrete Device Assignment is a new feature in Windows Server 2016, offering users the possibility of taking some of the PCI Express devices in their systems and pass them through directly to a guest VM. Co-Authored-By: Claudiu Belu Depends-On: I8e7782d3e1e9f8e92406604f05504a7754ffa3c2 Implements: blueprint hyper-v-pci-passthrough Change-Id: Ica395a982da1ffa4ead41601124d192a838ea1ad --- hyperv/nova/hostops.py | 26 ++++++++++++++++++++++ hyperv/nova/vmops.py | 20 ++++++++++++++++- hyperv/tests/unit/test_hostops.py | 30 +++++++++++++++++++++++++- hyperv/tests/unit/test_vmops.py | 36 ++++++++++++++++++++++++++++++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/hyperv/nova/hostops.py b/hyperv/nova/hostops.py index 5e748733..291b4356 100644 --- a/hyperv/nova/hostops.py +++ b/hyperv/nova/hostops.py @@ -193,6 +193,7 @@ class HostOps(object): obj_fields.HVType.HYPERV, obj_fields.VMMode.HVM)], 'numa_topology': self._get_host_numa_topology()._to_json(), + 'pci_passthrough_devices': self._get_pci_passthrough_devices(), } gpu_info = self._get_remotefx_gpu_info() @@ -200,6 +201,31 @@ class HostOps(object): return dic + def _get_pci_passthrough_devices(self): + """Get host PCI devices information. + + Obtains PCI devices information and returns it as a JSON string. + + :returns: a JSON string containing a list of the assignable PCI + devices information. + """ + + pci_devices = self._hostutils.get_pci_passthrough_devices() + + for pci_dev in pci_devices: + # NOTE(claudiub): These fields are required by the PCI tracker. + dev_label = 'label_%(vendor_id)s_%(product_id)s' % { + 'vendor_id': pci_dev['vendor_id'], + 'product_id': pci_dev['product_id']} + + # TODO(claudiub): Find a way to associate the PCI devices with + # the NUMA nodes they are in. + pci_dev.update(dev_type=obj_fields.PciDeviceType.STANDARD, + label=dev_label, + numa_node=None) + + return jsonutils.dumps(pci_devices) + def host_power_action(self, action): """Reboots, shuts down or powers up the host.""" if action in [constants.HOST_POWER_ACTION_SHUTDOWN, diff --git a/hyperv/nova/vmops.py b/hyperv/nova/vmops.py index 2476cac0..75cd117e 100644 --- a/hyperv/nova/vmops.py +++ b/hyperv/nova/vmops.py @@ -379,6 +379,13 @@ class VMOps(object): dynamic_memory_ratio = CONF.hyperv.dynamic_memory_ratio vnuma_enabled = False + if instance.pci_requests.requests: + # NOTE(claudiub): if the instance requires PCI devices, its + # host shutdown action MUST be shutdown. + host_shutdown_action = os_win_const.HOST_SHUTDOWN_ACTION_SHUTDOWN + else: + host_shutdown_action = None + self._vmutils.create_vm(instance_name, vnuma_enabled, vm_gen, @@ -391,7 +398,8 @@ class VMOps(object): instance.flavor.vcpus, cpus_per_numa_node, CONF.hyperv.limit_cpu_features, - dynamic_memory_ratio) + dynamic_memory_ratio, + host_shutdown_action=host_shutdown_action) self._configure_remotefx(instance, vm_gen) @@ -423,6 +431,16 @@ class VMOps(object): self._configure_secure_vm(context, instance, image_meta, secure_boot_enabled) + self._attach_pci_devices(instance) + + def _attach_pci_devices(self, instance): + for pci_request in instance.pci_requests.requests: + spec = pci_request.spec[0] + for counter in range(pci_request.count): + self._vmutils.add_pci_device(instance.name, + spec['vendor_id'], + spec['product_id']) + def _get_instance_vnuma_config(self, instance, image_meta): """Returns the appropriate NUMA configuration for Hyper-V instances, given the desired instance NUMA topology. diff --git a/hyperv/tests/unit/test_hostops.py b/hyperv/tests/unit/test_hostops.py index 04843058..49e97bbf 100644 --- a/hyperv/tests/unit/test_hostops.py +++ b/hyperv/tests/unit/test_hostops.py @@ -19,6 +19,7 @@ import mock from nova import context as nova_context from nova import exception from nova import objects +from nova.objects import fields as obj_fields from os_win import constants as os_win_const from oslo_config import cfg from oslo_serialization import jsonutils @@ -157,6 +158,7 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): mock_NUMATopology.assert_called_once_with( cells=[mock_NUMACell.return_value]) + @mock.patch.object(hostops.HostOps, '_get_pci_passthrough_devices') @mock.patch.object(hostops.HostOps, '_get_host_numa_topology') @mock.patch.object(hostops.HostOps, '_get_remotefx_gpu_info') @mock.patch.object(hostops.HostOps, '_get_cpu_info') @@ -168,7 +170,8 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): mock_get_storage_info_gb, mock_get_hypervisor_version, mock_get_memory_info, mock_get_cpu_info, - mock_get_gpu_info, mock_get_numa_topology): + mock_get_gpu_info, mock_get_numa_topology, + mock_get_pci_devices): mock_get_storage_info_gb.return_value = (mock.sentinel.LOCAL_GB, mock.sentinel.LOCAL_GB_FREE, mock.sentinel.LOCAL_GB_USED) @@ -180,6 +183,7 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): mock_get_hypervisor_version.return_value = mock.sentinel.VERSION mock_get_numa_topology.return_value._to_json.return_value = ( mock.sentinel.numa_topology_json) + mock_get_pci_devices.return_value = mock.sentinel.pcis mock_gpu_info = self._get_mock_gpu_info() mock_get_gpu_info.return_value = mock_gpu_info @@ -189,6 +193,7 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): mock_get_memory_info.assert_called_once_with() mock_get_cpu_info.assert_called_once_with() mock_get_hypervisor_version.assert_called_once_with() + mock_get_pci_devices.assert_called_once_with() expected = {'supported_instances': [("i686", "hyperv", "hvm"), ("x86_64", "hyperv", "hvm")], 'hypervisor_hostname': mock_node(), @@ -205,9 +210,32 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase): 'remotefx_available_video_ram': 2048, 'remotefx_gpu_info': mock.sentinel.FAKE_GPU_INFO, 'remotefx_total_video_ram': 4096, + 'pci_passthrough_devices': mock.sentinel.pcis, } self.assertEqual(expected, response) + @mock.patch.object(hostops.jsonutils, 'dumps') + def test_get_pci_passthrough_devices(self, mock_jsonutils_dumps): + mock_pci_dev = {'vendor_id': 'fake_vendor_id', + 'product_id': 'fake_product_id', + 'dev_id': 'fake_dev_id', + 'address': 'fake_address'} + mock_get_pcis = self._hostops._hostutils.get_pci_passthrough_devices + mock_get_pcis.return_value = [mock_pci_dev] + + expected_label = 'label_%(vendor_id)s_%(product_id)s' % { + 'vendor_id': mock_pci_dev['vendor_id'], + 'product_id': mock_pci_dev['product_id']} + expected_pci_dev = mock_pci_dev.copy() + expected_pci_dev.update(dev_type=obj_fields.PciDeviceType.STANDARD, + label=expected_label, + numa_node=None) + + result = self._hostops._get_pci_passthrough_devices() + + self.assertEqual(mock_jsonutils_dumps.return_value, result) + mock_jsonutils_dumps.assert_called_once_with([expected_pci_dev]) + def _test_host_power_action(self, action): self._hostops._hostutils.host_power_action = mock.Mock() diff --git a/hyperv/tests/unit/test_vmops.py b/hyperv/tests/unit/test_vmops.py index 2803139e..efe4042f 100644 --- a/hyperv/tests/unit/test_vmops.py +++ b/hyperv/tests/unit/test_vmops.py @@ -18,6 +18,7 @@ from eventlet import timeout as etimeout import mock from nova.compute import vm_states from nova import exception +from nova import objects from nova.objects import fields from nova.objects import flavor as flavor_obj from nova.tests.unit.objects import test_flavor @@ -565,6 +566,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self.assertEqual([], events) mock_is_neutron.assert_called_once_with() + @mock.patch.object(vmops.VMOps, '_attach_pci_devices') @mock.patch.object(vmops.VMOps, '_configure_secure_vm') @mock.patch.object(vmops.VMOps, '_requires_secure_boot') @mock.patch.object(vmops.VMOps, '_requires_certificate') @@ -587,9 +589,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_requires_certificate, mock_requires_secure_boot, mock_configure_secure_vm, + mock_attach_pci_devices, enable_instance_metrics, vm_gen=constants.VM_GEN_1, vnuma_enabled=False, + pci_requests=None, requires_sec_boot=True): self.flags(dynamic_memory_ratio=2.0, group='hyperv') self.flags(enable_instance_metrics_collection=enable_instance_metrics, @@ -605,6 +609,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): flavor = flavor_obj.Flavor(**test_flavor.fake_flavor) mock_instance.flavor = flavor + instance_pci_requests = objects.InstancePCIRequests( + requests=pci_requests or [], instance_uuid=mock_instance.uuid) + mock_instance.pci_requests = instance_pci_requests + host_shutdown_action = (os_win_const.HOST_SHUTDOWN_ACTION_SHUTDOWN + if pci_requests else None) if vnuma_enabled: mock_get_vnuma_config.return_value = ( @@ -632,7 +641,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self._vmops._vmutils.update_vm.assert_called_once_with( mock_instance.name, mock_instance.flavor.memory_mb, mem_per_numa, mock_instance.flavor.vcpus, cpus_per_numa, - CONF.hyperv.limit_cpu_features, dynamic_memory_ratio) + CONF.hyperv.limit_cpu_features, dynamic_memory_ratio, + host_shutdown_action=host_shutdown_action) mock_configure_remotefx.assert_called_once_with(mock_instance, vm_gen) mock_create_scsi_ctrl = self._vmops._vmutils.create_scsi_controller @@ -665,6 +675,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): msft_ca_required=mock_requires_certificate.return_value) mock_configure_secure_vm.assert_called_once_with(self.context, mock_instance, mock.sentinel.image_meta, requires_sec_boot) + mock_attach_pci_devices.assert_called_once_with(mock_instance) def test_create_instance(self): self._test_create_instance(enable_instance_metrics=True) @@ -680,6 +691,29 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self._test_create_instance(enable_instance_metrics=False, vnuma_enabled=True) + def test_create_instance_pci_requested(self): + vendor_id = 'fake_vendor_id' + product_id = 'fake_product_id' + spec = {'vendor_id': vendor_id, 'product_id': product_id} + request = objects.InstancePCIRequest(count=1, spec=[spec]) + self._test_create_instance(enable_instance_metrics=False, + pci_requests=[request]) + + def test_attach_pci_devices(self): + mock_instance = fake_instance.fake_instance_obj(self.context) + vendor_id = 'fake_vendor_id' + product_id = 'fake_product_id' + spec = {'vendor_id': vendor_id, 'product_id': product_id} + request = objects.InstancePCIRequest(count=2, spec=[spec]) + instance_pci_requests = objects.InstancePCIRequests( + requests=[request], instance_uuid=mock_instance.uuid) + mock_instance.pci_requests = instance_pci_requests + + self._vmops._attach_pci_devices(mock_instance) + + self._vmops._vmutils.add_pci_device.assert_has_calls( + [mock.call(mock_instance.name, vendor_id, product_id)] * 2) + @mock.patch.object(vmops.hardware, 'numa_get_constraints') @mock.patch.object(vmops.objects.ImageMeta, 'from_dict') def _check_get_instance_vnuma_config_exception(self, mock_from_dict,