From f82eebc0b0c7113ec179b85f709bef5bfbc1f833 Mon Sep 17 00:00:00 2001 From: Pascal Scholz Date: Thu, 26 Mar 2026 13:12:15 +0100 Subject: [PATCH] tests: Add an integration test to verify PCI device allocations This commit adds an integration test to verify that the guest sees the correct BDF. Moreover, we check that we can allocate a random free BDF and that freeing BDFs works. Signed-off-by: Pascal Scholz On-behalf-of: SAP pascal.scholz@sap.com Signed-off-by: Rob Bradford --- cloud-hypervisor/tests/common/utils.rs | 26 +++++ cloud-hypervisor/tests/integration.rs | 138 +++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/cloud-hypervisor/tests/common/utils.rs b/cloud-hypervisor/tests/common/utils.rs index d39cbcc58..1821575d4 100644 --- a/cloud-hypervisor/tests/common/utils.rs +++ b/cloud-hypervisor/tests/common/utils.rs @@ -1033,3 +1033,29 @@ pub(crate) fn make_guest_panic(guest: &Guest) { // Trigger guest a panic guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); } + +/// Extracts a BDF from a CHV returned response +pub(crate) fn bdf_from_hotplug_response( + s: &str, +) -> ( + u16, /* Segment ID */ + u8, /* Bus ID */ + u8, /* Device ID */ + u8, /* Function ID */ +) { + let json: serde_json::Value = serde_json::from_str(s).expect("should be valid JSON"); + let bdf_str = json["bdf"] + .as_str() + .expect("should contain string key `bdf`"); + + // BDF format: "SSSS:BB:DD.F" + let parts: Vec<&str> = bdf_str.split(&[':', '.'][..]).collect(); + assert_eq!(parts.len(), 4, "unexpected BDF format: {bdf_str}"); + + let segment_id = u16::from_str_radix(parts[0], 16).unwrap(); + let bus_id = u8::from_str_radix(parts[1], 16).unwrap(); + let device_id = u8::from_str_radix(parts[2], 16).unwrap(); + let function_id = u8::from_str_radix(parts[3], 16).unwrap(); + + (segment_id, bus_id, device_id, function_id) +} diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 17c93ba1e..54394903c 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -5699,6 +5699,144 @@ mod common_parallel { handle_child_output(r, &output); } + + #[test] + fn test_pci_device_id() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + + let api_socket = temp_api_path(&guest.tmp_dir); + + // Boot without network + let mut cmd = GuestCommand::new(&guest); + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_net() + .default_disks() + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + + // Add a network device with non-static device id request + let r = std::panic::catch_unwind(|| { + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), + ); + assert!(cmd_success); + // We now know the first free device ID on the bus + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, first_free_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_ne!(first_free_device_id, 0); + + // We expect a match from grep + let _ = String::from( + guest + .ssh_command(&format!( + "lspci -n | grep \"00:{first_free_device_id:02x}.0\"" + )) + .unwrap() + .trim(), + ); + // Calculate the succeeding device ID + let device_id_to_allocate = first_free_device_id + 1; + // We expect the succeeding device ID to be free + assert!(matches!( + guest.ssh_command(&format!( + "lspci -n | grep \"00:{device_id_to_allocate:02x}.0\"" + )), + Err(SshCommandError::NonZeroExitStatus(1)) + )); + + // Add a device to the next device slot explicitly + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test1337,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id={}", + guest.network.guest_mac1, guest.network.host_ip1, device_id_to_allocate, + ) + .as_str(), + ), + ); + assert!(cmd_success); + // Retrieve what BDF we actually reserved and assert it's equal to that we wanted to reserve + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, allocated_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_eq!(device_id_to_allocate, allocated_device_id); + // Check that the device ID is really in use + let _ = String::from( + guest + .ssh_command(&format!( + "lspci -n | grep \"00:{allocated_device_id:02x}.0\"" + )) + .unwrap() + .trim(), + ); + // Remove the first device to create a hole + let cmd_success = remote_command(&api_socket, "remove-device", Some("test0")); + assert!(cmd_success); + thread::sleep(std::time::Duration::new(5, 0)); + // We left a hole in the used PCI IDs. The guest sees no device on the respective ID + assert!(matches!( + guest.ssh_command(&format!( + "lspci -n | grep \"00:{first_free_device_id:02x}.0\"" + )), + Err(SshCommandError::NonZeroExitStatus(1)) + )); + // Reuse the device ID hole by dynamically coalescing with the first free ID + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), + ); + assert!(cmd_success); + // Check that CHV reports that we added the same device to the same ID + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, allocated_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_eq!(first_free_device_id, allocated_device_id); + + // Check that guest sees the same device again at the same BDF + let _ = String::from( + guest + .ssh_command(&format!( + "lspci -n | grep \"00:{allocated_device_id:02x}.0\"" + )) + .unwrap() + .trim(), + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } } mod dbus_api {