From ee67362728e09ed92a9b0a5e1e75c8303fbe0373 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Tue, 1 Apr 2025 10:05:09 -0700 Subject: [PATCH] Functional tests for one-time-use devices Related to blueprint one-time-use-devices Change-Id: I6f764666a44c74c5ac97dced568fc685dee013b6 --- .../libvirt/test_pci_in_placement.py | 171 +++++++++++++++++- .../libvirt/test_pci_sriov_servers.py | 22 ++- 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/nova/tests/functional/libvirt/test_pci_in_placement.py b/nova/tests/functional/libvirt/test_pci_in_placement.py index c5935da778..835bb1f8f8 100644 --- a/nova/tests/functional/libvirt/test_pci_in_placement.py +++ b/nova/tests/functional/libvirt/test_pci_in_placement.py @@ -99,7 +99,7 @@ class PlacementPCIInventoryReportingTests(PlacementPCIReportingTests): ) }, # PF_PROD_ID + slot 2 will match one PF but not their children - # VFs + # VFs and will be considered OTU { "vendor_id": fakelibvirt.PCI_VEND_ID, "product_id": fakelibvirt.PF_PROD_ID, @@ -107,6 +107,7 @@ class PlacementPCIInventoryReportingTests(PlacementPCIReportingTests): "traits": ",".join( [os_traits.HW_NIC_SRIOV, "CUSTOM_PF", "pf-white"] ), + "one_time_use": "true", }, # VF_PROD_ID + slot 3 will match two VFs but not their parent # PF @@ -147,6 +148,7 @@ class PlacementPCIInventoryReportingTests(PlacementPCIReportingTests): ], "0000:81:02.0": [ "HW_NIC_SRIOV", + "HW_PCI_ONE_TIME_USE", "CUSTOM_PF", "CUSTOM_PF_WHITE", ], @@ -1995,3 +1997,170 @@ class RCAndTraitBasedPCIAliasTests(PlacementPCIReportingTests): self.assert_placement_pci_view( "compute1", **compute1_expected_placement_view) self.assert_no_pci_healing("compute1") + + def test_one_time_use_workflow(self): + # The fake libvirt will emulate on the host: + # * two type-PCIs + pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pci=1, num_pfs=0, num_vfs=0) + device_spec = self._to_list_of_json_str( + [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + "one_time_use": "true", + }, + ] + ) + self.flags(group='pci', device_spec=device_spec) + pci_alias = { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + "name": "a-otu", + } + self.flags( + group="pci", + alias=self._to_list_of_json_str([pci_alias]), + ) + extra_spec = {"pci_passthrough:alias": "a-otu:1"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + self.start_compute(hostname="compute1", pci_info=pci_info) + + rc = "CUSTOM_PCI_%s_%s" % (fakelibvirt.PCI_VEND_ID, + fakelibvirt.PCI_PROD_ID) + addr = "0000:81:00.0" + expected_placement_view = { + "inventories": { + addr: { + rc: { + "total": 1, + "reserved": 0, + }, + }, + }, + "traits": { + addr: [ + "HW_PCI_ONE_TIME_USE", + ], + }, + "usages": { + addr: {rc: 0}, + }, + "allocations": {}, + } + + # Initially we expect no usage, no reservation + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + self.assertPCIDeviceCounts('compute1', total=1, free=1) + + # Create a server that uses our device + server = self._create_server(flavor_id=flavor_id, networks=[]) + + # Now we expect the usage to have gone up, and for the reserved + # count on our OTU device to have increased + expected_placement_view["usages"][addr][rc] = 1 + expected_placement_view["inventories"][addr][rc]["reserved"] = 1 + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + + # Delete the server to release the allocation (but not reservation) + self._delete_server(server) + + # We expect the usage to have gone back to zero, but the reserved + # count is unchanged + expected_placement_view["usages"][addr][rc] = 0 + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + + # Create another server that wants our device, but there are no + # cleaned devices available. It should fail to schedule because + # no host can satisfy the request. + server = self._create_server(flavor_id=flavor_id, networks=[], + expected_state="ERROR") + self.assertIn('No valid host', server['fault']['message']) + self._delete_server(server) + + # Externally "clean" the device and mark it as unreserved + self._reserve_placement_resource('compute1_%s' % addr, rc, 0) + expected_placement_view["inventories"][addr][rc]['reserved'] = 0 + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + + # Create a server that uses our device, confirming that after an + # external clean it is allocatable again. + server = self._create_server(flavor_id=flavor_id, networks=[]) + + def test_otu_configured_late(self): + # The fake libvirt will emulate on the host: + # * two type-PCIs + pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pci=1, num_pfs=0, num_vfs=0) + device_spec = [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + }, + ] + self.flags( + group='pci', device_spec=self._to_list_of_json_str(device_spec)) + pci_alias = { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + "name": "a-otu", + } + self.flags( + group="pci", + alias=self._to_list_of_json_str([pci_alias]), + ) + extra_spec = {"pci_passthrough:alias": "a-otu:1"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + self.start_compute(hostname="compute1", pci_info=pci_info) + + rc = "CUSTOM_PCI_%s_%s" % (fakelibvirt.PCI_VEND_ID, + fakelibvirt.PCI_PROD_ID) + addr = "0000:81:00.0" + expected_placement_view = { + "inventories": { + addr: { + rc: { + "total": 1, + "reserved": 0, + }, + }, + }, + "traits": { + addr: [], + }, + "usages": { + addr: {rc: 0}, + }, + "allocations": {}, + } + + # Initially we expect no usage, no reservation + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + self.assertPCIDeviceCounts("compute1", total=1, free=1) + + # Create a server that uses our device + self._create_server(flavor_id=flavor_id, networks=[]) + + # Expect to see the usage go up, but reserved stays at zero + expected_placement_view["usages"][addr][rc] = 1 + self.assert_placement_pci_view( + "compute1", **expected_placement_view) + + # Now configure the compute node to treat our device as OTU and restart + # the compute service (which will run the placement update) + device_spec[0]['one_time_use'] = 'true' + self.flags( + group="pci", device_spec=self._to_list_of_json_str(device_spec)) + self.restart_compute_service(hostname="compute1") + + # Now we expect to see the device go reserved and the OTU trait get + # added to it. + expected_placement_view["inventories"][addr][rc]["reserved"] = 1 + expected_placement_view["traits"][addr].append("HW_PCI_ONE_TIME_USE") + self.assert_placement_pci_view( + "compute1", **expected_placement_view) diff --git a/nova/tests/functional/libvirt/test_pci_sriov_servers.py b/nova/tests/functional/libvirt/test_pci_sriov_servers.py index 2c70d29126..cfb3ac64a7 100644 --- a/nova/tests/functional/libvirt/test_pci_sriov_servers.py +++ b/nova/tests/functional/libvirt/test_pci_sriov_servers.py @@ -179,13 +179,21 @@ class _PCIServersTestBase(base.ServersTestBase): f"Number of inventories on {real_rp_name} are not as " f"expected. Expected {inv}, actual {rp_inv}" ) - for rc, total in inv.items(): - self.assertEqual( - total, - rp_inv[rc]["total"]) - self.assertEqual( - total, - rp_inv[rc]["max_unit"]) + for rc, inv_assertions in inv.items(): + if isinstance(inv_assertions, int): + # Simple case where we just specify total to assert + total = inv_assertions + self.assertEqual( + total, + rp_inv[rc]["total"]) + self.assertEqual( + total, + rp_inv[rc]["max_unit"]) + else: + # In this case, inv_assertions is a dict of attributes + # in the inventory + inv_to_assert = {k: rp_inv[rc][k] for k in inv_assertions} + self.assertEqual(inv_to_assert, inv_assertions) rp_traits = self._get_provider_traits(rp['uuid']) self.assertEqual(