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 <pascal.scholz@cyberus-technology.de>
On-behalf-of: SAP pascal.scholz@sap.com
Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
Pascal Scholz
2026-03-26 13:12:15 +01:00
committed by Rob Bradford
parent 8259f92909
commit f82eebc0b0
2 changed files with 164 additions and 0 deletions

View File

@@ -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)
}

View File

@@ -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 {