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 {