Files
cloud-hypervisor/cloud-hypervisor/tests/common/tests_wrappers.rs
Rob Bradford 55adcc27da tests: Replace last vestiges of focal use with jammy
Focal has served us well for many years but is now beyond EOL. Remove
all remaining use of focal images from the CI.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-11 17:52:00 +00:00

3493 lines
108 KiB
Rust

// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::ffi::{CStr, CString};
use std::fs::{self, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::string::String;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use block::ImageType;
use net_util::MacAddr;
use test_infra::*;
use vmm_sys_util::tempdir::TempDir;
use vmm_sys_util::tempfile::TempFile;
use wait_timeout::ChildExt;
use crate::common::utils::{TargetApi, *};
// Start cloud-hypervisor with no VM parameters, only the API server running.
// From the API: Create a VM, boot it and check that it looks as expected.
pub(crate) fn _test_api_create_boot(target_api: &TargetApi, guest: &Guest) {
let mut child = GuestCommand::new(guest)
.args(target_api.guest_args())
.capture_output()
.spawn()
.unwrap();
// Wait for API server to be ready
assert!(wait_until(Duration::from_secs(5), || target_api
.remote_command("ping", None)));
// Create the VM first
let request_body = guest.api_create_body();
let temp_config_path = guest.tmp_dir.as_path().join("config");
std::fs::write(&temp_config_path, request_body).unwrap();
let create_config = temp_config_path.as_os_str().to_str().unwrap();
assert!(target_api.remote_command("create", Some(create_config),));
// Then boot it
assert!(target_api.remote_command("boot", None));
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// Start cloud-hypervisor with no VM parameters, only the API server running.
// From the API: Create a VM, boot it and check it can be shutdown and then
// booted again
pub(crate) fn _test_api_shutdown(target_api: &TargetApi, guest: &Guest) {
let mut child = GuestCommand::new(guest)
.args(target_api.guest_args())
.capture_output()
.spawn()
.unwrap();
// Wait for API server to be ready
assert!(wait_until(Duration::from_secs(5), || target_api
.remote_command("ping", None)));
// Create the VM first
let request_body = guest.api_create_body();
let temp_config_path = guest.tmp_dir.as_path().join("config");
std::fs::write(&temp_config_path, request_body).unwrap();
let create_config = temp_config_path.as_os_str().to_str().unwrap();
let r = std::panic::catch_unwind(|| {
assert!(target_api.remote_command("create", Some(create_config)));
// Then boot it
assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
// Sync and shutdown without powering off to prevent filesystem
// corruption.
guest.ssh_command("sync").unwrap();
guest.ssh_command("sudo shutdown -H now").unwrap();
// Wait for the guest to be fully shutdown
assert!(guest.wait_for_ssh_unresponsive(Duration::from_secs(20)));
// Then shut it down
assert!(target_api.remote_command("shutdown", None));
// Then boot it again
assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// Start cloud-hypervisor with no VM parameters, only the API server running.
// From the API: Create a VM, boot it and check it can be deleted and then recreated
// booted again.
pub(crate) fn _test_api_delete(target_api: &TargetApi, guest: &Guest) {
let mut child = GuestCommand::new(guest)
.args(target_api.guest_args())
.capture_output()
.spawn()
.unwrap();
// Wait for API server to be ready
assert!(wait_until(Duration::from_secs(5), || target_api
.remote_command("ping", None)));
// Create the VM first
let request_body = guest.api_create_body();
let temp_config_path = guest.tmp_dir.as_path().join("config");
std::fs::write(&temp_config_path, request_body).unwrap();
let create_config = temp_config_path.as_os_str().to_str().unwrap();
let r = std::panic::catch_unwind(|| {
assert!(target_api.remote_command("create", Some(create_config)));
// Then boot it
assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
// Sync and shutdown without powering off to prevent filesystem
// corruption.
guest.ssh_command("sync").unwrap();
guest.ssh_command("sudo shutdown -H now").unwrap();
// Wait for the guest to be fully shutdown
assert!(guest.wait_for_ssh_unresponsive(Duration::from_secs(20)));
// Then delete it
assert!(target_api.remote_command("delete", None));
assert!(target_api.remote_command("create", Some(create_config)));
// Then boot it again
assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// Start cloud-hypervisor with no VM parameters, only the API server running.
// From the API: Create a VM, boot it and check that it looks as expected.
// Then we pause the VM, check that it's no longer available.
// Finally we resume the VM and check that it's available.
pub(crate) fn _test_api_pause_resume(target_api: &TargetApi, guest: &Guest) {
let mut child = GuestCommand::new(guest)
.args(target_api.guest_args())
.capture_output()
.spawn()
.unwrap();
// Wait for API server to be ready
assert!(wait_until(Duration::from_secs(5), || target_api
.remote_command("ping", None)));
// Create the VM first
let request_body = guest.api_create_body();
let temp_config_path = guest.tmp_dir.as_path().join("config");
std::fs::write(&temp_config_path, request_body).unwrap();
let create_config = temp_config_path.as_os_str().to_str().unwrap();
assert!(target_api.remote_command("create", Some(create_config)));
// Then boot it
assert!(target_api.remote_command("boot", None));
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check that the VM booted as expected
guest.validate_cpu_count(None);
guest.validate_memory(None);
// We now pause the VM
assert!(target_api.remote_command("pause", None));
// Check pausing again fails
assert!(!target_api.remote_command("pause", None));
thread::sleep(std::time::Duration::new(2, 0));
// SSH into the VM should fail
ssh_command_ip(
"grep -c processor /proc/cpuinfo",
&guest.network.guest_ip0,
2,
5,
)
.unwrap_err();
// Resume the VM
assert!(target_api.remote_command("resume", None));
// Check resuming again fails
assert!(!target_api.remote_command("resume", None));
thread::sleep(std::time::Duration::new(2, 0));
// Now we should be able to SSH back in and get the right number of CPUs
guest.validate_cpu_count(None);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_pty_interaction(pty_path: PathBuf) {
let mut cf = std::fs::OpenOptions::new()
.write(true)
.read(true)
.open(pty_path)
.unwrap();
// Some dumb sleeps but we don't want to write
// before the console is up and we don't want
// to try and write the next line before the
// login process is ready.
thread::sleep(std::time::Duration::new(5, 0));
assert_eq!(cf.write(b"cloud\n").unwrap(), 6);
thread::sleep(std::time::Duration::new(2, 0));
assert_eq!(cf.write(b"cloud123\n").unwrap(), 9);
thread::sleep(std::time::Duration::new(2, 0));
assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22);
thread::sleep(std::time::Duration::new(2, 0));
// read pty and ensure they have a login shell
// some fairly hacky workarounds to avoid looping
// forever in case the channel is blocked getting output
let ptyc = pty_read(cf);
let mut empty = 0;
let mut prev = String::new();
loop {
thread::sleep(std::time::Duration::new(2, 0));
match ptyc.try_recv() {
Ok(line) => {
empty = 0;
prev = prev + &line;
if prev.contains("test_pty_console") {
break;
}
}
Err(mpsc::TryRecvError::Empty) => {
empty += 1;
assert!(empty <= 5, "No login on pty");
}
_ => {
panic!("No login on pty")
}
}
}
}
pub(crate) fn test_cpu_topology(
threads_per_core: u8,
cores_per_package: u8,
packages: u8,
use_fw: bool,
) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let total_vcpus = threads_per_core * cores_per_package * packages;
let direct_kernel_boot_path = direct_kernel_boot_path();
let mut kernel_path = direct_kernel_boot_path.to_str().unwrap();
let fw_path = fw_path(FwType::RustHypervisorFirmware);
if use_fw {
kernel_path = fw_path.as_str();
}
let mut child = GuestCommand::new(&guest)
.args([
"--cpus",
&format!(
"boot={total_vcpus},topology={threads_per_core}:{cores_per_package}:1:{packages}"
),
])
.default_memory()
.args(["--kernel", kernel_path])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest.get_cpu_count().unwrap_or_default(),
u32::from(total_vcpus)
);
assert_eq!(
guest
.ssh_command("lscpu | grep \"per core\" | cut -f 2 -d \":\" | sed \"s# *##\"")
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(0),
threads_per_core
);
assert_eq!(
guest
.ssh_command("lscpu | grep \"per socket\" | cut -f 2 -d \":\" | sed \"s# *##\"")
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(0),
cores_per_package
);
assert_eq!(
guest
.ssh_command("lscpu | grep \"Socket\" | cut -f 2 -d \":\" | sed \"s# *##\"")
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(0),
packages
);
#[cfg(target_arch = "x86_64")]
{
let mut cpu_id = 0;
for package_id in 0..packages {
for core_id in 0..cores_per_package {
for _ in 0..threads_per_core {
assert_eq!(
guest
.ssh_command(&format!("cat /sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id"))
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(0),
package_id
);
assert_eq!(
guest
.ssh_command(&format!(
"cat /sys/devices/system/cpu/cpu{cpu_id}/topology/core_id"
))
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(0),
core_id
);
cpu_id += 1;
}
}
}
}
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[allow(unused_variables)]
pub(crate) fn _test_guest_numa_nodes(acpi: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
#[cfg(target_arch = "aarch64")]
let kernel_path = if acpi {
edk2_path()
} else {
direct_kernel_boot_path()
};
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=6,max=12"])
.args(["--memory", "size=0,hotplug_method=virtio-mem"])
.args([
"--memory-zone",
"id=mem0,size=1G,hotplug_size=3G",
"id=mem1,size=2G,hotplug_size=3G",
"id=mem2,size=3G,hotplug_size=3G",
])
.args([
"--numa",
"guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0",
"guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1",
"guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2",
])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--api-socket", &api_socket])
.capture_output()
.default_disks()
.default_net()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
guest.check_numa_common(
Some(&[960_000, 1_920_000, 2_880_000]),
Some(&[&[0, 1, 2], &[3, 4], &[5]]),
Some(&["10 15 20", "20 10 25", "25 30 10"]),
);
// AArch64 currently does not support hotplug, and therefore we only
// test hotplug-related function on x86_64 here.
#[cfg(target_arch = "x86_64")]
{
guest.enable_memory_hotplug();
// Resize every memory zone and check each associated NUMA node
// has been assigned the right amount of memory.
resize_zone_command(&api_socket, "mem0", "4G");
resize_zone_command(&api_socket, "mem1", "4G");
resize_zone_command(&api_socket, "mem2", "4G");
// Resize to the maximum amount of CPUs and check each NUMA
// node has been assigned the right CPUs set.
resize_command(&api_socket, Some(12), None, None, None);
thread::sleep(std::time::Duration::new(5, 0));
guest.check_numa_common(
Some(&[3_840_000, 3_840_000, 3_840_000]),
Some(&[&[0, 1, 2, 9], &[3, 4, 6, 7, 8], &[5, 10, 11]]),
None,
);
}
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[allow(unused_variables)]
pub(crate) fn _test_power_button(guest: &Guest) {
let mut cmd = GuestCommand::new(guest);
let api_socket = temp_api_path(&guest.tmp_dir);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.capture_output()
.default_disks()
.default_net()
.args(["--api-socket", &api_socket]);
let child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(remote_command(&api_socket, "power-button", None));
});
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
handle_child_output(r, &output);
}
pub(crate) fn test_vhost_user_net(
tap: Option<&str>,
num_queues: usize,
prepare_daemon: &PrepareNetDaemon,
generate_host_mac: bool,
client_mode_daemon: bool,
) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let kernel_path = direct_kernel_boot_path();
let host_mac = if generate_host_mac {
Some(MacAddr::local_random())
} else {
None
};
let mtu = Some(3000);
let (mut daemon_command, vunet_socket_path) = prepare_daemon(
&guest.tmp_dir,
&guest.network.host_ip0,
tap,
mtu,
num_queues,
client_mode_daemon,
);
let net_params = format!(
"vhost_user=true,mac={},socket={},num_queues={},queue_size=1024{},vhost_mode={},mtu=3000",
guest.network.guest_mac0,
vunet_socket_path,
num_queues,
if let Some(host_mac) = host_mac {
format!(",host_mac={host_mac}")
} else {
String::new()
},
if client_mode_daemon {
"server"
} else {
"client"
},
);
let mut ch_command = GuestCommand::new(&guest);
ch_command
.args(["--cpus", format!("boot={}", num_queues / 2).as_str()])
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &api_socket])
.capture_output();
let mut daemon_child: std::process::Child;
let mut child: std::process::Child;
if client_mode_daemon {
child = ch_command.spawn().unwrap();
// Wait for the VMM to create the socket before starting the daemon
assert!(wait_until(Duration::from_secs(10), || Path::new(
&vunet_socket_path
)
.exists()));
daemon_child = daemon_command.spawn().unwrap();
} else {
daemon_child = daemon_command.spawn().unwrap();
// Wait for the daemon to create the socket before starting the VMM
assert!(wait_until(Duration::from_secs(10), || Path::new(
&vunet_socket_path
)
.exists()));
child = ch_command.spawn().unwrap();
}
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
if let Some(tap_name) = tap {
let tap_count = exec_host_command_output(&format!("ip link | grep -c {tap_name}"));
assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1");
}
if let Some(host_mac) = tap {
let mac_count = exec_host_command_output(&format!("ip link | grep -c {host_mac}"));
assert_eq!(String::from_utf8_lossy(&mac_count.stdout).trim(), "1");
}
#[cfg(target_arch = "aarch64")]
let iface = "enp0s4";
#[cfg(target_arch = "x86_64")]
let iface = "ens4";
assert_eq!(
guest
.ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str())
.unwrap()
.trim(),
"3000"
);
// 1 network interface + default localhost ==> 2 interfaces
// It's important to note that this test is fully exercising the
// vhost-user-net implementation and the associated backend since
// it does not define any --net network interface. That means all
// the ssh communication in that test happens through the network
// interface backed by vhost-user-net.
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
// The following pci devices will appear on guest with PCI-MSI
// interrupt vectors assigned.
// 1 virtio-console with 3 vectors: config, Rx, Tx
// 1 virtio-blk with 2 vectors: config, Request
// 1 virtio-blk with 2 vectors: config, Request
// 1 virtio-rng with 2 vectors: config, Request
// Since virtio-net has 2 queue pairs, its vectors is as follows:
// 1 virtio-net with 5 vectors: config, Rx (2), Tx (2)
// Based on the above, the total vectors should 14.
let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern());
assert_eq!(
guest
.ssh_command(&grep_cmd)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
10 + (num_queues as u32)
);
// ACPI feature is needed.
#[cfg(target_arch = "x86_64")]
{
guest.enable_memory_hotplug();
// Add RAM to the VM
let desired_ram = 1024 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
// Here by simply checking the size (through ssh), we validate
// the connection is still working, which means vhost-user-net
// keeps working after the resize.
assert!(wait_until(Duration::from_secs(10), || guest
.get_total_memory()
.unwrap_or_default()
> 960_000));
}
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let _ = daemon_child.kill();
let _ = daemon_child.wait();
handle_child_output(r, &output);
}
type PrepareBlkDaemon = dyn Fn(&TempDir, &str, usize, bool, bool) -> (std::process::Child, String);
pub(crate) fn test_vhost_user_blk(
num_queues: usize,
readonly: bool,
direct: bool,
prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>,
) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let kernel_path = direct_kernel_boot_path();
let (blk_params, daemon_child) = {
let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap();
// Start the daemon
let (daemon_child, vubd_socket_path) =
prepare_daemon(&guest.tmp_dir, "blk.img", num_queues, readonly, direct);
(
format!(
"vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128",
),
Some(daemon_child),
)
};
let mut child = GuestCommand::new(&guest)
.args(["--cpus", format!("boot={num_queues}").as_str()])
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
blk_params.as_str(),
])
.default_net()
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check both if /dev/vdc exists and if the block size is 16M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Check if this block is RO or RW.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | awk '{print $5}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
readonly as u32
);
// Check if the number of queues in /sys/block/vdc/mq matches the
// expected num_queues.
assert_eq!(
guest
.ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
num_queues as u32
);
// Mount the device
let mount_ro_rw_flag = if readonly { "ro,noload" } else { "rw" };
guest.ssh_command("mkdir mount_image").unwrap();
guest
.ssh_command(
format!("sudo mount -o {mount_ro_rw_flag} -t ext4 /dev/vdc mount_image/").as_str(),
)
.unwrap();
// Check the content of the block device. The file "foo" should
// contain "bar".
assert_eq!(
guest.ssh_command("cat mount_image/foo").unwrap().trim(),
"bar"
);
// ACPI feature is needed.
#[cfg(target_arch = "x86_64")]
{
guest.enable_memory_hotplug();
// Add RAM to the VM
let desired_ram = 1024 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
assert!(wait_until(Duration::from_secs(10), || guest
.get_total_memory()
.unwrap_or_default()
> 960_000));
// Check again the content of the block device after the resize
// has been performed.
assert_eq!(
guest.ssh_command("cat mount_image/foo").unwrap().trim(),
"bar"
);
}
// Unmount the device
guest.ssh_command("sudo umount /dev/vdc").unwrap();
guest.ssh_command("rm -r mount_image").unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
if let Some(mut daemon_child) = daemon_child {
let _ = daemon_child.kill();
let _ = daemon_child.wait();
}
handle_child_output(r, &output);
}
pub(crate) fn test_boot_from_vhost_user_blk(
num_queues: usize,
readonly: bool,
direct: bool,
prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>,
) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let disk_path = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
let (blk_boot_params, daemon_child) = {
let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap();
// Start the daemon
let (daemon_child, vubd_socket_path) = prepare_daemon(
&guest.tmp_dir,
disk_path.as_str(),
num_queues,
readonly,
direct,
);
(
format!(
"vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128",
),
Some(daemon_child),
)
};
let mut child = GuestCommand::new(&guest)
.args(["--cpus", format!("boot={num_queues}").as_str()])
.args(["--memory", "size=512M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
blk_boot_params.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Just check the VM booted correctly.
assert_eq!(guest.get_cpu_count().unwrap_or_default(), num_queues as u32);
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
if let Some(mut daemon_child) = daemon_child {
let _ = daemon_child.kill();
let _ = daemon_child.wait();
}
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_fs(
prepare_daemon: &dyn Fn(&TempDir, &str) -> (std::process::Child, String),
hotplug: bool,
use_generic_vhost_user: bool,
pci_segment: Option<u16>,
) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
let mut shared_dir = workload_path;
shared_dir.push("shared_dir");
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
#[cfg(target_arch = "aarch64")]
let kernel_path = if hotplug {
edk2_path()
} else {
direct_kernel_boot_path()
};
let (mut daemon_child, virtiofsd_socket_path) =
prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap());
let mut guest_command = GuestCommand::new(&guest);
guest_command
.default_cpus()
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--api-socket", &api_socket])
.args(["--event-monitor", format!("path={event_path}").as_str()]);
if pci_segment.is_some() {
guest_command.args([
"--platform",
&format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"),
]);
}
let fs_params = format!(
"socket={},id=myfs0,{}{}",
virtiofsd_socket_path,
if use_generic_vhost_user {
"queue_sizes=[1024,1024],virtio_id=26"
} else {
"tag=myfs,num_queues=1,queue_size=1024"
},
if let Some(pci_segment) = pci_segment {
format!(",pci_segment={pci_segment}")
} else {
String::new()
}
);
if !hotplug {
guest_command.args([
if use_generic_vhost_user {
"--generic-vhost-user"
} else {
"--fs"
},
fs_params.as_str(),
]);
}
let mut child = guest_command.capture_output().spawn().unwrap();
let add_arg = if use_generic_vhost_user {
"add-generic-vhost-user"
} else {
"add-fs"
};
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
if hotplug {
// Add fs to the VM
let (cmd_success, cmd_output, _) =
remote_command_w_output(&api_socket, add_arg, Some(&fs_params));
assert!(cmd_success);
if let Some(pci_segment) = pci_segment {
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
"{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
)));
} else {
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")
);
}
}
// Mount shared directory through virtio_fs filesystem
guest
.wait_for_ssh_command(
"mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/",
Duration::from_secs(10),
)
.unwrap();
// Check file1 exists and its content is "foo"
assert_eq!(
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
"foo"
);
// Check file2 does not exist
guest
.ssh_command("[ ! -f 'mount_dir/file2' ] || true")
.unwrap();
// Check file3 exists and its content is "bar"
assert_eq!(
guest.ssh_command("cat mount_dir/file3").unwrap().trim(),
"bar"
);
// ACPI feature is needed.
#[cfg(target_arch = "x86_64")]
{
guest.enable_memory_hotplug();
// Add RAM to the VM
let desired_ram = 1024 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
assert!(wait_until(Duration::from_secs(30), || guest
.get_total_memory()
.unwrap_or_default()
> 960_000));
// After the resize, check again that file1 exists and its
// content is "foo".
assert_eq!(
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
"foo"
);
}
if hotplug {
// Remove from VM
guest.ssh_command("sudo umount mount_dir").unwrap();
assert!(remote_command(&api_socket, "remove-device", Some("myfs0")));
// Wait for the device to be fully removed before re-adding
let removed_event = MetaEvent {
event: "device-removed".to_string(),
device_id: Some("myfs0".to_string()),
};
assert!(wait_until(Duration::from_secs(10), || {
check_sequential_events(&[&removed_event], &event_path)
}));
}
});
let (r, hotplug_daemon_child) = if r.is_ok() && hotplug {
let _ = daemon_child.kill();
let _ = daemon_child.wait();
// Remove the stale socket so wait_for_virtiofsd_socket actually waits
let _ = std::fs::remove_file(&virtiofsd_socket_path);
let (daemon_child, virtiofsd_socket_path) =
prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap());
let r = std::panic::catch_unwind(|| {
// Wait for the daemon socket to be ready
assert!(wait_until(Duration::from_secs(10), || Path::new(
&virtiofsd_socket_path
)
.exists()));
let fs_params = format!(
"id=myfs0,socket={},{}{}",
virtiofsd_socket_path,
if use_generic_vhost_user {
"queue_sizes=[1024,1024],virtio_id=26"
} else {
"tag=myfs,num_queues=1,queue_size=1024"
},
if let Some(pci_segment) = pci_segment {
format!(",pci_segment={pci_segment}")
} else {
String::new()
}
);
// Add back and check it works
let (cmd_success, cmd_output, _) =
remote_command_w_output(&api_socket, add_arg, Some(&fs_params));
assert!(cmd_success);
if let Some(pci_segment) = pci_segment {
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
"{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
)));
} else {
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")
);
}
// Mount shared directory through virtio_fs filesystem, retrying
// until the hotplugged device is recognized by the guest
guest
.wait_for_ssh_command(
"mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/",
Duration::from_secs(10),
)
.unwrap();
// Check file1 exists and its content is "foo"
assert_eq!(
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
"foo"
);
});
(r, Some(daemon_child))
} else {
(r, None)
};
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let _ = daemon_child.kill();
let _ = daemon_child.wait();
if let Some(mut daemon_child) = hotplug_daemon_child {
let _ = daemon_child.kill();
let _ = daemon_child.wait();
}
handle_child_output(r, &output);
}
pub(crate) fn test_virtio_pmem(discard_writes: bool, specify_size: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let pmem_temp_file = TempFile::new().unwrap();
pmem_temp_file.as_file().set_len(128 << 20).unwrap();
std::process::Command::new("mkfs.ext4")
.arg(pmem_temp_file.as_path())
.output()
.expect("Expect creating disk image to succeed");
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args([
"--pmem",
format!(
"file={}{}{}",
pmem_temp_file.as_path().to_str().unwrap(),
if specify_size { ",size=128M" } else { "" },
if discard_writes {
",discard_writes=on"
} else {
""
}
)
.as_str(),
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check for the presence of /dev/pmem0
assert_eq!(
guest.ssh_command("ls /dev/pmem0").unwrap().trim(),
"/dev/pmem0"
);
// Check changes persist after reboot
assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), "");
assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n");
guest
.ssh_command("echo test123 | sudo tee /mnt/test")
.unwrap();
assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), "");
assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "");
guest.reboot_linux(0);
assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), "");
assert_eq!(
guest
.ssh_command("sudo cat /mnt/test || true")
.unwrap()
.trim(),
if discard_writes { "" } else { "test123" }
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_vsock(guest: &Guest, hotplug: bool) {
let socket = temp_vsock_path(&guest.tmp_dir);
let api_socket = temp_api_path(&guest.tmp_dir);
let mut cmd = GuestCommand::new(guest);
cmd.args(["--api-socket", &api_socket]);
cmd.default_cpus();
cmd.default_memory();
cmd.default_kernel_cmdline();
cmd.default_disks();
cmd.default_net();
if !hotplug {
cmd.args(["--vsock", format!("cid=3,socket={socket}").as_str()]);
}
let mut child = cmd.capture_output().spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
if hotplug {
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-vsock",
Some(format!("cid=3,socket={socket},id=test0").as_str()),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")
);
thread::sleep(std::time::Duration::new(10, 0));
// Check adding a second one fails
assert!(!remote_command(
&api_socket,
"add-vsock",
Some("cid=1234,socket=/tmp/fail")
));
}
// Validate vsock works as expected.
guest.check_vsock(socket.as_str());
guest.reboot_linux(0);
// Validate vsock still works after a reboot.
guest.check_vsock(socket.as_str());
if hotplug {
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
}
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn test_memory_mergeable(mergeable: bool) {
let memory_param = if mergeable {
"mergeable=on"
} else {
"mergeable=off"
};
// We assume the number of shared pages in the rest of the system to be constant
let ksm_ps_init = get_ksm_pages_shared();
let disk_config1 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest1 = Guest::new(Box::new(disk_config1));
let mut child1 = GuestCommand::new(&guest1)
.default_cpus()
.args(["--memory", format!("size=512M,{memory_param}").as_str()])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", guest1.default_net_string().as_str()])
.args(["--serial", "tty", "--console", "off"])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest1.wait_vm_boot().unwrap();
});
if r.is_err() {
kill_child(&mut child1);
let output = child1.wait_with_output().unwrap();
handle_child_output(r, &output);
panic!("Test should already be failed/panicked"); // To explicitly mark this block never return
}
let ksm_ps_guest1 = get_ksm_pages_shared();
let disk_config2 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest2 = Guest::new(Box::new(disk_config2));
let mut child2 = GuestCommand::new(&guest2)
.default_cpus()
.args(["--memory", format!("size=512M,{memory_param}").as_str()])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", guest2.default_net_string().as_str()])
.args(["--serial", "tty", "--console", "off"])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest2.wait_vm_boot().unwrap();
let ksm_ps_guest2 = get_ksm_pages_shared();
if mergeable {
println!(
"ksm pages_shared after vm1 booted '{ksm_ps_guest1}', ksm pages_shared after vm2 booted '{ksm_ps_guest2}'"
);
// We are expecting the number of shared pages to increase as the number of VM increases
assert!(ksm_ps_guest1 < ksm_ps_guest2);
} else {
assert!(ksm_ps_guest1 == ksm_ps_init);
assert!(ksm_ps_guest2 == ksm_ps_init);
}
});
kill_child(&mut child1);
kill_child(&mut child2);
let output = child1.wait_with_output().unwrap();
child2.wait().unwrap();
handle_child_output(r, &output);
}
// This test validates that it can find the virtio-iommu device at first.
// It also verifies that both disks and the network card are attached to
// the virtual IOMMU by looking at /sys/kernel/iommu_groups directory.
// The last interesting part of this test is that it exercises the network
// interface attached to the virtual IOMMU since this is the one used to
// send all commands through SSH.
pub(crate) fn _test_virtio_iommu(_acpi: bool /* not needed on x86_64 */) {
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 = if _acpi {
edk2_path()
} else {
direct_kernel_boot_path()
};
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
format!(
"path={},iommu=on",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={},iommu=on",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
])
.args(["--net", guest.default_net_string_w_iommu().as_str()])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Verify the virtio-iommu device is present.
assert!(
guest
.does_device_vendor_pair_match("0x1057", "0x1af4")
.unwrap_or_default()
);
// On AArch64, if the guest system boots from FDT, the behavior of IOMMU is a bit
// different with ACPI.
// All devices on the PCI bus will be attached to the virtual IOMMU, except the
// virtio-iommu device itself. So these devices will all be added to IOMMU groups,
// and appear under folder '/sys/kernel/iommu_groups/'.
//
// Verify the first disk is in an iommu group.
assert!(
guest
.ssh_command("ls /sys/kernel/iommu_groups/*/devices")
.unwrap()
.contains("0000:00:02.0")
);
// Verify the second disk is in an iommu group.
assert!(
guest
.ssh_command("ls /sys/kernel/iommu_groups/*/devices")
.unwrap()
.contains("0000:00:03.0")
);
// Verify the network card is in an iommu group.
assert!(
guest
.ssh_command("ls /sys/kernel/iommu_groups/*/devices")
.unwrap()
.contains("0000:00:04.0")
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// ivshmem test
// This case validates that read data from host(host write data to ivshmem backend file,
// guest read data from ivshmem pci bar2 memory)
// and write data to host(guest write data to ivshmem pci bar2 memory, host read it from
// ivshmem backend file).
// It also checks the size of the shared memory region.
pub(crate) fn _test_ivshmem(guest: &Guest, ivshmem_file_path: impl AsRef<Path>, file_size: &str) {
let ivshmem_file_path = ivshmem_file_path.as_ref();
let test_message_read = String::from("ivshmem device test data read");
// Modify backend file data before function test
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(ivshmem_file_path)
.unwrap();
file.seek(SeekFrom::Start(0)).unwrap();
file.write_all(test_message_read.as_bytes()).unwrap();
file.write_all(b"\0").unwrap();
file.flush().unwrap();
let output = fs::read_to_string(ivshmem_file_path).unwrap();
let nul_pos = output.as_bytes().iter().position(|&b| b == 0).unwrap();
let c_str = CStr::from_bytes_until_nul(&output.as_bytes()[..=nul_pos]).unwrap();
let file_message = c_str.to_string_lossy().to_string();
// Check if the backend file data is correct
assert_eq!(test_message_read, file_message);
let device_id_line = String::from(
guest
.ssh_command("lspci -D | grep \"Inter-VM shared memory\"")
.unwrap()
.trim(),
);
// Check if ivshmem exists
assert!(!device_id_line.is_empty());
let device_id = device_id_line.split(" ").next().unwrap();
// Check shard memory size
assert_eq!(
guest
.ssh_command(
format!("lspci -vv -s {device_id} | grep -c \"Region 2.*size={file_size}\"")
.as_str(),
)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// guest don't have gcc or g++, try to use python to test :(
// This python program try to mmap the ivshmem pci bar2 memory and read the data from it.
let ivshmem_test_read = format!(
r#"
import os
import mmap
from ctypes import create_string_buffer, c_char, memmove
if __name__ == "__main__":
device_path = f"/sys/bus/pci/devices/{device_id}/resource2"
fd = os.open(device_path, os.O_RDWR | os.O_SYNC)
PAGE_SIZE = os.sysconf('SC_PAGESIZE')
with mmap.mmap(fd, PAGE_SIZE, flags=mmap.MAP_SHARED,
prot=mmap.PROT_READ | mmap.PROT_WRITE, offset=0) as shmem:
c_buf = (c_char * PAGE_SIZE).from_buffer(shmem)
null_pos = c_buf.raw.find(b'\x00')
valid_data = c_buf.raw[:null_pos] if null_pos != -1 else c_buf.raw
print(valid_data.decode('utf-8', errors='replace'), end="")
shmem.flush()
del c_buf
os.close(fd)
"#
);
guest
.ssh_command(
format!(
r#"cat << EOF > test_read.py
{ivshmem_test_read}
EOF
"#
)
.as_str(),
)
.unwrap();
let guest_message = guest.ssh_command("sudo python3 test_read.py").unwrap();
// Check the probe message in host and guest
assert_eq!(test_message_read, guest_message);
let test_message_write = "ivshmem device test data write";
// Then the program writes a test message to the memory and flush it.
let ivshmem_test_write = format!(
r#"
import os
import mmap
from ctypes import create_string_buffer, c_char, memmove
if __name__ == "__main__":
device_path = f"/sys/bus/pci/devices/{device_id}/resource2"
test_message = "{test_message_write}"
fd = os.open(device_path, os.O_RDWR | os.O_SYNC)
PAGE_SIZE = os.sysconf('SC_PAGESIZE')
with mmap.mmap(fd, PAGE_SIZE, flags=mmap.MAP_SHARED,
prot=mmap.PROT_READ | mmap.PROT_WRITE, offset=0) as shmem:
shmem.flush()
c_buf = (c_char * PAGE_SIZE).from_buffer(shmem)
encoded_msg = test_message.encode('utf-8').ljust(1000, b'\x00')
memmove(c_buf, encoded_msg, len(encoded_msg))
shmem.flush()
del c_buf
os.close(fd)
"#
);
guest
.ssh_command(
format!(
r#"cat << EOF > test_write.py
{ivshmem_test_write}
EOF
"#
)
.as_str(),
)
.unwrap();
let _ = guest.ssh_command("sudo python3 test_write.py").unwrap();
let output = fs::read_to_string(ivshmem_file_path).unwrap();
let nul_pos = output.as_bytes().iter().position(|&b| b == 0).unwrap();
let c_str = CStr::from_bytes_until_nul(&output.as_bytes()[..=nul_pos]).unwrap();
let file_message = c_str.to_string_lossy().to_string();
// Check to send data from guest to host
assert_eq!(test_message_write, file_message);
}
pub(crate) fn _test_simple_launch(guest: &Guest) {
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.args(["--serial", "tty", "--console", "off"])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
guest.validate_cpu_count(None);
guest.validate_memory(None);
assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000");
assert!(check_sequential_events(
&guest
.get_expected_seq_events_for_simple_launch()
.iter()
.collect::<Vec<_>>(),
&event_path
));
// It's been observed on the Bionic image that udev and snapd
// services can cause some delay in the VM's shutdown. Disabling
// them improves the reliability of this test.
let _ = guest.ssh_command("sudo systemctl disable udev");
let _ = guest.ssh_command("sudo systemctl stop udev");
let _ = guest.ssh_command("sudo systemctl disable snapd");
let _ = guest.ssh_command("sudo systemctl stop snapd");
guest.ssh_command("sudo poweroff").unwrap();
let latest_events = [
&MetaEvent {
event: "shutdown".to_string(),
device_id: None,
},
&MetaEvent {
event: "deleted".to_string(),
device_id: None,
},
&MetaEvent {
event: "shutdown".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(20), || {
check_latest_events_exact(&latest_events, &event_path)
}));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_multi_cpu(guest: &Guest) {
let mut cmd = GuestCommand::new(guest);
cmd.args(["--cpus", "boot=2,max=4"])
.default_memory()
.default_kernel_cmdline()
.capture_output()
.default_disks()
.default_net();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
assert_eq!(
guest
.ssh_command(r#"sudo dmesg | grep "smp: Brought up" | sed "s/\[\ *[0-9.]*\] //""#)
.unwrap()
.trim(),
"smp: Brought up 1 node, 2 CPUs"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_cpu_affinity(guest: &Guest) {
// We need the host to have at least 4 CPUs if we want to be able
// to run this test.
let host_cpus_count = exec_host_command_output("nproc");
assert!(
String::from_utf8_lossy(&host_cpus_count.stdout)
.trim()
.parse::<u16>()
.unwrap_or(0)
>= 4
);
let mut child = GuestCommand::new(guest)
.default_cpus_with_affinity()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let pid = child.id();
let taskset_vcpu0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_vcpu0.stdout).trim(), "0,2");
let taskset_vcpu1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_vcpu1.stdout).trim(), "1,3");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_queue_affinity(guest: &Guest) {
// We need the host to have at least 4 CPUs if we want to be able
// to run this test.
let host_cpus_count = exec_host_command_output("nproc");
assert!(
String::from_utf8_lossy(&host_cpus_count.stdout)
.trim()
.parse::<u16>()
.unwrap_or(0)
>= 4
);
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={},num_queues=4,queue_affinity=[0@[0,2],1@[1,3],2@[1],3@[3]]",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let pid = child.id();
let taskset_q0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_q0.stdout).trim(), "0,2");
let taskset_q1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_q1.stdout).trim(), "1,3");
let taskset_q2 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q2 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_q2.stdout).trim(), "1");
let taskset_q3 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q3 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
assert_eq!(String::from_utf8_lossy(&taskset_q3.stdout).trim(), "3");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_pci_msi(guest: &Guest) {
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.capture_output()
.default_disks()
.default_net();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern());
let r = std::panic::catch_unwind(|| {
assert_eq!(
guest
.ssh_command(&grep_cmd)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
12
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_net_ctrl_queue(guest: &Guest) {
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.args(["--net", guest.default_net_string_w_mtu(3000).as_str()])
.capture_output()
.default_disks();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
#[cfg(target_arch = "aarch64")]
let iface = "enp0s4";
#[cfg(target_arch = "x86_64")]
let iface = "ens4";
let r = std::panic::catch_unwind(|| {
assert_eq!(
guest
.ssh_command(
format!("sudo ethtool -K {iface} rx-gro-hw off && echo success").as_str()
)
.unwrap()
.trim(),
"success"
);
assert_eq!(
guest
.ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str())
.unwrap()
.trim(),
"3000"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_pci_multiple_segments(
guest: &Guest,
max_num_pci_segments: u16,
pci_segments_for_disk: u16,
) {
// Prepare another disk file for the virtio-disk device
let test_disk_path = String::from(
guest
.tmp_dir
.as_path()
.join("test-disk.raw")
.to_str()
.unwrap(),
);
assert!(
exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success()
);
assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success());
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline_with_platform(Some(&format!(
"num_pci_segments={max_num_pci_segments}"
)))
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!("path={test_disk_path},pci_segment={pci_segments_for_disk},image_type=raw")
.as_str(),
])
.capture_output()
.default_net();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let grep_cmd = "lspci | grep \"Host bridge\" | wc -l";
let r = std::panic::catch_unwind(|| {
// There should be MAX_NUM_PCI_SEGMENTS PCI host bridges in the guest.
assert_eq!(
guest
.ssh_command(grep_cmd)
.unwrap()
.trim()
.parse::<u16>()
.unwrap_or_default(),
max_num_pci_segments
);
// Check both if /dev/vdc exists and if the block size is 4M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 4M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Mount the device.
guest.ssh_command("mkdir mount_image").unwrap();
guest
.ssh_command("sudo mount -o rw -t ext4 /dev/vdc mount_image/")
.unwrap();
// Grant all users with write permission.
guest.ssh_command("sudo chmod a+w mount_image/").unwrap();
// Write something to the device.
guest
.ssh_command("sudo echo \"bar\" >> mount_image/foo")
.unwrap();
// Check the content of the block device. The file "foo" should
// contain "bar".
assert_eq!(
guest
.ssh_command("sudo cat mount_image/foo")
.unwrap()
.trim(),
"bar"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_direct_kernel_boot(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
guest.validate_cpu_count(None);
guest.validate_memory(None);
let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern());
assert_eq!(
guest
.ssh_command(&grep_cmd)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
12
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_block(
guest: &Guest,
disable_io_uring: bool,
disable_aio: bool,
verify_os_disk: bool,
backing_files: bool,
image_type: ImageType,
) {
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
let mut blk_file_path = workload_path;
blk_file_path.push("blk.img");
let initial_backing_checksum = if verify_os_disk {
compute_backing_checksum(guest.disk_config.disk(DiskType::OperatingSystem).unwrap())
} else {
None
};
assert!(
guest.num_cpu >= 4,
"_test_virtio_block requires at least 4 CPUs to match num_queues=4"
);
let mut cloud_child = GuestCommand::new(guest)
.default_cpus()
.args(["--memory", "size=512M,shared=on"])
.default_kernel_cmdline()
.args([
"--disk",
format!(
"path={},backing_files={},image_type={image_type}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
if backing_files { "on" } else { "off" },
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!(
"path={},readonly=on,direct=on,num_queues=4,_disable_io_uring={},_disable_aio={}",
blk_file_path.to_str().unwrap(),
disable_io_uring,
disable_aio,
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check both if /dev/vdc exists and if the block size is 16M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Check both if /dev/vdc exists and if this block is RO.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | awk '{print $5}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Check if the number of queues is 4.
assert_eq!(
guest
.ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4
);
});
if verify_os_disk {
// Use clean shutdown to allow cloud-hypervisor to clear
// the dirty bit in the QCOW2 v3 image.
kill_child(&mut cloud_child);
} else {
let _ = cloud_child.kill();
}
let output = cloud_child.wait_with_output().unwrap();
handle_child_output(r, &output);
if verify_os_disk {
disk_check_consistency(
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
initial_backing_checksum,
);
}
}
pub fn _test_virtio_block_dynamic_vhdx_expand(guest: &Guest) {
const VIRTUAL_DISK_SIZE: u64 = 100 << 20;
const EMPTY_VHDX_FILE_SIZE: u64 = 8 << 20;
const FULL_VHDX_FILE_SIZE: u64 = 112 << 20;
const DYNAMIC_VHDX_NAME: &str = "dynamic.vhdx";
let vhdx_pathbuf = guest.tmp_dir.as_path().join(DYNAMIC_VHDX_NAME);
let vhdx_path = vhdx_pathbuf.to_str().unwrap();
// Generate a 100 MiB dynamic VHDX file
std::process::Command::new("qemu-img")
.arg("create")
.args(["-f", "vhdx"])
.arg(vhdx_path)
.arg(VIRTUAL_DISK_SIZE.to_string())
.output()
.expect("Expect generating dynamic VHDX image");
// Check if the size matches with empty VHDx file size
assert_eq!(vhdx_image_size(vhdx_path), EMPTY_VHDX_FILE_SIZE);
let mut cloud_child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!("path={vhdx_path}").as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check both if /dev/vdc exists and if the block size is 100 MiB.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 100M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write 100 MB of data to the VHDx disk
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=100")
.unwrap();
});
// Check if the size matches with expected expanded VHDx file size
assert_eq!(vhdx_image_size(vhdx_path), FULL_VHDX_FILE_SIZE);
kill_child(&mut cloud_child);
let output = cloud_child.wait_with_output().unwrap();
handle_child_output(r, &output);
disk_check_consistency(vhdx_path, None);
}
fn vhdx_image_size(disk_name: &str) -> u64 {
std::fs::File::open(disk_name)
.unwrap()
.seek(SeekFrom::End(0))
.unwrap()
}
#[cfg(target_arch = "x86_64")]
pub fn _test_split_irqchip(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("grep -c IO-APIC.*timer /proc/interrupts || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
assert_eq!(
guest
.ssh_command("grep -c IO-APIC.*cascade /proc/interrupts || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[cfg(target_arch = "x86_64")]
pub(crate) fn _test_dmi_serial_number(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline_with_platform(Some("serial_number=a=b;c=d"))
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("sudo cat /sys/class/dmi/id/product_serial")
.unwrap()
.trim(),
"a=b;c=d"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_dmi_uuid(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline_with_platform(Some("uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a"))
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("sudo cat /sys/class/dmi/id/product_uuid")
.unwrap()
.trim(),
"1e8aa28a-435d-4027-87f4-40dceff1fa0a"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_dmi_oem_strings(guest: &Guest) {
let s1 = "io.systemd.credential:xx=yy";
let s2 = "This is a test string";
let oem_strings = format!("oem_strings=[{s1},{s2}]");
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline_with_platform(Some(&oem_strings))
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("sudo dmidecode --oem-string count")
.unwrap()
.trim(),
"2"
);
assert_eq!(
guest
.ssh_command("sudo dmidecode --oem-string 1")
.unwrap()
.trim(),
s1
);
assert_eq!(
guest
.ssh_command("sudo dmidecode --oem-string 2")
.unwrap()
.trim(),
s2
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_serial_off(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.args(["--serial", "off"])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Test that there is no ttyS0
assert_eq!(
guest
.ssh_command(GREP_SERIAL_IRQ_CMD)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_multiple_network_interfaces(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args([
"--net",
guest.default_net_string().as_str(),
"tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.128",
"tap=mytap1,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.128",
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let tap_count = exec_host_command_output("ip link | grep -c mytap1");
assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1");
// 3 network interfaces + default localhost ==> 4 interfaces
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_console(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.args(["--console", "tty"])
.args(["--serial", "null"])
.capture_output()
.spawn()
.unwrap();
let text = String::from("On a branch floating down river a cricket, singing.");
let cmd = format!("echo {text} | sudo tee /dev/hvc0");
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(
guest
.does_device_vendor_pair_match("0x1043", "0x1af4")
.unwrap_or_default()
);
guest.ssh_command(&cmd).unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&output.stdout).contains(&text));
});
handle_child_output(r, &output);
}
pub(crate) fn _test_console_file(guest: &Guest) {
let console_path = guest.tmp_dir.as_path().join("console-output");
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.args([
"--console",
format!("file={}", console_path.to_str().unwrap()).as_str(),
])
.capture_output()
.spawn()
.unwrap();
guest.wait_vm_boot().unwrap();
guest.ssh_command("sudo shutdown -h now").unwrap();
let _ = child.wait_timeout(std::time::Duration::from_secs(20));
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let r = std::panic::catch_unwind(|| {
// Check that the cloud-hypervisor binary actually terminated
assert!(output.status.success());
// Do this check after shutdown of the VM as an easy way to ensure
// all writes are flushed to disk
let mut f = std::fs::File::open(console_path).unwrap();
let mut buf = String::new();
f.read_to_string(&mut buf).unwrap();
if !buf.contains(CONSOLE_TEST_STRING) {
eprintln!(
"\n\n==== Console file output ====\n\n{buf}\n\n==== End console file output ===="
);
}
assert!(buf.contains(CONSOLE_TEST_STRING));
});
handle_child_output(r, &output);
}
pub(crate) fn _test_direct_kernel_boot_noacpi(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_pci_bar_reprogramming(guest: &Guest) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args([
"--net",
guest.default_net_string().as_str(),
"tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.128",
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// 2 network interfaces + default localhost ==> 3 interfaces
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
3
);
let init_bar_addr = guest
.ssh_command("sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource")
.unwrap();
// Remove the PCI device
guest
.ssh_command("echo 1 | sudo tee /sys/bus/pci/devices/0000:00:05.0/remove")
.unwrap();
// Only 1 network interface left + default localhost ==> 2 interfaces
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
// Remove the PCI device
guest
.ssh_command("echo 1 | sudo tee /sys/bus/pci/rescan")
.unwrap();
// Back to 2 network interface + default localhost ==> 3 interfaces
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
3
);
let new_bar_addr = guest
.ssh_command("sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource")
.unwrap();
// Let's compare the BAR addresses for our virtio-net device.
// They should be different as we expect the BAR reprogramming
// to have happened.
assert_ne!(init_bar_addr, new_bar_addr);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_memory_overhead(guest: &Guest, guest_memory_size_kb: u32) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_net()
.default_disks()
.capture_output()
.spawn()
.unwrap();
guest.wait_vm_boot().unwrap();
let r = std::panic::catch_unwind(|| {
let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb);
eprintln!("Guest memory overhead: {overhead} vs {MAXIMUM_VMM_OVERHEAD_KB}");
assert!(overhead <= MAXIMUM_VMM_OVERHEAD_KB);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_landlock(guest: &Guest) {
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(guest)
.args(["--api-socket", &api_socket])
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.args(["--landlock"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check /dev/vdc is not there
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc.*16M || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
// Now let's add the extra disk.
let mut blk_file_path = dirs::home_dir().unwrap();
blk_file_path.push("workloads");
blk_file_path.push("blk.img");
// As the path to the hotplug disk is not pre-added, this remote
// command will fail.
assert!(!remote_command(
&api_socket,
"add-disk",
Some(
format!(
"path={},id=test0,readonly=true",
blk_file_path.to_str().unwrap()
)
.as_str()
),
));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_disk_hotplug(guest: &Guest, landlock_enabled: bool) {
let api_socket = temp_api_path(&guest.tmp_dir);
let mut blk_file_path = dirs::home_dir().unwrap();
blk_file_path.push("workloads");
blk_file_path.push("blk.img");
let mut cmd = GuestCommand::new(guest);
if landlock_enabled {
cmd.args(["--landlock"]).args([
"--landlock-rules",
format!("path={blk_file_path:?},access=rw").as_str(),
]);
}
cmd.args(["--api-socket", &api_socket])
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.default_net()
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check /dev/vdc is not there
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc.*16M || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
// Now let's add the extra disk.
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(
format!(
"path={},id=test0,readonly=true",
blk_file_path.to_str().unwrap()
)
.as_str(),
),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")
);
// Wait for the hotplugged disk to appear in the guest
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or_default() == 1)
}));
// And check the block device can be read.
guest
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16")
.unwrap();
// Let's remove it the extra disk.
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
// Wait for the disk to disappear
assert!(wait_until(Duration::from_secs(10), || guest
.ssh_command("lsblk | grep -c vdc.*16M || true")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or(1) == 0)));
// And add it back to validate unplug did work correctly.
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(
format!(
"path={},id=test0,readonly=true",
blk_file_path.to_str().unwrap()
)
.as_str(),
),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")
);
// Wait for the hotplugged disk to appear in the guest
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or_default() == 1)
}));
// And check the block device can be read.
guest
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16")
.unwrap();
// Reboot the VM.
guest.reboot_linux(0);
// Check still there after reboot
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
// Wait for the disk to disappear
assert!(wait_until(Duration::from_secs(20), || guest
.ssh_command("lsblk | grep -c vdc.*16M || true")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or(1) == 0)));
guest.reboot_linux(1);
// Check device still absent
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc.*16M || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_virtio_block_topology(guest: &Guest, loop_dev: &str) {
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!("path={loop_dev}").as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// MIN-IO column
assert_eq!(
guest
.ssh_command("lsblk -t| grep vdc | awk '{print $3}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4096
);
// PHY-SEC column
assert_eq!(
guest
.ssh_command("lsblk -t| grep vdc | awk '{print $5}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4096
);
// LOG-SEC column
assert_eq!(
guest
.ssh_command("lsblk -t| grep vdc | awk '{print $6}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4096
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_net_hotplug(
guest: &Guest,
max_num_pci_segments: u16,
pci_segment: Option<u16>,
) {
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()
.default_net()
.default_disks()
.capture_output();
if pci_segment.is_some() {
cmd.default_kernel_cmdline_with_platform(Some(&format!(
"num_pci_segments={max_num_pci_segments}"
)));
} else {
cmd.default_kernel_cmdline();
}
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let r = std::panic::catch_unwind(|| {
// Add network
let (cmd_success, cmd_output, _) = 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,
if let Some(pci_segment) = pci_segment {
format!(",pci_segment={pci_segment}")
} else {
String::new()
}
)
.as_str(),
),
);
assert!(cmd_success);
if let Some(pci_segment) = pci_segment {
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
"{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
)));
} else {
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")
);
}
// Wait for the hotplugged network interface to appear
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("ip -o link | wc -l")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or_default() == 3)
}));
// Test the same using the added network interface's IP
assert_eq!(
ssh_command_ip(
"ip -o link | wc -l",
&guest.network.guest_ip1,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT
)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
3
);
// Remove network and wait for it to disappear
assert!(remote_command(&api_socket, "remove-device", Some("test0"),));
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("ip -o link | wc -l")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or_default() == 2)
}));
// Add network
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-net",
Some(
format!(
"id=test1,tap=,mac={},ip={},mask=255.255.255.128{}",
guest.network.guest_mac1,
guest.network.host_ip1,
if let Some(pci_segment) = pci_segment {
format!(",pci_segment={pci_segment}")
} else {
String::new()
}
)
.as_str(),
),
);
assert!(cmd_success);
if let Some(pci_segment) = pci_segment {
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
"{{\"id\":\"test1\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
)));
} else {
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test1\",\"bdf\":\"0000:00:06.0\"}")
);
}
// Wait for the hotplugged network interface to appear
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("ip -o link | wc -l")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or_default() == 3)
}));
guest.reboot_linux(0);
// 2 network interfaces + default localhost ==> 3 interfaces
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
3
);
// Test the same using the added network interface's IP
assert_eq!(
ssh_command_ip(
"ip -o link | wc -l",
&guest.network.guest_ip1,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT
)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
3
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_counters(guest: &Guest) {
let api_socket = temp_api_path(&guest.tmp_dir);
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args(["--net", guest.default_net_string().as_str()])
.args(["--api-socket", &api_socket])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let orig_counters = get_counters(&api_socket);
guest
.ssh_command("dd if=/dev/zero of=test count=8 bs=1M")
.unwrap();
let new_counters = get_counters(&api_socket);
// Check that all the counters have increased
assert!(new_counters > orig_counters);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_watchdog(guest: &Guest) {
let api_socket = temp_api_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args(["--net", guest.default_net_string().as_str()])
.args(["--watchdog"])
.args(["--api-socket", &api_socket])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let mut expected_reboot_count = 1;
// Enable the watchdog with a 15s timeout
enable_guest_watchdog(guest, 15);
assert_eq!(get_reboot_count(guest), expected_reboot_count);
assert_eq!(
guest
.ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Allow some normal time to elapse to check we don't get spurious reboots
thread::sleep(std::time::Duration::new(40, 0));
// Check no reboot
assert_eq!(get_reboot_count(guest), expected_reboot_count);
// Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns.
guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap();
// Allow some time for the watchdog to trigger (max 30s) and reboot to happen
guest.wait_vm_boot_custom_timeout(120).unwrap();
// Check a reboot is triggered by the watchdog
expected_reboot_count += 1;
assert_eq!(get_reboot_count(guest), expected_reboot_count);
#[cfg(target_arch = "x86_64")]
{
// Now pause the VM and remain offline for 30s
assert!(remote_command(&api_socket, "pause", None));
let latest_events = [
&MetaEvent {
event: "pausing".to_string(),
device_id: None,
},
&MetaEvent {
event: "paused".to_string(),
device_id: None,
},
];
assert!(check_latest_events_exact(&latest_events, &event_path));
assert!(remote_command(&api_socket, "resume", None));
// Check no reboot
assert_eq!(get_reboot_count(guest), expected_reboot_count);
}
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_pvpanic(guest: &Guest) {
let api_socket = temp_api_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut cmd = GuestCommand::new(guest);
cmd.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args(["--net", guest.default_net_string().as_str()])
.args(["--pvpanic"])
.args(["--api-socket", &api_socket])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Trigger guest a panic
make_guest_panic(guest);
// Wait for the panic event to be recorded
let expected_sequential_events = [&MetaEvent {
event: "panic".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(10), || {
check_latest_events_exact(&expected_sequential_events, &event_path)
}));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_tap_from_fd(guest: &Guest) {
// Create a TAP interface with multi-queue enabled
let num_queue_pairs: usize = 2;
use std::str::FromStr;
let taps = net_util::open_tap(
Some("chtap0"),
Some(std::net::IpAddr::V4(
std::net::Ipv4Addr::from_str(&guest.network.host_ip0).unwrap(),
)),
None,
&mut None,
None,
num_queue_pairs,
Some(libc::O_RDWR | libc::O_NONBLOCK),
)
.unwrap();
let mut child = GuestCommand::new(guest)
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args([
"--net",
&format!(
"fd=[{},{}],mac={},num_queues={}",
taps[0].as_raw_fd(),
taps[1].as_raw_fd(),
guest.network.guest_mac0,
num_queue_pairs * 2
),
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
guest.reboot_linux(0);
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// test creates two macvtap interfaces in 'bridge' mode on the
// same physical net interface, one for the guest and one for
// the host. With additional setup on the IP address and the
// routing table, it enables the communications between the
// guest VM and the host machine.
// Details: https://wiki.libvirt.org/page/TroubleshootMacvtapHostFail
pub(crate) fn _test_macvtap(
guest: &Guest,
hotplug: bool,
guest_macvtap_name: &str,
host_macvtap_name: &str,
) {
let api_socket = temp_api_path(&guest.tmp_dir);
let phy_net = "eth0";
// Clean up any stale macvtap interfaces from previous test runs
exec_host_command_status(&format!(
"sudo ip link del {guest_macvtap_name} 2>/dev/null"
));
exec_host_command_status(&format!("sudo ip link del {host_macvtap_name} 2>/dev/null"));
// Create a macvtap interface for the guest VM to use
assert!(
exec_host_command_status(&format!(
"sudo ip link add link {phy_net} name {guest_macvtap_name} type macvtap mod bridge"
))
.success()
);
assert!(
exec_host_command_status(&format!(
"sudo ip link set {} address {} up",
guest_macvtap_name, guest.network.guest_mac0
))
.success()
);
assert!(exec_host_command_status(&format!("sudo ip link show {guest_macvtap_name}")).success());
let tap_index =
fs::read_to_string(format!("/sys/class/net/{guest_macvtap_name}/ifindex")).unwrap();
let tap_device = format!("/dev/tap{}", tap_index.trim());
assert!(exec_host_command_status(&format!("sudo chown $UID.$UID {tap_device}")).success());
let cstr_tap_device = CString::new(tap_device).unwrap();
let tap_fd1 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) };
assert!(tap_fd1 > 0);
let tap_fd2 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) };
assert!(tap_fd2 > 0);
// Create a macvtap on the same physical net interface for
// the host machine to use
assert!(
exec_host_command_status(&format!(
"sudo ip link add link {phy_net} name {host_macvtap_name} type macvtap mod bridge"
))
.success()
);
// Use default mask "255.255.255.0"
assert!(
exec_host_command_status(&format!(
"sudo ip address add {}/24 dev {}",
guest.network.host_ip0, host_macvtap_name
))
.success()
);
assert!(
exec_host_command_status(&format!("sudo ip link set dev {host_macvtap_name} up")).success()
);
let mut guest_command = GuestCommand::new(guest);
guest_command
.default_cpus()
.default_memory()
.default_kernel_cmdline()
.default_disks()
.args(["--api-socket", &api_socket]);
let net_params = format!(
"fd=[{},{}],mac={},num_queues=4",
tap_fd1, tap_fd2, guest.network.guest_mac0
);
if !hotplug {
guest_command.args(["--net", &net_params]);
}
let mut child = guest_command.capture_output().spawn().unwrap();
if hotplug {
// Wait for the VMM process to listen to the API socket
assert!(wait_until(Duration::from_secs(10), || remote_command(
&api_socket,
"ping",
None
)));
// Hotplug the virtio-net device
let (cmd_success, cmd_output, _) =
remote_command_w_output(&api_socket, "add-net", Some(&net_params));
assert!(cmd_success);
#[cfg(target_arch = "x86_64")]
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"_net2\",\"bdf\":\"0000:00:05.0\"}")
);
#[cfg(target_arch = "aarch64")]
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"_net0\",\"bdf\":\"0000:00:05.0\"}")
);
}
// The functional connectivity provided by the virtio-net device
// gets tested through wait_vm_boot() as it expects to receive a
// HTTP request, and through the SSH command as well.
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
guest.reboot_linux(0);
assert_eq!(
guest
.ssh_command("ip -o link | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
2
);
});
kill_child(&mut child);
exec_host_command_status(&format!("sudo ip link del {guest_macvtap_name}"));
exec_host_command_status(&format!("sudo ip link del {host_macvtap_name}"));
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
pub(crate) fn _test_vdpa_block(guest: &Guest) {
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(guest)
.default_cpus()
.args(["--memory", "size=512M,hugepages=on"])
.default_kernel_cmdline_with_platform(Some("num_pci_segments=2,iommu_segments=1"))
.default_disks()
.default_net()
.args(["--vdpa", "path=/dev/vhost-vdpa-0,num_queues=1"])
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check both if /dev/vdc exists and if the block size is 128M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 128M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Check the content of the block device after we wrote to it.
// The vpda-sim-blk should let us read what we previously wrote.
guest
.ssh_command("sudo bash -c 'echo foobar > /dev/vdc'")
.unwrap();
assert_eq!(
guest.ssh_command("sudo head -1 /dev/vdc").unwrap().trim(),
"foobar"
);
// Hotplug an extra vDPA block device behind the vIOMMU
// Add a new vDPA device to the VM
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-vdpa",
Some("id=myvdpa0,path=/dev/vhost-vdpa-1,num_queues=1,pci_segment=1,iommu=on"),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"myvdpa0\",\"bdf\":\"0001:00:01.0\"}")
);
// Wait for the hotplugged device to appear
assert!(wait_until(Duration::from_secs(10), || guest
.does_device_vendor_pair_match("0x1057", "0x1af4")
.unwrap_or_default()));
assert!(
guest
.ssh_command("ls /sys/kernel/iommu_groups/*/devices")
.unwrap()
.contains("0001:00:01.0")
);
// Check both if /dev/vdd exists and if the block size is 128M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdd | grep -c 128M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write some content to the block device we've just plugged.
guest
.ssh_command("sudo bash -c 'echo foobar > /dev/vdd'")
.unwrap();
// Check we can read the content back.
assert_eq!(
guest.ssh_command("sudo head -1 /dev/vdd").unwrap().trim(),
"foobar"
);
// Unplug the device
let cmd_success = remote_command(&api_socket, "remove-device", Some("myvdpa0"));
assert!(cmd_success);
// Wait for the device to disappear
assert!(wait_until(Duration::from_secs(10), || guest
.ssh_command("lsblk | grep -c vdd || true")
.is_ok_and(|s| s.trim().parse::<u32>().unwrap_or(1) == 0)));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}